启发:从MNS事务消息谈分布式事务

事务消息本质上解决的问题是业务系统与消息系统之间的事务问题(跨系统分布式事务),其基本原理即两阶段提交以及最终一致性保障。最近看了下阿里云mns事务消息的实现原理,介绍的蛮简洁透彻的,对了解分布式事务实现原理挺有帮助,在阅读本文前推荐大家先仔细阅读下阿里云”mns事务消息”一文。

事务消息


背景描述

有时候我们需要实现本地操作和消息发送的事务一致性功能。即:消息发送成功,则本地操作成功;反之,如果消息发送失败,本地操作失败(成功也需要rollback)。保证不出现操作成功但消息发送失败;或者操作失败但消息发送成功的情况; 另外,消费端,我们也希望消息一定被成功处理一次,不会因为消息端程序崩溃而导致消息没有成功处理,进而需要人工重置消费进度。

解决方案

利用消息服务MNS的延迟消息功能来实现。

准备工作

创建两个队列:

  • 1.事务消息队列 消息的有效期小于消息延迟时间。即如果生产者不主动修改(提交)消息可见时间,消息对消费者不可见;
  • 2.操作日志队列 记录事务消息的操作记录信息。消息延迟时间为事务操作超时时间。日志队列中的消息确认(删除)后将对消费者不可见。

具体步骤

  • 1.发送一条事务准备消息到事务消息队列;
  • 2.写操作日志信息到操作日志队列,日志中包含步骤1消息的消息句柄;
  • 3.执行本地事务操作;
  • 4.如果步骤3成功,提交消息(消息对消费者可见);反之,回滚消息;
  • 5.确认步骤2中的操作日志(删除该日志消息);
  • 6.步骤4后,消费者可以接收到事务消息;
  • 7.消费者处理消息;
  • 8.消费者确认删除消息; 如下图:

异常分析:

生产者异常(例如:进程重启):

A.读取操作日志队列超时未确认日志 B.检查事务结果 C.如果检查得到事务已经成功,则提交消息(重复提交无副作用,同一句柄的消息只能成功提交一次) D.确认操作日志

消费者异常(例如:进程重启):

消息服务提供至少保证消费一次的特性,只要步骤8不成功,消息在一段时间后可以继续可见,被当前消费者或者其他消费者处理。

消息服务不可达(例如:断网)

消息发送和接收处理状态以及操作日志都在消息服务端,消息服务本身具备高可靠和高可用的特点,所以只要网络恢复,事务可以继续,能保证只要生产者:操作成功,则消费者一定能够拿到消息并处理成功;或操作失败, 则消费者收不到消息的最终一致性。

原文地址

在mns消息模型中两阶段提交的体现是:

  • 1.在执行事务前先preSendMessage:其背后的原理是创建一个delay message,但是这个delay message的delaytime > lifetime, 基于这个前提在得到确切的commit/rollback操作前,这个消息对于接受者是永远不可见的;
  • 2.本地事务结束后commit/rollback message:如果本地事务提交成功,需要将之前提交的delay message设置为消费者可见(底层实现应该与将delay变为0类似);对应的如果本地事务提交失败,需要将之前的delay message删除;

这个过程需要注意到,我们务必保证在preSendMessage没得到最终确认之前不被消费者获取到,因此需要将发送的lifetime小于delaytime。

看到这里也许你有疑问,为什么要将过程切分成两阶段提交?我们先假设如果采用一次提交的策略,很显然这次提交的切入点只能存在于①本地事务开始之前②本地事务中③本地事务结束之后,那么先看这三个切入点各自存在什么问题。

  • ①本地事务开始之前提交消息:在本地事务未完成之前,消息的消费者读取到了message,如果消费者后续的服务调用中存在对该次本地事务提交有依赖,那必然导致数据不一致问题;如果本地事务的执行结果是失败的,却通知了消费者,很显然会导致不可预期的数据错误。
  • ②本地事务中:在本地事务中提交消息同样会存在①中的问题,即便sendmessage是在本地事务的最后执行,因为事务的提交和消息被接受到的时序是无法保证的;
  • ③本地事务结束之后:不同于①②两个提交点,本地事务完成之后我们能够明确的知道本地事务的执行结果,因此能够确保事务提交(回滚)与消息被接受是有序的;然而如果消息没有被成功发送消费者接受不到消息,而本地事务却得到了正确执行,这就导致了数据不一致问题,并且如果没有操作日志,这个问题将变得难以追溯;

