分布式事务

单数据源事务/多数据源事务

在分布式场景下,一个系统由多个子系统组成,这些子系统通过互相调用来组合出更复杂的业务。在微服务系统架构中,每个子系统被称作一个微服务,每个微服务都维护自己的数据库,以保持独立性。

例如,一个电商系统可能由购物微服务、库存微服务、订单微服务等组成。购物微服务通过调用库存微服务和订单微服务来整合出购物业务。用户请求购物微服务完成下单时,购物微服务一方面调用库存微服务扣减相应商品的库存数量,另一方面调用订单微服务插入订单记录。

常见分布式事务解决方案

分布式事务模型

描述分布式事务,常常会使用以下几个名词:

  1. 事务参与者:例如每个数据库就是一个事务参与者
  2. 事务协调者:访问多个数据源的服务程序,例如 shopping-service 就是事务协调者
  3. 资源管理器(Resource Manager, RM):通常与事务参与者同义
  4. 事务管理器(Transaction Manager, TM):通常与事务协调者同义

在分布式事务模型中,一个 TM 管理多个 RM,即一个服务程序访问多个数据源;TM 是一个全局事务管理器,协调多方本地事务的进度,使其共同提交或回滚,最终达成一种全局的 ACID 特性。

二阶段提交

2PC 是一种实现分布式事务的简单模型,这两个阶段是:

  1. 准备阶段:事务协调者向各个事务参与者发起询问请求,参与者回复 yes(表示已准备好,允许提交全局事务)或 no(表示无法拿到全局事务所需的本地资源)或超时。
  2. 提交阶段:如果各个参与者回复的都是 yes,则协调者向所有参与者发起事务提交操作;如果任何一个参与者回复 no 或者超时,则协调者向所有参与者发起事务回滚操作。

所有的参与者都需要实现三个接口:

  • Prepare(): TM 调用该接口询问各个本地事务是否就绪
  • Commit(): TM 调用该接口要求各个本地事务提交
  • Rollback(): TM 调用该接口要求各个本地事务回滚

存在的问题:

  1. 性能差
  2. 准备阶段完成后,如果协调者宕机,所有的参与者都收不到提交或者回滚指令
  3. 脑裂,无法决定下一步是否进行全体回滚

2PC 之后又出现了 3PC,把两阶段过程变成了三阶段过程:询问阶段、准备阶段、提交或回滚阶段。3PC 利用超时机制解决了 2PC 的同步阻塞问题,避免资源被永久锁定,进一步加强了整个事务过程的可靠性。但 3PC 同样无法应对宕机问题,只不过出现多数据源中数据不一致问题的概率更小。

TCC方案

TCC 是 Try、Confirm、Cancel 三个词的缩写,其本质是一个应用层面上的 2PC,同样分为两个阶段:

  1. 阶段一:准备阶段。协调者调用所有的每个微服务提供的 try 接口,将整个全局事务涉及到的资源锁定住,若锁定成功 try 接口向协调者返回 yes。
  2. 阶段二:提交阶段。若所有的服务的 try 接口在阶段一都返回 yes,则进入提交阶段,协调者调用所有服务的 confirm 接口,各个服务进行事务提交。如果有任何一个服务的 try 接口在阶段一返回 no 或者超时,则协调者调用所有服务的 cancel 接口。

TCC 通过不断重试解决 2PC 无法应对宕机问题的缺陷。由于 try 操作锁住了全局事务涉及的所有资源,保证了业务操作的所有前置条件得到满足,因此无论是 confirm 阶段失败还是 cancel 阶段失败都能通过不断重试直至 confirm 或 cancel 成功。

事务状态表

另外有一种类似 TCC 的事务解决方案,借助事务状态表来实现。假设要在一个分布式事务中实现调用 repo-service 扣减库存、调用 order-service 生成订单两个过程。在这种方案中,协调者 shopping-service 维护一张事务状态表:

初始状态为 1,每成功调用一个服务则更新一次状态,最后所有的服务调用成功,状态更新到 3。

有了这张表,就可以启动一个后台任务,扫描这张表中事务的状态,如果一个分布式事务一直未到状态 3,说明这条事务没有成功执行,于是可以重新调用 repo-service 扣减库存、调用 order-service 生成订单,直至所有的调用成功,事务状态到 3。

如果多次重试仍未使得状态到 3,可以将事务状态置为 error,通过人工介入进行干预。

由于存在服务的调用重试,因此每个服务的接口要根据全局的分布式事务 ID 做幂等。

基于消息中间件的最终一致性

无论是 2PC & 3PC 还是 TCC、事务状态表,基本都遵守 XA 协议的思想,即这些方案本质上都是事务协调者协调各个事务参与者的本地事务的进度,使所有本地事务共同提交或回滚,最终达成一种全局的 ACID 特性。

但是这些全局事务方案由于操作繁琐、时间跨度大,或者在全局事务期间会排他地锁住相关资源,使得整个分布式系统的全局事务的并发度不会太高。这很难满足电商等高并发场景对事务吞吐量的要求,因此互联网服务提供商探索出了很多与 XA 协议背道而驰的分布式事务解决方案。其中利用消息中间件实现的最终一致性全局事务就是一个经典方案。

在这个模型中,用户不再是请求整合后的 shopping-service 进行下单,而是直接请求 order-service 下单,order-service 一方面添加订单记录,另一方面会调用 repo-service 扣减库存。

MQtransaction

这种实现方式的流程是:

  1. order-service 负责向 MQ server 发送扣减库存消息(repo_deduction_msg);repo-service 订阅 MQ server 中的扣减库存消息,负责消费消息。
  2. 用户下单后,order-service 先执行插入订单记录的查询语句,后将 repo_deduction_msg 发到消息中间件中,这两个过程放在一个本地事务中进行,一旦“执行插入订单记录的查询语句”失败,导致事务回滚,“将 repo_deduction_msg 发到消息中间件中”就不会发生;同样,一旦“将 repo_deduction_msg 发到消息中间件中”失败,抛出异常,也会导致“执行插入订单记录的查询语句”操作回滚,最终什么也没有发生。
  3. repo-service 接收到 repo_deduction_msg 之后,先执行库存扣减查询语句,后向 MQ sever 反馈消息消费完成 ACK,这两个过程放在一个本地事务中进行,一旦“执行库存扣减查询语句”失败,导致事务回滚,“向 MQ sever 反馈消息消费完成 ACK”就不会发生,MQ server 在 Confirm 机制的驱动下会继续向 repo-service 推送该消息,直到整个事务成功提交;同样,一旦“向 MQ sever 反馈消息消费完成 ACK”失败,抛出异常,也会导致“执行库存扣减查询语句”操作回滚,MQ server 在 Confirm 机制的驱动下会继续向 repo-service 推送该消息,直到整个事务成功提交。

存在问题,有可能中间网络问题导致不一致问题,比如 order 发送出了,并且 repo 进行 deduct 操作但是 ack 失败。

trueImple

通过这种设计,实现了消息在发送方不丢失,消息在接收方不被重复消费,联合起来就是消息不漏不重,严格实现了 order-service 和 repo-service 的两个数据库中数据的最终一致性。

基于消息中间件的最终一致性全局事务方案是互联网公司在高并发场景中探索出的一种创新型应用模式,利用 MQ 实现微服务之间的异步调用、解耦合和流量削峰,支持全局事务的高并发,并保证分布式数据记录的最终一致性。