原理

2PC:

实现:

  • 准备阶段(prepare),协调者向各个参与者发起询问,说要执行一个事务,各参与者可能回复YES、NO或者超时;
  • 提交阶段(commit): 如果所有参与者回复的都是YES,则事务协调者向所有参与者发起事务提交操作,即commit操作,所有参与者各自执行任务,然后发送ACK;
  • 回滚阶段(rollback):如果有一个参与者回复的是NO,或者超时了,则事务协调者向所有参与者发起事务回滚操作,所有参与者各自回滚事务,然后发送ACK。

问题:

  • 同步阻塞:同步执行过程中,所有参与节点都是事务阻塞的,无法支持高并发。
  • 单点问题:一旦协调者故障,参与者会一直阻塞下去。
  • 数据不一致:阶段1完成之后,如果在阶段2事务协调者向所有参与者发送了commit,但其中一个超时或者出错了(没有正确返回ACK),则其他参与者是提交还是回滚呢?这是一个悬而未决的问题。 只能用于单一类型数据库,跨数据库种类、其他缓存或者文件等资源不支持。

3PC:

实现:

canCommit、preCommit(写redo和undo log)和doCommit(提交或回滚)阶段。 相比二阶段提交,三阶段贴近降低了阻塞范围,(can,pre)在等待超时后协调者或者参与者会中断事务。避免了协调者单点问题。阶段3(do)协调者出现问题时,参与者会继续提交事务。但是数据不一致问题依然存在,当在参与者收到preCommit请求后等待do Commit指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

事件队列(ebay)

本地消息表+调用方重试+接收方冥等,本质上是一种最终一致性解决方案。

MQ事务消息

RocketMQ,支持的方式也是类似于二阶段提交(未开源),将消息发送拆成了两个阶段,prepare阶段和confirm阶段。

  • 阶段1:调用prepare接口,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,只是暂存。
  • 阶段2:执行本地事务。
  • 阶段3:调用confirm接口,确认发送消息。此时消息中间件才会把消息发送给消费方。 rocketmq会定时扫描所有prepare但是还没有confirm的消息,回调给业务方,询问这条消息是发送还是取消,业务方自己判断(DB更新成功还是失败)这条消息是发送还是取消。

TCC:

本质上是一个应用层面的2PC,try对应prepare,confirm对应commit,cancel对应rollback。

  • try:完成所有业务检查与资源预留
  • confirm:确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具- 备幂等设计,Confirm失败后需要进行重试。
  • cancel:取消执行,释放Try阶段预留的业务资源 Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。 每个服务提供者都要提供一组业务逻辑相反的操作,互为补偿,同时回滚操作要保证冥等。
    这里提到TCC借鉴了2PC的思路,那么它如何解决2PC的问题呢?也就是说在confirm或者cancel阶段,调用方宕机或者某个服务超时了,如何处理?答案是:不断重试,不管是confirm还是cancel失败了,都不断重试。这就要求confirm和cancel是冥等操作。注意,重试是由TCC框架来执行的,而不是业务方自己实现。

开源组件

Seata