在分布式、微服务大行其道的今天,分布式系统成为了标配。而说到使用分布式,或者拆分微服务的好处,你肯定能想到一大堆。既然有了分布式系统,那不可避免的就会用到分布式事务,这也是面试过程中经常会问的。暂不说你有没有真正使用过分布式事务,起码得对分布式事务有一定的了解。常见的有几种解决方案,各自有什么优缺点等。本文就来扒一扒分布式事务的解决方案,各一线大厂是怎么面对这个问题的。
概念
什么是分布式事务?简单的说,就是一次大操作由不同小操作组成,这些小操作分布在不同服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。举个例子:你上淘宝买东西,需要先扣钱,然后商品库存 -1。而服务架构设计为了方便水平扩展,扣款和库存分别属于两个服务,这两个服务中间要经过网络、网关、主机等一系列中间层,如果任何一个地方出了问题,比如网络抖动、突发异常等待,都会导致数据不一致,比如扣款成功了,但是库存没-1,就会出现超卖的现象,而这就是分布式事务需要解决的问题。
基本原理
对于分布式系统绕不开的理论就是CAP理论,它是分布式系统中问题产生的根源。任何系统要实现高可用就不得不遵循CAP理论。CAP由Eric Brewer在2000年PODC会议上提出,是Eric Brewer在Inktomi期间研发搜索引擎、分布式web缓存时得出的关于数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)的猜想。对于分布式系统最多只能同时拥有CAP其中的两个,没法三者兼顾。如果你能找到一种方案解决CAP问题,肯定能名留青史,哈哈哈哈。好了,废话不多说,简单讲一下这个理论
- 一致性(Consistency):分布式环境下多个节点的数据是否强一致
- 可用性(Availability):分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果
- 分区容忍性(Partition-tolerance):在网络分区的情况下,被分隔的节点仍能正常对外服务, 由于分布式系统通常由多个节点构成,由于网络是不可靠的,所以存在分布式集群中的节点因为网络通信故障导致被孤立成一个个小集群的可能性,即网络分区,分区容错性要求在出现网络分区时系统仍然能够对外提供一致性的可用服务。
一些常用的分布式中间件对于CAP做了取舍,如选择AP,弱化C的有Cassandra、Dynamo 等。选择CP,弱化A的有HBase、MongoDB 等。Eric Brewer 教授在 2012 年就曾指出 CAP 理论证明不能同时满足一致性、可用性,以及分区容错性的观点在实际系统设计指导上存在一定的误导性。 传统对于 CAP 理论的理解认为在设计分布式系统时必须满足 P,然后在 C 和 A 之间进行取舍,这是片面的。实际中网络出现分区的可能性还是比较小的,尤其是目前网络环境正在变得越来越好,甚至许多系统都拥有专线支撑,所以在网络未出现分区时,还是应该兼顾 A 和 C。另外就是对于一致性、可用性,以及分区容错性三者在度量上也应该有一个评定范围,最简单的以可用性来说,当有多少占比请求出现响应超时才可以被认为是不满足可用性,而不是一出现超时就认为是不可用的。最后我们需要考虑的一点就是分布式系统一般都是一个比较大且复杂的系统,我们应该从更小的粒度上对各个子系统进行评估和设计,而不是简单的从整体上武断决策。
事务的概念
刚性事务
在使用数据库时对于事务的概念应该很熟悉了,数据库事务有一个基本特征,即ACID原则,只要支持事务的数据库,都会遵循这个原则
原子性(Atomicity)
:所谓原子性是指,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。一致性(Consistency)
:事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B 50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。隔离性(Isolation)
:所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。持久性(Durability)
:如果事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。
ACID是4大原则的英文首字母缩写,对于遵循ACID原则的事务,也称之为刚性事务。它是强一致性的,我们日常使用的Mysql,Oracle都是满足ACID事务的数据库
柔性事务
在分布式系统的事务中,基本是不可能像刚性事务那样满足ACID的,因此诞生了一个适用于分布式系统的事务原则,即BASE原则。
基本可用(Basically Available)
:指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务,等。软状态(Soft State)
:指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。最终一致性(Eventual Consistency)
:系统中的各节点数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。
BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。对于BASE原则的事务也称之为柔性事务。柔性事务分为:两阶段型,补偿型,异步确保型,最大努力通知型几种。 其中最大努力通知型由于支付宝提出的
让分布式集群始终对外提供可用的一致性服务一直是富有挑战和趣味的任务。暂且抛开可用性,拿一致性来说,对于关系型数据库我们通常利用事务来保证数据的强一致性,但是当我们的数据量越来越大,大到单库已经无法承担时,我们不得不采取分库分表的策略对数据库实现水平拆分,或者引入 NoSQL 技术,构建分布式数据库集群以分摊读写压力,从而提升数据库的存储和响应能力,但是多个数据库实例也为我们使用数据库带来了许多的限制,比如主键的全局唯一、联表查询、数据聚合等等,另外一个相当棘手的问题就是数据库的事务由原先的单库事务变成了现在的分布式事务。
两阶段提交协议(2PC:Two-Phase Commit)
二阶段提交是分布式事务传统解决方案,目前为止还广泛存在。两阶段提交协议的目标在于为分布式系统保证数据的一致性,许多分布式系统采用该协议提供对分布式事务的支持。顾名思义,该协议将一个分布式的事务过程拆分成两个阶段: 准备事务 和 事务提交 。为了让整个数据库集群能够正常的运行,该协议指定了一个 协调者 单点,用于协调整个数据库集群各节点的运行。为了简化描述,我们将数据库集群中的各个节点称为 参与者 ,三阶段提交协议中同样包含协调者和参与者这两个角色定义。
第一阶段:投票
该阶段的主要目的在于确定数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:
- 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果;
- 事务参与者收到请求之后,执行事务但不提交,并记录事务日志;
- 参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。
第二阶段:事务提交
在经过第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在 3 种可能性:
- 所有的参与者都回复能够正常执行事务。
- 一个或多个参与者回复事务执行失败。
- 协调者等待回复超时。
对于第一种情况就是我们期望的情况,协调者会向所有的参与者发出提交事务的通知。其步骤如下:
- 协调者向各个参与者发送 commit 通知,请求提交事务;
- 参与者收到事务提交通知之后执行 commit 操作,然后释放占有的资源;
- 参与者向协调者返回事务 commit 结果信息。
对于第二种和第三种情况,协调者会视为异常情况,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:
- 协调者向各个参与者发送事务 rollback 通知,请求回滚事务;
- 参与者收到事务回滚通知之后执行 rollback 操作,然后释放占有的资源;
- 参与者向协调者返回事务 rollback 结果信息。
二阶段提交的缺点
两阶段提交协议原理简单、易于实现,但是缺点也是显而易见的,如下
- 单点问题:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,就会影响整个数据库集群的正常运行。比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。
- 同步阻塞:两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率极其低下。
- 数据不一致性:两阶段提交协议虽然是分布式数据强一致性所设计,但仍然存在数据不一致性的可能性。比如在第二阶段中,假设协调者发出了事务 commit 通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
补偿型事务(TCC)
TCC (Try、Commit、Cancel) 是一种补偿型事务,该模型要求应用的每个服务提供 try、confirm、cancel 三个接口,它的核心思想是通过对资源的预留(提供中间态),尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源。我们以一个简单的电商系统为例,小明账号余额为1000元,在淘宝上花 100 元买了一本书,获赠 10 个积分,产品上有如下几个操作:
- 订单系统创建商品订单
- 支付系统接受小明的支付
- 库存系统扣减产品库存
- 会员系统给小明账户增加会员积分
这几个动作需要作为一个事务执行,要么都成功要么都失败。举个异常例子,订单的状态都修改为“已支付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑人么,当然不能允许这种情况发生了!但是如果你不用 TCC 分布式事务方案的话,就用个 Spring Cloud 开发这么一个微服务系统,很有可能会干出这种事儿来。所以说,我们有必要使用 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。
落地实现 TCC 分布式事务
那么现在到底要如何来实现一个 TCC 分布式事务,使得各个服务,要么一起成功?要么一起失败呢?首先,订单服务那儿,它的代码大致来说应该是这样子的:
public class OrderService {
// 库存服务
@Autowired
private InventoryService inventoryService;
// 积分服务
@Autowired
private CreditService creditService;
// 支付服务
@Autowired
private WmsService payService;
// 对这个订单
public void pay(){
//对本地的的订单数据库修改订单状态为"已支付"
orderDAO.updateStatus(OrderStatus.PAYED);
//调用支付服务支付金额
payService.payMoney();
//调用库存服务扣减库存
inventoryService.reduceStock();
//调用会员服务增加积分
creditService.addCredit();
}
}
光是凭借这段代码,是不足以实现 TCC 分布式事务的啊?!别着急,我们对这个订单服务修改点儿代码。首先,上面那个订单服务先把自己的状态修改为:OrderStatus.UPDATING。这是啥意思呢?也就是说,在 pay() 那个方法里,你别直接把订单状态修改为已支付啊!你先把订单状态修改为 UPDATING,也就是修改中的意思。这个状态是个没有任何含义的这么一个状态,代表有人正在修改这个状态罢了。然后呢,库存服务直接提供的那个 reduceStock() 接口里,也别直接扣减库存啊,你可以是冻结掉库存。举个例子,本来你的库存数量是 100,你别直接 100 - 2 = 98,扣减这个库存!你可以把可销售的库存:100 - 2 = 98,设置为 98 没问题,然后在一个单独的冻结库存的字段里,设置一个 2。也就是说,有 2 个库存是给冻结了。积分服务同理。上面这套改造接口的过程,其实就是所谓的 TCC 分布式事务中的第一个 T 字母代表的阶段,也就是 Try 阶段。至于Commit阶段,Cancel阶段基本上和Try阶段 改造过程雷同。只不过Commit阶段和Cancel阶段会修改真实的库存或者金额。
根据刚刚的例子,我们先来捋一捋TCC的每个阶段各个服务需要做什么事情,如果采用 TCC 事务模式,那么各个系统需要改造为如下状态
订单服务
阶段 | 动作 |
---|---|
try | 创建一个订单,状态显示为 “待支付” |
confirm | 更新订单的状态为“已完成” |
cancel | 更新订单的状态为“已取消” |
支付服务
阶段 | 动作 |
---|---|
try | 冻结小明账户中的 100 元,此时小明看到的余额依然是 1000 元 |
confirm | 将账户余额变为 900 元,并清除刚刚的冻结记录。 |
cancel | 清除刚刚的冻结记录。 |
库存服务
阶段 | 动作 |
---|---|
try | 假设库存中还有 10 本书,冻结其中的一本书,现实库存依然有 10 本书。 |
confirm | 将剩余库存更新为 9 本书,并清除冻结记录。 |
cancel | 清除刚刚的冻结记录。 |
会员服务
阶段 | 动作 |
---|---|
try | 假设小明原积分 3000 分,给小明账户预增加 10 积分,账户显示的积分依然是 3000 分。 |
confirm | 将账户积分更新为 3010,并清除预增加记录。 |
cancel | 清除预增加积分记录。 |
聊到这儿,基本上大家应该都知道 TCC 分布式事务具体是怎么回事了!大概原理就是如此,其中感知各个阶段的执行情况以及推进执行下一个阶段的这件事情(比如各个服务的Try阶段是否都执行成功),就需要依靠 TCC 分布式事务框架来推动后续的执行,自己手写实现,太复杂容易出错,我们可以使用现成的轮子。比如国内开源的 ByteTCC、Himly、TCC-Transaction。看起来我们刚刚分析的过程好像顺利成章,但仍有一些极端情况没有考虑到,比如事务要取消,所有的服务都要执行Cancel,但这时库存服务突然宕机了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?所以,TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。那万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢?那也很简单,TCC 事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功!当然了,如果你的代码没有写什么 Bug,有充足的测试,而且 Try 阶段都基本尝试了一下,那么其实一般 Confirm、Cancel 都是可以成功的!
TCC补偿事务的缺点
补偿事务逻辑清晰,很好理解,TCC 事务将分布式事务从资源层提到业务层来实现,可以让业务灵活选择资源的锁定粒度,并且全局事务执行过程中不会一直持有锁,所以系统的吞吐量比 2PC 模式要高很多。但缺点同样明显:
- 增加编码难度:原来一个接口可以做的事情,需要拆分为三个接口
- 接口支持幂等:在Confirm 和Cancel 执行异常情况下,TCC 会一直重试,直到它们都执行成功,这就要求接口需要支持重复调用的操作,即支持幂等,尤其是支付接口。
- 业务侵入性强:基本上与业务耦合在一起,使用并不灵活。例如刚刚的例子,业务流程是:订单 -> 支付 -> 库存 -> 会员。各个服务本身是不知道这个业务的调用链路的,需要在TCC上配置。如果后期增加了物流服务,链路变为:订单 -> 支付 -> 库存 -> 会员 -> 物流。需要额外的人力维护TCC
- 接口允许空回滚:原因是异常发生在Try时,部分服务没有收到 Try 请求从而触发整个事务的 cancel 操作,try 失败或者没有执行 try 操作的参与方收到 cancel 请求时,要进行空回滚操作。
- 防止资源悬挂:原因网络异常导致两个阶段无法保证严格的顺序执行,出现参与方侧 try 请求比 cancel 请求更晚到达的情况,cancel 会执行空回滚而确保事务的正确性,但是此时 try 方法也不可以再被执行。
补偿型事务(Saga)
Saga 并不是一个新概念,其相关论文在 1987 年就发布了。Saga 和 TCC 一样,也是一种补偿事务,但是它没有 try 阶段,而是把分布式事务看作一组本地事务构成的事务链。事务链中的每一个正向事务操作,都对应一个可逆的事务操作。Saga 事务协调器负责按照顺序执行事务链中的分支事务,分支事务执行完毕,即释放资源。如果某个分支事务失败了,则按照反方向执行事务补偿操作。论文地址:sagas,Saga补偿事务由两阶段组成
confirm阶段
:每个Saga由一系列sub-transaction Ti 组成cancel阶段
:每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果
假如一个 Saga 的分布式事务链有 n 个分支事务构成,[T1,T2,…,Tn],那么该分布式事务的执行情况有三种:
- T1,T2,…,Tn:n 个事务全部执行成功了。
- T1,T2,…,Ti,Ci,…,C2,C1:执行到第 i (i<=n) 个事务的时候失败了,则按照 i->1 的顺序依次调用补偿操作。如果补偿失败了,就一直重试。补偿操作可以优化为并行执行。
- T1,T2,…,Ti (失败),Ti (重试),Ti (重试),…,Tn:适用于事务必须成功的场景,如果发生失败了就一直重试,不会执行补偿操作。
举个例子,假如国庆节小明要出去玩,从北京出发,先去伦敦,在伦敦游玩三天,再去巴黎,在巴黎游玩三天,然后再返回北京。整个行程中涉及不同航空公司的机票预订以及伦敦和巴黎当地的酒店预订,小明的计划是如果任何一张机票或酒店预订不上,就取消本次出行计划。假如综合旅游出行服务平台提供这种一键下单的功能,那么这就是一个长事务,用 Saga 模式编排服务的话,就如下图所示:任何一个环节失败的话,就通过补偿操作取消前面的行程预订。
Saga对于ACID的保证和TCC一样
ACID | 解释 |
---|---|
原子性(Atomicity) | 正常情况下保证 |
一致性(Consistency) | 在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的 |
隔离性(Isolation) | 在某个时间点,A事务能够读到B事务部分提交的结果 |
持久性(Durability) | 和本地事务一样,只要commit则数据被持久 |
Saga 不保证事务隔离性的,本地事务提交后变更就对其他事务可见了。其他事务如果更改了已经提交成功的数据,可能会导致补偿操作失败。比如扣款失败,但是钱已经花掉了,业务设计上需要考虑这种场景并从业务设计上规避这种问题。要求业务设计实现上遵循三个策略:
- 允许空补偿:网络异常导致事务的参与方只收到了补偿操作指令,因为没有执行过正常操作,因此要进行空补偿。
- 保持幂等性:事务的正向操作和补偿操作都可能被重复触发,因此要保证操作的幂等性。
- 防止资源悬挂:网络异常导致事务的正向操作指令晚于补偿操作指令到达,则要丢弃本次正常操作,否则会出现资源悬挂问题。
Saga模式和TCC模式对比,设想一种场景:A->B->C->D。这种场景下,如果发生如下情况:
- B,C的数据提交成功了,但是D失败了;
- 此时另外的用户来读取BCD的值;
- ABCD进行回滚;
不管是TCC还是SAGA模式,一旦事务出现异常,都会进入回滚阶段,但是,恰巧,在BC刚数据提交成功D失败了,但是还未来得及回滚的情况下,有用户来读取BCD的值。
- TCC:由于try操作的是冻结字段,所以,其他用户在try和cancel的间隙来读数据,那么读的数据也是正确的,因为目标字段并没有发生改变。
- Saga:由于直接操作的目标字段,所以,try阶段,由于数据已经提交了,那么其他用户在try和cancel的间隙来读取数据,读到的数据,就是有问题的“脏数据”。
结论
TCC模式 | Saga模式 | |
---|---|---|
优点 | 不存在上述脏数据的问题 | 数据没有很好的隔离,有脏数据的风险 |
缺点 | 开发麻烦,每个目标字段需要一个冻结字段来支撑事务操作 | 不存在上述脏数据的问题 |
最终一致性分布式事务
前面说到的2PC,3PC等事务需要资源的全局锁定,导致性能极差,因而衍生出TCC,Saga等柔性事务的分布式事务方案。TCC
属于强一致性事务的方案,适用资金流转业务相关业务,比如:支付、交易等场景。根据 CAP
理论,这种实现需要牺牲可用性。而如果是一般的分布式事务场景,比如:订单插入之后要调用库存服务更新库存,库存数据没有资金那么的敏感,为了用户体验可以使用可靠消息最终一致性方案。下面是一种可靠消息最终一致性事务方案的详细实现流程:
正常流程按照图中的数字顺序依次执行:
- A 系统发送
预发送
消息给消息服务系统。 - 消息服务系统存储预发送的消息到消息数据库。
- 消息服务系统返回存储预发送消息的结果到 A 系统。
- 如果第 3 步返回的结果是成功的, A 系统则执行业务操作,否则不执行。
- A 系统业务操作成功后,通知消息服务系统 。
- 消息服务系统发送消息到 MQ ,并且更新
预发送
消息状态为已发送
(但不是已消费
)。 - MQ 发送消息到 B 系统。
- B 系统执行业务操作,保证幂等性,防止同一个消息重复执行。
- B 系统向 MQ 返回 ack ,消息服务系统进行确认成功消费消息,让消息服务系统将消息状态置为
已消费
。 - 消息回复系统定时去消息服务系统查一下消息数据,查看有没有状态为非已消费(
预发送
和已发送
)状态的超时(比如 2 分钟以上还未消费的)消息。 - 如果第 10 步发现有非已消费状态的超时消息,调用 A 系统提供的查询接口,查询次条消息对应的业务数据是否为处理成功。
- 如果业务数据是处理成功的状态,那么就再次调用确认并发送消息,即进入第 6 步。如果业务数据是处理失败的,那么就调用消息服务系统进行删除该条消息数据。
来看看这个流程出现异常情况的时候,这个系统是怎么保证一致性的:
- 第 1 步失败,相当于什么都没做。
- 第 2 步失败,第 3 步会返回失败结果,A 系统不执行业务操作。
- 第 3 步失败,A 系统不执行业务操作,消息恢复系统在第 12 步判断业务处理失败。
- 第 4 步失败,A 系统回滚业务,同样消息恢复系统在第 12 步判断业务处理失败。
- 第 5、6、7、8、9 步失败,消息恢复系统在第 12 步判断业务处理成功,重试第 6 步直到成功为止。如果在第 9 步失败了,B 系统会重复消费某条消息,所以 B 系统要设计成幂等操作,对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次调用而产生了副作用。
最终一致性的思想就是只要消息数据持久化了,我就假设后面一定会被消费,就算后面挂了一堆东西,但是我们把挂掉的服务再全部启动,这条消息还是会被消费,不会丢失,可以保证最终一致性。刚刚的实现流程已经具体到生产方和消费方的每个步骤,看起来比较复杂,这里我简化一下,只关注生产方对消息的处理情况,消费方和生产方处理方式是雷同的。
概括来说生产方A系统需要处理4步操作:
- 发送方将半事务消息发送至消息队列 MQ 服务端。
- 消息队列 MQ 服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。
事务消息回查步骤如下:
- 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行操作。
到这里保证了生产方A系统的事务一定是执行成功了,而且消息已经到了MQ中。剩下的就是无论如何要使得消费方B系统,把生成方A系统的消息给成功消费掉。消息最终一致性只保证最终数据一致性,在事务执行过程中两个系统仍然可用,但会出现数据不一致。此方案对消息中间件MQ要求要高一点,使用支持消息事物的 RocketMQ
可以简化消息恢复系统和消息服务系统。