分布式事务的几种解决方式

2阶段提交(2PC, 3PC等)

2阶段提交是分布式事务传统解决方案,现今为止还广泛存在。

当一个事务跨越多个节点时,为了保持事务ACID特性
需要引入一个作为协调者来统一掌控所有节点(称作参与者)的操作结果

并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。

因此,二阶段提交的算法思路可以概括为:
参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

以开会为例

甲乙丙丁四人要组织一个会议,需要确定会议时间,不妨设甲是协调者,乙丙丁是参与者。

投票阶段

1.甲发邮件给乙丙丁,周二十点开会是否有时间;
2.乙回复有时间;
3.丙回复有时间;
4.丁迟迟不回复,此时对于这个活动,甲乙丙均处于阻塞状态,算法无法继续进行;
5.丁回复有时间(或者没有时间);

提交阶段

1.协调者甲将收集到的结果反馈给乙丙丁(什么时候反馈,以及反馈结果如何,在此例中取决与丁的时间与决定);
2.乙收到;
3.丙收到;
4.丁收到;

不仅要锁住参与者的所有资源,而且要锁住协调者资源,开销大。

一句话总结就是:2PC效率很低,对高并发很不友好。

柔性事务

所谓柔性事务是相对强制锁表的刚性事务而言(TCC就是柔性事务)。

流程入下:服务器A的事务如果执行顺利,那么事务A就先行提交,如果事务B也执行顺利,则事务B也提交,整个事务就算完成。

但是如果事务B执行失败,事务B本身回滚,这时事务A已经被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态。

缺点是业务侵入性太强,还要补偿操作,缺乏普遍性,没法大规模推广。

消息最终一致性

可以自己写一个可靠消息服务,实现一些业务逻辑。

第一阶段:上游应用执行业务并发送MQ消息

1.上游应用发送待确认消息到可靠消息系统
2.可靠消息系统保存待确认消息并返回
3.上游应用执行本地业务
4.上游应用通知可靠消息系统确认业务已执行并发送消息。

可靠消息系统修改消息状态为发送状态并将消息投递到 MQ 中间件。

详解

首先,上游服务需要发送一条消息给可靠消息服务。

这条消息说白了,你可以认为是对下游服务一个接口的调用,里面包含了对应的一些请求参数。

然后,可靠消息服务就得把这条消息存储到自己的数据库里去,状态为“待确认”。
接着,上游服务就可以执行自己本地的数据库操作,根据自己的执行结果,再次调用可靠消息服务的接口。

如果本地数据库操作执行成功了,那么就找可靠消息服务确认那条消息。
如果本地数据库操作失败了,那么就找可靠消息服务删除那条消息。

此时如果是确认消息,那么可靠消息服务就把数据库里的消息状态更新为“已发送”,同时将消息发送给MQ。

这里有一个很关键的点,就是更新数据库里的消息状态和投递消息到MQ。
这俩操作,你得放在一个方法里,而且得开启本地事务。

  • 如果数据库里更新消息的状态失败了,那么就抛异常退出了,就别投递到MQ;
  • 如果投递MQ失败报错了,那么就要抛异常让本地数据库事务回滚。
  • 这俩操作必须得一起成功,或者一起失败。

如果上游服务是通知删除消息,那么可靠消息服务就得删除这条消息。

第二阶段:下游应用监听 MQ 消息并执行业务

下游应用监听 MQ 消息并执行业务,并且将消息的消费结果通知可靠消息服务。

1.下游应用监听 MQ 消息组件并获取消息
2.下游应用根据 MQ 消息体信息处理本地业务
3.下游应用向 MQ ACK确认
4.确认消息被消费
5.下游应用通知可靠消息系统消息被成功消费,可靠消息将该消息状态更改为“已完成”

详解

下游服务就一直等着从MQ消费消息好了,如果消费到了消息,那么就操作自己本地数据库。

如果操作成功了,就反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把消息的状态设置为“已完成”。

如何保证上游服务对消息的100%可靠投递?

如果在上述投递消息的过程中各个环节出现了问题该怎么办?

如果上游服务给可靠消息服务发送待确认消息的过程出错了,那没关系,上游服务可以感知到调用异常的,就不用执行下面的流程了,这是没问题的。

如果上游服务操作完本地数据库之后,通知可靠消息服务确认消息或者删除消息的时候,出现了问题。

比如:没通知成功,或者没执行成功,或者是可靠消息服务没成功的投递消息到MQ。这一系列步骤出了问题怎么办?

其实也没关系,因为在这些情况下,那条消息在可靠消息服务的数据库里的状态会一直是“待确认”。

此时,我们在可靠消息服务里写一个后台定时运行的线程,不停的检查各个消息的状态。

如果一直是“待确认”状态,就认为这个消息出了点什么问题。

此时的话,就可以回调上游服务提供的一个接口,问问说,兄弟,这个消息对应的数据库操作,你执行成功了没啊?

如果上游服务答复说,我执行成功了,那么可靠消息服务将消息状态修改为“已发送”,同时投递消息到MQ。
如果上游服务答复说,没执行成功,那么可靠消息服务将数据库中的消息删除即可。

通过这套机制,就可以保证,可靠消息服务一定会尝试完成消息到MQ的投递。

如何保证下游服务对消息的100%可靠接收?

那如果下游服务消费消息出了问题,没消费到?或者是下游服务对消息的处理失败了,怎么办?

其实也没关系,跟上面一样,写一个后台定时运行的线程,不断的检查消息状态。

如果消息状态一直是“已发送”,始终没有变成“已完成”,那么就说明下游服务始终没有处理成功。

此时可靠消息服务就可以再次尝试重新投递消息到MQ,让下游服务来再次处理。
只要下游服务的接口逻辑实现幂等性,保证多次处理一个消息,不会插入重复数据即可。

总结

在上面的通用方案设计里,完全依赖可靠消息服务的各种自检机制来确保:

1.如果上游服务的数据库操作没成功,下游服务是不会收到任何通知
2.如果上游服务的数据库操作成功了,可靠消息服务死活都会确保将一个调用消息投递给下游服务,而且一定会确保下游服务务必成功处理这条消息。

通过这套机制,保证了基于MQ的异步调用/通知的服务间的分布式事务保障。


阿里开源的RocketMQ,就实现了可靠消息服务的所有功能,核心思想跟上面类似。
但问题是缺乏文档,无论是在Apache项目主页,还是在阿里的页面上,最多只告诉你如何用,而原理性或者指导性的东西非常缺乏。

  • 对分布式事务的几种实现方式的形象归纳:

    2PC:你每天上班,要经过一条10公里的只有两条车道的马路到达公司。这条路可能会很堵(大几率),经常需要两三个小时,上班时间没有保证,慢。

    柔性事务:选择一条很绕,长30公里但很少堵车的路。上班时间有保证,但是必须早起,付出足够的时间和汽油。这是柔性事务的问题,必须用具体业务来回滚,很难模块化,代码量增多,需要额外的开发成本。

    消息最终一致性:选择一条有点绕,长20公里的山路,路不平,只有suv可以走,这是事务消息最终一致性问题。引入了新的消息中间件,需要额外的开发成本。


摘自:
Spring Cloud分布式事务终极解决方案探讨
石杉的架构笔记

------本文结束感谢阅读------
0%