分布式事务

柔性事务

  • 在 CAP 理论和 BASE 理论的基础上,提出了柔性事务的概念。柔性事务追求的是最终一致性。
  • 柔性事务就是 BASE 理论 + 业务实践。柔性事务追求的目标是:我们根据自身业务特性,通过适当的方式来保证系统数据的最终一致性。
    • TCC
    • Saga
    • MQ事务
    • 本地消息表

刚性事务

  • 刚性事务追求的就是强一致性。
    • 2PC
    • 3PC

2PC

  • 2PC(Two-Phase Commit)的含义:
    • 2 -> 指代事务提交的 2 个阶段
    • P -> Prepare(准备阶段)
    • C -> Commit(提交阶段)
  • 2PC 将事务的提交过程分为 2 个阶段:准备阶段 和 提交阶段
    • 准备阶段的核心是“询问”事务参与者执行本地数据库事务操作是否成功:
      • 事务协调者/管理者向所有参与者发送消息询问:“你是否可以执行事务操作呢?”,并等待其答复。
      • 事务参与者接收到消息之后,开始执行本地数据库事务预操作如写 redo log/undo log 日志。但此时并不会提交事务!
      • 事务参与者如果执行本地数据库事务操作成功,那就回复:“就绪”,否则就回复:“未就绪”。
    • 提交阶段的核心是“询问”事务参与者提交事务是否成功:
      • 当所有事务参与者都是“就绪”状态时:
        • 事务参与者接收到 commit 消息后执行提交本地数据库事务操作,执行完成之后释放整个事务期间所占用的资源。
        • 事务协调者/管理者向所有参与者发送消息:“你们可以提交事务啦!”(commit 消息)。
        • 事务参与者回复:“事务已经提交”(ack 消息)。
        • 事务协调者/管理者收到所有事务参与者的 ack 消息后,整个分布式事务过程正式结束。
      • 当任一事务参与者是“未就绪”状态时:
        • 事务协调者/管理者向所有参与者发送消息:“你们可以执行回滚操作了!”(rollback 消息)。
        • 事务参与者接收到 rollback 消息后执行本地数据库事务回滚,执行完成之后释放整个事务期间所占用的资源。
        • 事务参与者回复:“事务已经回滚”(ack 消息)。
        • 事务协调者/管理者收到所有事务参与者的 ack 消息后,取消事务。
    • 总结
      • 准备阶段的主要目的是测试事务参与者能否执行本地数据库事务操作(!!! 注意:这一步并不会提交事务)。
      • 提交阶段中,事务协调者/管理者会根据准备阶段中事务参与者的消息来决定是执行事务提交还是回滚操作。
      • 提交阶段之后一定会结束当前的分布式事务。
    • 2PC 的优点
      • 实现起来非常简单,各大主流数据库如 MySQL、Oracle 都有自己实现。
      • 针对的是数据强一致性。不过,仍然可能存在数据不一致的情况。
    • 2PC 存在的问题
      • 同步阻塞:事务参与者会在正式提交事务之前一直占用相关的资源。比如用户小明转账给小红,其他事务要操作用户小明或小红时,就会阻塞。
      • 数据不一致:由于网络问题或事务协调者/管理者宕机都有可能造成数据不一致的情况。比如在提交阶段,部分网络问题导致部分参与者收不到 commit/rollback 消息时,就会导致数据不一致。
      • 单点问题:事务协调者/管理者在其中也是一个很重要的角色,如果事务协调者/管理者在准备阶段完成之后挂掉,事务参与者就会一直卡在提交阶段。

3PC

  • 3PC 是在人们在 2PC 的基础上做了一些优化得到的。3PC 把 2PC 中的准备阶段(Prepare)进一步细化,分为 2 个阶段:
    • 询问阶段(CanCommit):这一步不会执行事务操作,只会询问事务参与者能否执行本地数据库事务操作。
    • 准备阶段(PreCommit):当所有事务参与者都返回“可执行”后,事务参与者才会执行本地数据库事务预操作如写 redo log/undo log 日志。
    • 此外,3PC 还引入了超时机制来避免事务参与者一直阻塞占用资源。

