分布式事务 指的是当事务的操作分布在不同的节点上时, 需要保证事务的ACID特性。 其中最重要的一点是要保证在各个节点上的事务要么同时成功,要么同时失败。

分布式事务的解决方案有多种,下面对几种常见的进行一一说明。

2PC

2PC, Two-phase Commit 。两阶段提交,通过引入协调者(Coordinator)来协调所有参与者的行为,并最终决定这些参与者是否要真正执行事务。

两个阶段分别如下:

  • 第一阶段:准备阶段。由协调者询问各个参与节点事务是否执行成功,参与节点返回事务执行结果。参与节点收到通知之后,会进行准备操作,例如执行insert(此时并未commit),准备完成之后会告诉协调节点自己已经准备完成。
  • 第二阶段:提交阶段。协调节点在收到所有参与节点都执行成功之后,就会通知所有节点进行提交操作。若任一节点执行失败,协调节点则会通知所有参与节点回滚数据。

主要缺点如下:

  • 阻塞。 需要等待所有参与者确认OK之后才能commit, 其处理的时间取决于处理事务最慢的那个参与节点。所以不适合并发高的场景。
  • 单点故障。 特别是对协调节点的依赖很大,若协调节点发生故障,则整个事务都将不可用
  • 数据不一致。比如在第二阶段,某些参与节点在接收到commit之后发生故障,将会导致这些参与节点的数据与其他节点数据不一致

TCC

其核心思想是对于每个操作,都需要有一个与其对应的补偿(或者叫撤销)操作

一般分为三个阶段:

  • try: 尝试去执行,完成所有业务的一致性检查,预留必须的业务资源

  • confirm: 该阶段对业务进行确认提交,不做任何检查,因为 try 阶段已经检查过了,默认 Confirm 阶段是不会出错的

  • cancel: 若业务执行失败,则进入该阶段,它会释放 try 阶段占用的所有业务资源,并回滚 Confirm 阶段执行的所有操作

TCC有一个大的前提,就是这三个动作必须都是幂等的,对业务有一定的要求。拿资金转账来说,try就是冻结金额;confirm就是完成扣减;cancel就是解冻,只要对应的订单号是一样的,多次执行也不会有任何问题。

由于TCC中事务的发起方是直接在各个业务节点上,所以不需要协调者, 这样也就不会存在2PC中所说的资源阻塞的问题,因为在confirm阶段每个参与节点都已经提交事务了,不需要等待其他节点,所以其并发能力较好。

其最大的缺点是对代码的侵入性太强,相当于业务层去保证事务,需要业务自己去将confirm和cancel抽象出来,对于某些场景会造成大量的编码,甚至难以执行。

本地消息表

通过mq来实现,当发起事务的系统发送mq之后,需要消费的服务接受到该mq之后,就会执行自己的相应处理逻辑,从而达到最终一致性。

本地消息表

业务系统写数据库和发送mq不是一个事务过程, 所以可能会存在写数据库成功,但发送mq失败, 此时就需要引入一个本地消息表,在写数据的同时写入一条消息到本地消息表中,一般状态分为“进行中”、“完成”等状态,由于写业务表和写本地消息表都是操作同一个DB,所以是一个本地事务,能够保证同时完成或同时失败。

引入本地消息表还有两个注意点:

  1. 需要有一个定时任务,去定期扫描消息表中不是完成状态的消息,进行重新发送
  2. 由于mq存在重复发送的问题,需要其他的业务系统做好幂等处理

缺点:

  1. 本地消息表与业务系统耦合比较严重, 对代码的抽象不好处理
  2. 由于每条消息都要写本地消息表,对数据库的压力比较大

支持事务的mq

其实是对本地消息表的一个封装,如前文所说,普通的mq消息无法和单机数据库一样,具备提交、回滚的能力,所以支持事务的mq,比如rocketmq相当于将本地消息表在mq内部进行了实现。

其他方面的实现和本地消息表类似。

参考资料:

  1. https://www.cnblogs.com/lfs2640666960/p/12300585.html
  2. https://juejin.cn/post/7012425995634343966#heading-19