分布式事务

基础理论

CAP理论和BASE理论

一致性的三种级别

  • 强一致性

    系统写入了什么,读出来就是什么。

  • 弱一致性

    不一定可以读取到最新写入的值,也不保证多少时间之后能读取到最新的,只尽量保证某个时刻达到数据一致的状态。

  • 最终一致性

    弱一致性的升级版。不一定可以读取到最新写入的值,但保证在一定时间内达到数据一致的状态。

业界比较推崇最终一致性,但是某些对数据一致要求非常严格的场景,比如银行转账,还是要保证强一致性。


分布式事务解决方案

XA规范的角色

XA规范是X/Open组织定义的分布式事务处理标准。

  1. AP (Application Program):应用程序本身
  2. RM(Resource Manager):资源管理器,事务的参与者,绝大部分情况下指数据库
  3. TM(Transaction Manager):事务管理器,负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等

2PC (2-Phase Commit 2阶段提交协议)


准备阶段 Prepare

准备阶段的核心是询问事务参与者执行本地数据库事务操作是否成功。工作流程为:

  1. TM向所有涉及到的RM发送消息,询问是否能执行事务操作
  2. RM接收到消息之后,执行本地数据库事务预操作,比如写redo log/undo log日志。
  3. RM如果执行本地数据库事务操作成功,就回复TM “Yes” 表示已就绪,否则回复”No”表示未就绪。

提交阶段 Commit

提交阶段的核心是询问事务参与者提交本地事务是否成功。当所有事务参与者都是就绪状态时:

  1. TM向所有事务参与者发送Commit消息,让其提交事务。
  2. RM接收到Commit消息后,执行提交本地数据库事务操作,执行完成后释放整个事务期间锁占用的资源
  3. RM发送ACK消息,表示事务已经提交。
  4. TM收到所有事务参与者的ACK消息之后,整个分布式事务过程正式结束。
image-20240612175623117

当任一事务参与者是未就绪状态:

  1. TM向所有参与者发送Rollback消息,通知执行回滚操作。
  2. RM接收到Rollback消息后,执行本地数据库事务回滚,执行完成之后,释放整个事务期间所占用的资源
  3. RM发送ACK消息,表示事务已经回滚。
  4. TM收到所有RM的ACK消息后,中断事务
image-20240612180036000

总结

  1. 准备阶段的主要目的是测试RM能否执行本地数据库事务操作。
  2. 提交阶段中,TM会根据准备阶段中RM返回的消息,来决定执行事务提交还是回滚操作。
  3. 提交阶段之后,一定会结束当前的分布式事务。

2PC的优点

  • 实现简单,各大主流数据库MySQL、Oracle等都有自己的实现。
  • 针对的是数据强一致性

2PC的缺点:

  • 同步阻塞:事务参与者在正式提交事务之前,会一直占用相关资源。比如用户小明转账给小红,其他事务想要操作小明或小红时,就会阻塞。
  • 数据不一致:由于网络问题或者TM宕机,都有可能造成数据不一致。例如提交阶段,部分网络出现问题,导致部分参与者收不到Commit/Rollback消息,导致数据不一致
  • 单点问题:如果TM在准备阶段完成之后挂掉,事务参与者们就会一直卡在提交阶段。

3PC(3阶段提交协议)

3PC在2PC的基础上做了一些优化得到,它把准备阶段做了进一步的细化,分为准备阶段CanCommit)和预提交阶段(PreCommit)。


准备阶段 CanCommit

TM向RM发送准备请求(CanCommit),顺便询问一些信息,比如事务参与者能否执行本地数据库事务操作。RM回复”Yes”、”No”或者直接超时未回复。


预提交阶段 PreCommit

如果准备阶段有任一RM回复”No”或直接超时未回复,TM就给所有RM发送Abort消息(中断请求),RM收到消息后直接中断事务。

如果准备阶段所有的RM都回复”Yes”,TM就向所有RM发送PreCommit消息(预提交请求),RM收到消息之后执行本地数据库事务预操作,比如写 redo log/undo log 日志。如果RM成功执行了事务预操作,就返回”Yes”,否则返回”No”。

预提交阶段,TM和RM都引入了超时机制,如果参与者没有收到TM的PreCommit消息,或者TM没有收到参与者返回的预执行结果状态,在超过等待时间之后,事务就会中断,从而避免事务阻塞。


执行事务提交阶段 DoCommit

如果预提交阶段有任一RM回复”No”或者直接超时未回复的话,TM就会给所有RM发送Abort消息(请求中断),RM收到消息后进行事务回滚,释放资源,中断本次事务。

TM向所有的RM发送DoCommit消息(执行事务提交请求),RM收到消息之后执行本地数据库事务提交,并在完成后释放占用的资源。 当事务提交成功后,RM返回”Yes”。

如果RM在等待时间内没有收到TM的DoCommit消息,RM会认为TM可能发生故障,但是选择直接进行事务提交,如果没有收到的TM消息是Abort消息,就会引起数据不一致。只要预提交阶段所有参与者都返回了Yes,那么只要进入第三阶段,事务基本上都能执行成功。