”单次提交“遇到的主要问题是:无法保障本地事务与消息被接受到的时序问题(或者说两个分布式事务的时序)以及数据的一致性问题。再回到”两阶段提交“,两阶段提交能解决这两个问题吗?两阶段提交的确认操作是在本地事务完成之后(这个类似于③),因此其能够解决时序问题,但是如果这个确认操作执行的过程中发生了宕机等情况导致确认操作失败,依然会导致数据不一致问题。

在mns事务消息中最终一致性的实现:

mns通过延迟消息机制实现了两阶段提交,其如何保证数据一致性问题呢?一般我们的策略都是通过操作流水来进行补偿以达到数据的最终一致性,同样的mns也是基于这个原理实现。

  • 在preSendMessage之后,mns会在日志队列中记录一条opLog(opLog通过记录preSendMessage的receipthandle来进行关联),并且将这个opLog的delayTime设置为事务的超时时间;
  • 当本地事务执行结束,并且preSendMessage被commit/rollback之后,再将这条opLog删除;
  • 同时存在一个任务监听日志队列,当接收到opLog的消息,检查对应的preSendMessage相关联的本地事务是否执行成功。如果本地事务执行成功,则通过opLog中保存的receipthandle补偿一次对preSendMessage的commit操作,如果checker发现本地事务执行失败,那对应的补偿一次rollback操作;

通过建立对opLog的监听,我们能够确保事务的最终一致性吗?回答这个问题前,我们先看这个问题的本质:最终、一致性。 最终一致性问题的产生是由于发生了一些不可预期的问题,导致一个事务被提交(回滚),但消息没被commit(rollback)。我们通过opLog来追溯那些没有得到最终确认的消息并进行补偿(最终),并且通过检查本地事务的状态来确认这次补偿是commit或者是rollback(一致性)。正是基于这个补偿的策略,mns事务消息解决了”两阶段提交”所遗留的一致性问题,但这个过程中我们需要注意几个细节:

  • 补偿策略执行的时候需要明确知道本地事务的执行结果,因此我们的本地事务中需要记录preSendMessage所关联的本地事务操作结果。我们的做法是本地事务中同时记录下preSendMessage的receipthandle, 当补偿任务执行的时候,会通过opLog关联的receipthandle来检查,如果没有找到相关记录,那认为之前的本地事务被rollback了,否则commit;
  • MNS如何建立了preSendMessage<=>Local Transaction<=>opLog之间的关联关系?最简单的实现肯定是通过preSendMessage的MessageId来实现,不过mns通过preSendMessage的receipthandle来建立了这个关联(ReceiptHandle含义)同时避免了额外的存储;
  • mns的补偿机制建立在对opLog的监听,那么我们怎么确定一个补偿的执行时机是合适的呢?补偿一定要在事务有明确结果之后执行才有意义,那么什么时候能得到明确的事务执行结果?其实我们是无法确切的知道这个时间点的,但我们能够有一个最低期望时间:不管一个事务成功或者失败,它的周期都不能超过事务的超时时间。因此我们在发送opLog时需要设置opLog的delayTime>TransactionTimeout(如何确认transactionTimeout)来保证补偿任务执行的时候本地事务一定执行完成。

从mns事务消息到分布式事务的启发

上面啰嗦的写了一堆,看到这我们不妨对思考下mns事务消息解决的是业务系统(本地事务)与消息中间件之间的事务协同问题,如果是两个业务系统之间的分布式事务如何实现? 好吧,如果坚持看到这,你可能觉得我标题党了…那么我建议你再读一下”mns事务消息“一文。