TCC(补偿事务)

  • TCC 是 Try、Confirm、Cancel 三个词的缩写,它分为三个阶段:
    • Try(尝试)阶段:尝试执行。完成业务检查,并预留好必需的业务资源。
    • Confirm(确认)阶段:确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel。
    • Cancel(取消)阶段:取消执行,释放 Try 阶段预留的业务资源。
  • 一般情况下,当我们使用 TCC 模式时,需要自己实现 Try、Confirm、Cancel 这三个方法,来达到最终一致性。
  • TCC 模式不需要依赖于底层数据资源的事务支持,但是需要我们手动实现更多的代码,属于侵入业务代码的一种分布式解决方案。

MQ事务

  • 事务允许事件流应用将消费、处理、生产消息的整个过程定义为一个原子操作。
  • MQ 发送方(比如物流服务)在消息队列上开启一个事务,然后发送一个“半消息”给 MQ Server/Broker。事务提交之前,半消息对于 MQ 订阅方/消费者(比如第三方通知服务)不可见。
  • “半消息”发送成功后,MQ 发送方就开始执行本地事务。
  • MQ 发送方的本地事务执行成功后,“半消息”变成正常消息,可以正常被消费。MQ 发送方的本地事务执行失败时,会直接回滚。

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

  • RocketMQ 中的 Broker 会定期去 MQ 发送方上反查这个事务的本地事务的执行情况,并根据反查结果决定提交或者回滚这个事务。
  • 事务反查机制的实现依赖于我们业务代码实现的对应接口,比如要查看创建物流信息的本地事务是否执行成功时,直接在数据库中查询对应的物流信息是否存在即可。

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

  • 消息消费失败时,RocketMQ 会自动进行消费重试。如果超过最大重试次数该消息还是没有正确消费,RocketMQ 会将其放到死信队列。

本地消息表 QMQ

  • 维护一个本地消息表用来存放消息发送的状态,保存消息发送情况到本地消息表的操作和业务操作要在一个事务里提交。这样的话,业务执行成功代表消息表也写入成功。
  • 我们再单独起一个线程定时轮询消息表,把没处理的消息发送到消息中间件。
  • 消息发送成功后,更新消息状态为成功或者直接删除消息。
  • QMQ 的事务消息方案中,即使消息队列挂了也不会影响数据库事务的执行。
  • 因此,QMQ 实现的方案能更加适应于大多数业务。不过,这种方法同样适用于其他消息队列,只能说 QMQ 封装得更好,开箱即用罢了。

Saga

  • Saga 属于长事务解决方案,其核心思想是将长事务拆分为多个本地短事务(本地短事务序列)。
    • 长事务 —> T1, T2 ~ Tn 个本地短事务。
    • 每个短事务都有一个补偿动作 —> C1, C2 ~ Cn。
  • 如果 T1, T2 ~ Tn 这些短事务都能顺利完成的话,整个事务也就顺利结束,否则,将采取恢复模式。

反向恢复

  • 简介:如果 Ti 短事务提交失败,则补偿所有已完成的事务(一直执行 Ci 对 Ti 进行补偿)。
  • 执行顺序:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。

正向恢复

  • 简介:如果 Ti 短事务提交失败,则一直对 Ti 进行重试,直至成功为止。

  • 执行顺序:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。

  • 和 TCC 类似,Saga 正向操作与补偿操作都需要业务开发者自己实现,因此也属于侵入业务代码的一种分布式解决方案。和 TCC 很大的一个不同是 Saga 没有“Try”动作,它的本地事务 Ti 直接被提交。因此,性能非常高!

  • 理论上来说,补偿操作一定能够执行成功。不过,当网络出现问题或者服务器宕机时,补偿操作也会执行失败。这种情况下,往往需要我们进行人工干预。此外,为了提高容错性(如 Saga 系统本身也可能会崩溃),保证所有的短事务都得以提交或补偿,我们还需要将这些操作通过日志记录下来(Saga log,类似于数据库的日志机制)。这样,Saga 系统恢复后,我们就知道短事务执行到哪里了或者补偿操作执行到哪里了。

  • 另外,因为 Saga 没有进行“Try”动作预留资源,所以不能保证隔离性。这也是 Saga 比较大的一个缺点。