分布式事务

本文最后更新于:2024年3月18日 凌晨

分布式事务

  • 分布式事务是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
  • 例如在大型电商系统中,下单接口通常会扣减库存,减去优惠,生成订单 id,而订单服务与库存,优惠,订单 id 都是不同的服务,下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败,本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

柔性事务

  • 不同于 ACID 的刚性事务,在分布式场景下基于 BASE 理论,就出现了柔性事务的概念,要想通过柔性事务来达到最终的一致性,就需要依赖于一些特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样,但是都不满足的话,是不可能做柔性事务的。

分布式事务使用场景

转账

  • 转账是最经典的分布式事务场景,假设用户 A 使用银行 app 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后增加用户 B 账户中的余额,此时就会出现 2 种异常情况:1. 用户 A 的账户扣款成功,用户 B 账户余额增加失败 2. 用户 A 账户扣款失败,用户 B 账户余额增加成功,对于银行系统来说,以上 2 种情况都是不允许发生,此时就需要分布式事务来保证转账操作的成功。

下单扣库存

  • 在电商系统中,下单是用户最常见操作,在下单接口中必定会涉及生成订单 id,扣减库存等操作,对于微服务架构系统,订单 id 与库存服务一般都是独立的服务,此时就需要分布式事务来保证整个下单接口的成功。

同步超时

  • 继续以电商系统为例,在微服务体系架构下,我们的支付与订单都是作为单独的系统存在,订单的支付状态依赖支付系统的通知,假设一个场景:我们的支付系统收到来自第三方支付的通知,告知某个订单支付成功,接收通知接口需要同步调用订单服务变更订单状态接口,更新订单状态为成功,流程图如下,从图中可以看出有两次调用,第三方支付调用支付服务,以及支付服务调用订单服务,这两步调用都可能出现调用超时的情况,此处如果没有分布式事务的保证,就会出现用户订单实际支付情况与最终用户看到的订单支付情况不一致的情况。

分布式事务的解决方案

两阶段提交/XA

  • 两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
  • 目前支付宝使用两阶段提交思想实现了分布式事务服务(Distributed Transaction Service, DTS),它是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性,具体可参考支付宝官方文档:https://tech.antfin.com/docs/2/46887
img img
  • 流程
    • 第一阶段(prepare):协调者询问参与者事务是否执行成功,参与者发回事务执行结果,询问可以看成一种投票,需要参与者都同意才能执行。
    • 第二阶段(commit/rollback):如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务,否则,协调者发送通知让参与者回滚事务,需要注意的是,在准备阶段,参与者执行了事务,但是还未提交,只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
  • 存在的问题
    • 同步阻塞:所有事务参与者在等待其它参与者响应的时候都处于同步阻塞等待状态,无法进行其它操作。
    • 单点问题:协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响,特别是在提交阶段发生故障,所有参与者会一直同步阻塞等待,无法完成其它操作。
    • 数据不一致:在提交阶段,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
    • 太过保守:任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

TCC

  • 关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出,TCC 事务机制相比于上面介绍的 XA,解决了其几个缺点:
    1. 解决了协调者单点,由主业务方发起并完成这个业务活动,业务活动管理器也变成多点,引入集群。
    2. 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
    3. 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。
  • TCC(Try Confirm Cancel)
    • Try 阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性),在 Try 阶段,是对业务系统进行检查及资源预览,比如订单和存储操作,需要检查库存剩余数量是否够用,并进行预留,预留操作的话就是新建一个可用库存数量字段,Try 阶段操作是对这个可用库存数量进行操作。
    • Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性,要求具备幂等设计,Confirm 失败后需要进行重试。
    • Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
  • 特性
    • 基于 TCC 实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为 Try,Confirm,Cancel 三个接口,所以代码实现复杂度相对较高。
    • TCC设计之初认为Confirm 与Cance 一定会成功。
    • Confirm 与Cance 尽可能不要产生服务通信,只做最简单的事情。
    • Confirm 与Cance 如果失败,由事务中间件进行"重试”补偿。
    • 极小概率情况下,C/C彻底失败,则需要定时任务检测或人工介入。

本地消息表

  • 本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
img
  • 流程
    1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
    2. 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
    3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
  • 本地消息表实现的条件
    • 消费者与生成者的接口都要支持幂等。
    • 生产者需要额外的创建消息表。
    • 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作。
  • 容错机制
    • 步骤 1 失败时,事务直接回滚。
    • 步骤 2,3 写 mq 与消费 mq 失败会进行重试。
    • 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作。
  • 此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行,消息日志可以存储到本地文本,数据库或消息队列,再通过业务规则自动或人工发起重试,人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

可靠消息最终一致性

  • 目前市面上支持该方案的 MQ 只有阿里的 RocketMQ,该方案应用场景也比较多,比如用户注册成功后发送邮件,电商系统给用户发送优惠券等需要保证最终一致性的场景。
img
  • 大致流程如下:
    1. A 系统先向 mq 发送一条 prepare 消息,如果 prepare 消息发送失败,则直接取消操作。
    2. 如果消息发送成功,则执行本地事务。
    3. 如果本地事务执行成功,则向 mq 发送一条 confirm 消息,如果发送失败,则发送回滚消息。
    4. B 系统定期消费 mq 中的 confirm 消息,执行本地事务,并发送 ack 消息,如果 B 系统中的本地事务失败,会一直不断重试,如果是业务失败,会向 A 系统发起回滚请求。
    5. mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况,如果该 prepare 消息本地事务处理成功,则重新发送 confirm 消息,否则直接回滚该消息。
  • 该方案与本地消息最大的不同是去掉了本地消息表,其次本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。

尽最大努力通知

  • 最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。
  • 这个方案的大致意思就是:
    1. 系统 A 本地事务执行完之后,发送个消息到 MQ
    2. 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口。
    3. 要是系统 B 执行成功就 OK 了,要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。