分布式事务常用的几种实现
前言
目前,业内解决分布式事务问题,都基本不用JTA这种强一致性的解决方案,基本是采用如下两套方案:
- 基于TCC的事务框架
- 消息队列
JTA强一致性的解决方案有那些?
graph LR
服务C-->数据库C
服务C-->服务A
服务C-->服务B
服务A-->数据库A
服务B-->数据库B
- 服务A和服务B,如果是同步调用,要求一起成功,或者一起失败,那么此时应选用TCC的事务框架
- 服务A和服务B,如果是异步调用,比如服务C先调用服务A后,服务C不用管服务B的执行结果,直接返回,那么这种情况下,应选用消息队列
异步方式如何回滚?
一、基于消息队列 [异步服务]
举一个扣款例子,服务A扣款,服务B增加余额。
事务开始
(1)给账户zhangsan,扣100元
(2)将(给账户zhaosi,加100元)封装为消息,发送给消息队列
事务结束
如何保证第一步和第二步是在同一个事务里完成的,如何保证两者的一致性。在事务结束后,第一步操作和第二步操作都能成功,或者有补偿机制?
此时可以在服务A中添加一个事务表,用于记录事务记录。此时事务流程如下:
事务开始
(1)给支付宝账户zhangsan,扣100元
(2)给事件表插入一条记录 (记录包含用户转账记录和此转账记录的状态 如 'unfinished')
事务结束
此时是对同一数据库的两张表操作,因此可以用数据库的事务进行保证。
另外,起一个定时程序,定时扫描事务表,发现一个状态为'UNFINISHED'的事件,就进行封装为消息,发送到消息中间件,然后将状态改为'FINISHED'.
幂等性解决:
注意了,这一版还存在一个幂等性问题!
仔细看,定时程序做了如下三个操作
(1)定时扫描事务表,发现一个状态为'UNFINISHED'的事件
(2)将事件信息,封装为消息,发送到消息中间件
(3)将事件状态改为'FINISHED'
OK,假设在步骤(2)的时候,发送完消息体,还未执行步骤(3),定时程序阵亡了!然后重启定时程序,发现刚那个事务的状态依然为'UNFINISHED',因此重新发送。这样,就会出现重复消费问题。因此,幂等性也是需要保证的!
此时解决方案可以通过在消息队列中的消费侧,也维护一个带主键的表,可以选txid为主键。
如果一旦出现重复消费,则在事务里直接报出主键冲突错误,从而保证了幂等性!
二、基于TCC [同步服务]
如果服务A和服务B之间是同步调用,比如服务C需要按流程调服务A和服务B,服务A和服务B要么一起成功,要么一起失败。
针对这种情况,目前业内普遍推荐使用TCC事务来解决的!
简单说方案对代码侵入性强,每套业务逻辑、都要按try(请求资源)、confirm(操作资源)、cancel(取消资源),拆分为三个接口!
存在的问题:
(1)cancel或者confirm出现异常了,你怎么处理?
(2)大量逻辑重复
三、对接第三方平台
如果碰到,不同平台之间调用,你要怎么保证事务?比如,我的服务要调银行接口,你觉得可能让银行接你的tcc框架么?或者让银行接你的消息对列?你们觉得现实么?如何保证自己的应用中与第三方平台数据的一致性?
业内针对这种涉及到第三方接口的服务调用,如何保证一致性?
最终一致性保证,即最大努力通知策略。
最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等。
比如对接支付宝的业务,支付成功后,支付宝发送给业务平台支付成功的回调,业务平台收到通知后1入库2返回支付宝接口成功标识。如果1入库失败,则支付宝会重复进行通知。不存在1成功2失败的情况下,因为是同步的,1失败就执行不到2的逻辑。另外重要的一点是如果支付宝一直通知不到业务平台时,支付宝也支持通过查询的方式获取结果。中间可以加中间状态,支付中等。
References:
- 老生常谈——利用消息队列处理分布式事务
- 分布式事务之TCC事务模型
- 分布式场景下基于重试机制的一致性解决方案
- https://www.jianshu.com/p/b264a196b177
- https://mp.weixin.qq.com/s/tbEgefNtGz66pSJiT1042Q