# 分布式事务解决方案

在这里插入图片描述

# 分布式事务

在分布式系统中,由于服务和数据库被拆分到不同的节点上,传统的本地事务(ACID)无法直接适用。为了保证数据的一致性,业界演进出了多种分布式事务解决方案。

这些方案通常可以分为两大阵营:追求强一致性的刚性事务,和追求最终一致性的柔性事务(基于 BASE 理论)

# 强一致性方案(刚性事务)

这类方案要求所有节点在事务提交时,数据必须完全一致,适用于对数据准确性要求极高的场景(如金融转账)

# 2PC(Two-Phase Commit,二阶段提交)

将事务的提交过程分为两个阶段:准备阶段提交阶段,由一个“协调者”来统一指挥多个“参与者”

P1 - 准备阶段 (Prepare): 协调者向所有参与者发送准备请求,参与者执行本地事务但不提交,并锁定资源,然后向协调者回复 Yes 或 No

P2 - 提交阶段 (Commit): 如果所有参与者都回复 Yes,协调者下发 Commit 指令;只要有一个回复 No 或超时,协调者下发 Rollback 指令

缺点:

  1. 同步阻塞:参与者在等待协调者指令时,会一直锁定资源,性能极差
  2. 单点故障: 如果协调者在第二阶段挂了,参与者会陷入无休止的等待
  3. 数据不一致: 如果在 Commit 阶段发生网络分区,部分参与者收到了指令而部分没收到,会导致数据不一致

# 3PC(Three-Phase Commit,三阶段提交)

为了解决 2PC 的同步阻塞单点故障问题,3PC 在 2PC 的基础上引入了超时机制,并将原本的准备阶段一拆为二,演进为三个阶段

P1 - 询问阶段 (CanCommit): 协调者向参与者发送询问请求。参与者只做基本的自身状态和资源检查,口头回复 Yes 或 No,此时不锁定资源

P2 - 预提交阶段 (PreCommit): 如果 P1 全部回复 Yes,协调者下发预提交请求。参与者此时执行本地事务并锁定资源,但不提交,随后向协调者回复 Ack

P3 - 提交阶段 (DoCommit): 如果 P2 全部回复 Ack,协调者下发正式 Commit 指令,参与者正式提交事务并释放资源

改善点: 在 P1(CanCommit)阶段只是口头询问而不锁资源。在 P3 阶段,如果参与者长时间收不到协调者的 Commit 指令,参与者超时后会自动提交事务,避免了 2PC 中无限期等待的单点故障

数据不一致问题依然存在: 如果在 P3 阶段发生网络分区,协调者实际想下发 Rollback(回滚),但部分参与者由于网络断开没收到,超时后它们自动提交了,而收到指令的执行了回滚,导致数据严重不一致。因此工业界极少使用

# 最终一致性方案(柔性事务)

这类方案核心思想是:允许中间状态的不一致,只要保证最终数据一致即可。性能通常远高于 2PC

# TCC

TCC 是一种业务层面的二阶段提交,需要侵入业务代码,手动实现三个接口。TCC这种方案应该是在企业中应用比较广泛的一种方案。TCC是Try、Confirm、Cancel三个词语的缩写

TCC主要分为3个操作

Try:一阶段,负责业务资源检查和预留

Confirm:二阶段提交操作,所有的Try都成功了,则执行Confirm操作。Confirm真正执行业务,使用Try预留的资源

Cancel:二阶段回滚操作,只要有一个Try失败了,则走到Cancel操作。Cancel释放Try预留的资源

在这里插入图片描述

优点:并发程度高,在业务层面锁定资源

缺点:业务侵入性强,一个业务需要提供Try/Confirm/Cancel三个方法

# SAGA

Saga 是一种长事务解决方案。它将一个分布式事务拆分为若干个本地局部事务 T1,T2,,TnT_1, T_2, \dots, T_n。每个局部事务都有一个对应的补偿事务 C1,C2,,CnC_1, C_2, \dots, C_n

正常流程: 顺次执行 T1T2TnT_1 \rightarrow T_2 \rightarrow \dots \rightarrow T_n

异常流程: 如果在执行到 TiT_i 时失败了,Saga 恢复系统会有两种策略

  1. 正向恢复(Forward Recovery): 重试失败的 TiT_i,适用于必须要成功的场景
  2. 反向恢复(Backward Recovery): 顺次执行补偿事务,即 CiC2C1C_i \rightarrow \dots \rightarrow C_2 \rightarrow C_1,冲正前面已经提交的事务

在这里插入图片描述

SAGA特点为

  1. 并发度高,不需要长期锁定资源
  2. 开发量大,需要定义正向操作和补偿操作
  3. 不能保证隔离型

