一、第一种方案:能不用分布式事务就不用
明确系统是否真的需要分布式事务;
因为不论任何一种分布式解决方案都会增加你系统的复杂度,这样的成本还是挺高的,千万不要因为追求某些设计,而引入不必要的成本和复杂度。
二、第二种方案:XA 分布式事务 (MySQL是支持XA事务的)
属于2PC;
XA是由X/Open组织提出的分布式事务的规范。
X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准,也就是了定义了规范和API接口,由这个厂商进行具体的实现。这个思想在java 平台里面到处都是。
X/Open DTP 定义了三个组件: AP,TM,RM
AP(Application Program):也就是应用程序,可以理解为使用DTP的程序
RM(Resource Manager):资源管理器,这里可以理解为一个DBMS系统,或者消息服务器管理系统,应用程序通过资源管理器对资源进行控制。资源必须实现XA定义的接口
TM(Transaction Manager):事务管理器,负责协调和管理事务,提供给AP应用程序编程接口以及管理资源管理器
其中,AP 可以和TM 以及 RM 通信,TM 和 RM 互相之间可以通信,DTP模型里面定义了XA接口,TM 和 RM 通过XA接口进行双向通信,例如:TM通知RM提交事务或者回滚事务,RM把提交结果通知给TM。AP和RM之间则通过RM提供的Native API 进行资源控制,这个没有进行约API和规范,各个厂商自己实现自己的资源控制,比如Oracle自己的数据库驱动程序。
三、第三种方案:TCC 方案
属于2PC;
TCC 模型是把锁的粒度完全交给业务处理,它需要每个子事务业务都实现Try-Confirm / Cancel 接口。
Try:
尝试执行业务
完成所有业务检查(一致性)
预留必须业务资源(准隔离性)
Confirm:
确认执行业务;
真正执行业务,不作任何业务检查
只使用Try阶段预留的业务资源
Confirm 操作满足幂等性
Cancel:
取消执行业务
释放Try阶段预留的业务资源
Cancel操作满足幂等性
这三个阶段,都会按本地事务的方式执行。不同于 XA的prepare ,TCC 无需将 XA 的投票期间的所有资源挂起,因此极大的提高了吞吐量。
四、第四种方案:本地消息表+MQ
执行步骤:
- A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
- 接着 A 系统将这个消息发送到 MQ 中去;
- B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息;
- B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
- 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
- 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。
注意事项:
1.严重依赖于数据库的消息表来管理事务,高并发场景下,mysql会是个瓶颈
2.本地消息队列是 BASE 理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等
五、第五种方案:可靠消息最终一致性方案
抛弃本地的消息表,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。
消息队列 RocketMQ 事务消息交互流程如下所示:
其中:
- 发送方向消息队列 RocketMQ 服务端发送消息。
- 服务端将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
- 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半消息进行操作。
说明:事务消息发送对应步骤 1、2、3、4,事务消息回查对应步骤 5、6、7。
六、第六种方案:最大努力通知方案
这个方案的大致意思就是:
1.系统 A 本地事务执行完之后,发送个消息到 MQ;
2.这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
3.要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。
其实,RocketMQ 的消息重试,符合这个解释