总结

3PC除了将2PC的准备阶段进行了进一步细化,还同时在TM和RM中引入了超时机制。如果在一定时间内没有收到事务参与者的消息,就默认失败,进而避免事务参与者一直阻塞,占用资源。

2PC中只有TM才拥有超时机制,当RM长时间无法与TM通讯的情况下(比如TM挂掉了),就会无法释放资源,产生阻塞问题。

但是3PC并没有完全解决2PC的阻塞问题,也引入了一些新的问题,比如性能糟糕,且仍然存在数据不一致的问题,因此3PC的实际应用并不广泛。多数应用会选择通过复制状态机(在多个独立的节点上运行相同的应用或服务,并确保所有节点以相同顺序执行相同操作,从而使每个节点都维护相同的状态,通常依赖于一致性协议Raft、Paxos等来同步各节点之间的状态)解决2PC的阻塞问题。


TCC 补偿事务

TCC是Try、Confirm、Cancel的缩写,分为三个阶段:

  1. Try阶段

    尝试执行。完成业务检查,并预留好必需的资源。

  2. Confirm阶段

    确认执行。当所有的RM的Try阶段执行成功之后,就会执行Confirm,处理预留好的业务资源。否则,执行Cancel。

  3. Cancel阶段

    取消执行。释放Try阶段预留的业务资源。


以转账场景为例:

  1. Try阶段检查账户余额是否充足,预留好转账资金。
  2. Confirm阶段真正执行扣钱操作。
  3. 释放Try阶段预留的转账资金。

Try阶段出现问题可以执行Cancel,如果Confirm或者Cancel阶段失败,怎么办?

TCC会记录事务日志,并持久化事务日志到某种存储介质上(本地文件、关系型数据库、Zookeeper)。事务日志包含了事务的执行状态,可 以此判断出事务提交成功还是失败,以及具体失败在哪一步。

  • 如果发现是Confirm或者Cancel阶段失败的话,会进行重试,重试次数通常为6此,超过的话就人工介入处理。如果美誉偶特殊Bug,Confirm或者Cancel阶段出现问题的概率比较小。
  • 如果事务提交成功,就可以删除对应的事务日志,节省资源。

TCC模式不需要依赖底层数据资源的事务支持,但需要手动实现更多的代码,属于侵入业务代码的分布式解决方案。

TCC和2PC的对比

TCC和2PC/3PC的区别?

  1. 2PC/3PC依赖数据库层面的事务,TCC主要通过修改业务代码实现,侵入业务代码。
  2. 2PC/3PC追求强一致性在两阶段提交的整个过程中,一直会持有数据库的锁。TCC追求最终一致性,不会一直持有各个业务资源的锁

MQ事务

RocketMQ、Kafka都提供了事务相关的功能。事务允许事件流应用将消费、处理、生产消息整个过程定义为一个原子操作

以RocketMQ为例:

image-20240614163030432

  1. MQ发送方(如物流服务)在消息队列上开启一个事务,然后发送一个半事务消息给MQ Server。事务提交之前,半事务消息对MQ消费者不可见
  2. 半事务消息发送成功,MQ发送方就开始执行本地事务。
  3. MQ发送方的本地事务执行成功的话,半事务消息就变成正常消息,可以被正常消费;本地事务执行失败的话,直接回滚

MQ发送方提交或者回滚事务消息时失败怎么办?

RocketMQ中的Broker会定期去MQ发送方上反查这个事务的本地事务执行情况,并根据反查结果决定提交或者回滚这个事务。

事务反查机制的实现依赖于 业务代码实现的对应的接口。比如要查看创建物流信息的本地事务是否执行成功,直接在数据库中查询对应的物流信息是否存在即可


如果正常消息没有被正确消费怎么办?

消息消费失败的话,RocketMQ会自动进行消费重试。如果超过最大重试次数,就会认为这个消息有问题,将其放入死信队列。进入死信队列的消费一般需要人工处理,手动排查问题。


QMQ

QMQ是去哪儿网内部广泛使用的消息中间件,包括跟交易相关的订单场景、报价搜索等高吞吐量场景。21年数据显示,公司内部日常消息qps在60w左右,生产上承载将近4W+的消息topic,消息的端到端延迟可以控制在10ms以内。

本地消息利用了关系型数据库的事务能力,会在数据库中存放一张 本地事务消息表,即将业务的执行 和 将消息放入消息表中的操作,放在同一个事务中提交

  • 这样,本地事务执行成功的话,消息肯定也插入成功,然后再调用其他服务,如果其他服务调用成功,就修改这条本地消息的状态为成功,或者直接删除。
  • 有一个后台线程定时轮询消息表,发现没有处理的消息,会一直调用相应的服务,一般会设置重试次数,超过重试次数就特殊记录,等待人工处理。
image-20240614195843877

RocketMQ的事务消息方案中,如果消息队列挂掉,数据库事务就无法执行,整个应用也就挂掉了。QMQ的事务消息方案,即使消息队列挂了,也不会影响数据库事务的执行,因此QMQ实现的方案更适应于大多数业务。