# 本地消息表

这是大型互联网公司非常经典的模式,利用了数据库本地事务和消息队列

  1. A 系统在执行本地业务时,顺便在同一个数据库的“本地消息表”里插入一条消息,利用本地事务保证业务和消息同时成功。
  2. A 系统有一个后台任务,轮询本地消息表,把消息发到 MQ。
  3. B 系统消费 MQ 消息,执行自己的业务。执行成功后,通知 A 系统把消息状态改为“已完成”。
  4. 如果 B 失败或超时,A 的后台任务会不断重试发送消息,直到 B 成功(B 需做幂等处理)

通过这种方案就能达到事务的最终一致性,这种不断重试的思路,也体现了我们后面要提到的最大努力通知

本地消息表特点为:

  1. 需要创建额外的消息表,不断对消息表轮询

# RocketMQ事务消息

在本地消息表方案中,生产者需要额外创建本地消息表,还要对本地消息进行轮询。RocketMQ在4.3之后的版本正式支持事务消息,该事务消息的本质是把本地消息表放在RocketMQ上,解决生产端消息发送和本地事务执行的原子性问题

在这里插入图片描述 RocketMQ实现分布式事务的流程如下

  1. producer向mq server发送一个半消息
  2. mq server将消息持久化成功后,向发送方确认消息已经发送成功,此时消息并不会被consumer消费
  3. producer开始执行本地事务逻辑
  4. producer根据本地事务执行结果向mq server发送二次确认,mq收到commit状态,将消息标记为可投递,consumer会消费该消息。mq收到rollback则删除半消息,consumer将不会消费该消息,如果收到unknow状态,mq会对消息发起回查
  5. 在断网或者应用重启等特殊情况下,步骤4提交的2次确认有可能没有到达mq server,经过固定时间后mq会对该消息发起回查
  6. producer收到回查后,需要检查本地事务的执行状态
  7. producer根据本地事务的最终状态,再次提交二次确认,mq仍按照步骤4对半消息进行操作

看到这,可能有人会问了,我们先执行本地事务,执行成功后再发送消息,这样不也可以保证生产端消息发送和本地事务执行的原子性?

其实这样做还是有可能会造成数据不一致的问题。假如本地事务执行成功,发送消息,由于网络延迟,消息发送成功,但是回复超时了,抛出异常,本地事务回滚。但是消息其实投递成功并被消费了,此时就会造成数据不一致的情况

那消息投递到mq server,consumer消费失败怎么办?

如果是消费超时,重试即可。如果是由于代码等原因真的消费失败了,此时就得人工介入,重新手动发送消息,达到最终一致性。

# 最大努力通知

做过微信充值或者支付宝充值的小伙伴对这个方案应该比较熟悉,因为最大努力通知这种方案在充值系统中经常被使用

充值系统通过不断的重试将充值结果推送给账户系统。因此账户系统接收充值结果的系统要保持幂等。另外充值充值系统还要提供回查接口,让账户系统主动校验充值的状态

在这里插入图片描述

# Seata AT模式

它在底层通过自动生成逆向 SQL(回滚日志)的方式,实现了类似 2PC 的效果,但对业务完全无侵入

一阶段: Seata 的代理数据源会拦截你的 SQL,在业务数据修改前保存“前镜像”到undo_log表,业务修改后保存“后镜像”到undo_log表,然后提交本地事务

二阶段

  1. 如果全局事务成功,异步快速删除 undo_log
  2. 如果全局事务失败,读取对应的 undo_log,进行脏写校验(对比当前数据库真实数据与“后镜像”,如果不一致说明数据被别人动过,需要人工介入),如果校验通过,利用 undo_log 的前镜像数据,自动反向生成 SQL 回滚数据

Seata AT模式特点:

  1. 对代码无侵入,开发速度较快
  2. 需要用全局锁来保证隔离性,并发程度较低

# 总结

方案 一致性级别 性能 开发成本 适用场景
2PC / 3PC 强一致性 极低 低 (框架层) 跨库的微小事务,对并发要求不高的传统行业(如 ERP、传统 OA)
TCC 最终一致性 极高 核心金融、转账、账务系统等对资金安全要求极高的核心业务
Saga 最终一致性 业务流程长、参与者多、或者需要调用第三方服务的场景
本地消息表 / RocketMQ事务消息 最终一致性 跨服务异步解耦、对实时性要求不高的外围业务(如发券、积分、通知)
Seata AT 最终一致性 中高 极低 并发量不高、追求快速开发、不想改动业务代码的微服务系统(企业中后台)