分布式事务几种方案

在了解方案前先了解一下几个理论

理论

CAP

C consistency 一致性: 所有节点在同一时间访问时, 拿到的均为最新数据

A availability 可用性: 向非故障节点发起请求后, 每一次请求都能在有限时间内获得合理的响应(而非错误或超时)

P partition tolerate 分区容错性: 部分节点挂了, 依旧能正常对外提供服务

在分布式系统下,P是前提,仅能实现CP/AP

CP 一致性 + 分区容错: 当网络分区发生时,为了保证数据一致,系统会拒绝服务或报错,牺牲可用性(A)。适用于银行转账、交易系统

AP 可用性 + 分区容错: 当网络分区发生时,系统保证可用,但允许读取旧数据,牺牲强一致性(C),满足最终一致性。适用于大多数Web应用、社交媒体

CA 一致性 + 可用性: 仅可在单机环境下成立,常用数据库如mysql oracle等

BASE

BASE是下面几个单词的缩写, 此理论是互联网实践而来,是对CAP理论的演变和权衡结果.

Basically Available 基本可用: 在系统发生故障或高负载时, 必须保证系统核心功能可用, 非核心功能使用降级熔断等策略保证--基本可用

Soft-state 软状态: 允许系统存在短暂、无锁、可过渡的中间状态,节点间数据暂时不一致, 但不影响整体可用性.

​ 与硬状态(ACID事务, 实时锁定)相对

Eventually Consistent 最终一致性: BASE理论强调最终一致性, 即在有限时间有穷步后, 结果最终一致.

一致性的三个状态

一致性强调结果, 即数据最后能否一致

强一致性: 系统写了什么, 读出来就是什么

弱一致性: 不保证多久后读取数据是最新的, 甚至永远可能读不到最新值. 都不保证

最终一致性: 现在不一定一致, 但在有限时间后一定一致

幂等性

同一个操作执行多次, 结果相同

常见方案

XA 标准化分布式事务

这是对2PC 的标准化实现, 是X/Open 组织制定的、跨数据库/消息队列的分布式事务工业级标准协议. 流程参见2PC

支持XA 的有MySQL、Oracle、PostgreSQL、SQL Server、RocketMQ、Kafka 等几乎所有主流中间件

2PC 两阶段提交

借用XA 的角色定义: AP 业务应用(Application Program), TM 事务管理器(Transaction Manager), RM 参与者(Resource Manager)

  1. AP 向TM 发起全局事务
  2. TM向RM A, B发送要执行的SQl
  3. RM A, B开启事务, 执行业务SQL, 两个执行成功/失败均向TM 返回
  4. TM 根据返回决定向RM 发送commit或rollback

优点: 强一致, 开发简单. 适用于金融等系统

缺点: 性能差, 协调者节点挂了会导致锁无法释放(可用心跳包等方法解决), 极端情况下会导致一部分commit, 一部分rollback

3PC 三阶段提交

  1. CanCommit: 仅检查库存, 锁.
  2. PreCommit: 所有节点的Can 均返回成功时, 向所有节点发送PreCommit, 此步骤开启事务但不提交.
  3. DoCommit: 所有节点的PreCommit 均返回成功时,向所有节点发送DoCommit.

3PC 为所有节点增加了超时决策逻辑, 在PreCommit 后超时可选自动Abort/DoCommit, 在CanCommit 超时自动Abort.

极端情况下仍有可能导致协调者只给部分节点发送了DoCommit, 剩下节点可能Abort/DoCommit.

生产几乎不用

AT 自动事务模式 (Seata)

官方链接: https://seata.apache.org/zh-cn/docs/overview/what-is-seata 你可以阅读官方文档获取更详细的说明

三个角色

TM(Transaction Manager): 业务应用里的事务发起方

TC(Transaction Coordinator): Seata Server, 管理全局事务状态、协调各分支提交/回滚、维护全局锁

RM(Resource Manager): 由 Seata 代理数据源实现, 自动拦截 SQL、生成日志、执行回滚

一阶段执行流程

  1. 开启本地事务, 解析SQL 的操作(update等)、表(product),条件(where name = 'TXC')等相关的信息
  2. 查询前镜像:根据SQL定位到记录, 将记录保留一份
  3. 执行业务 SQL, 将执行后的结果当做后镜像: 根据前镜像的结果,通过 主键 定位数据
  4. 将前后镜像和业务SQL 组成一条回滚日志记录, 插入到UNDO_LOG 表中
  5. 向TC 申请指定表指定主键的记录的全局锁
  6. 如果申请到全局锁, 本地事务提交(携带undo log一块).
    如果拿锁失败则继续尝试, 超出尝试范围将放弃. 回滚本地事务, 释放本地锁
  7. 将本地事务结果上报给TC

二阶段执行流程

回滚

  1. 收到 TC 的分支回滚请求, 开启本地事务
  2. 根据 XID(全局事务锁ID) 和 Branch ID(分支事务ID) 查找到 UNDO_LOG 表中对应记录
  3. 那记录中的后镜像与当前数据比较, 如果不同, 则说明数据被当前全局事务之外的动作修改. 此时根据配置策略处理, 详见文档
  4. 根据 UNDO_LOG 的前镜像和 SQL 语句生成对应回滚语句
  5. 提交本地事务. 上报本地事务执行结果给 TC

提交

  1. 收到 TC 的分支提交请求, 将请求放入异步队列, 立刻响应处理成功结果给 TC
  2. 异步队列删除 UNDO_LOG 表的记录

更详细流程

下方解释和图表来自对豆包的总结, 不保证正确.

TC 是被动接收方, 通过 @GlobalTransactional 注解实现开启事务、提交、回滚、获取当前状态等方法. 根据context判断是rm还是tm, tc在一开始是不知道有多少rm的, 而是通过 tm 调用 rm, 然后 rm 主动携带 branchID xid 等向 tc 报道. 在完成后 tm 会根据执行向 tc 上报 commit/rollback

步骤发起方协议 / 动作接收方作用
1TMGlobalBeginRequestTC开启全局事务,获取 XID
2TMRPC 调用 (含 XID)RM业务驱动 RM 干活
3RM执行本地 SQL, 生成 UNDO LOGDB执行业务逻辑
4RMBranchRegisterRequest (含 lockKey)TCRM 主动报到,TC 知道了 RM 的存在并分配全局锁
5RMCommit Local TransactionDB提交本地事务,释放本地锁
6TMGlobalCommit/RollbackRequestTC告诉 TC 是提交还是回滚
7TCBranchCommit/RollbackRequestRMTC 主动推送命令,让 RM 执行二阶段操作
8RM删除 UNDO_LOG / 执行回滚 SQLDB完成二阶段收尾

TCC Try-Confirm-Cancel

  1. 用户下单商品, 执行Try检查商品和预留字段的库存, 增加预留库存
  2. 操作1 无问题执行Confirm, 实际扣减商品库存, 删除预留库存.
    操作1 存在问题执行Cancel, 取消预留库存, 执行其他恢复代码

优点: 性能好于2PC, 因为取消了事务导致的长锁(但可能需要短锁/乐观锁保证库存检查和预留)

缺点: 开发量大, 每个业务均需编写三个接口: TrySell, ConfirmSell, CancelSell, 需修改数据库表结构

衍生方案

通用型 TCC 异步化

try阶段不变, confirm/cancel 阶段基于 MQ 异步

异步确保型 TCC

借用 Seata 图片, 感觉类似于 RocketMQ 的半消息机制

  1. try阶段发消息给mq, 只做存储, 消费者无法读取到
  2. confirm/cancel 阶段消费者才可以读取

异步确保型TCC

补偿型 TCC

https://seata.apache.org/zh-cn/blog/tcc-mode-applicable-scenario-analysis/#%E5%9B%9B%E8%A1%A5%E5%81%BF%E5%9E%8B-tcc-%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88 seata官网文章

补偿型 TCC 适用并发冲突少 / 外部业务, 只需要 Try 和 Cancel 两个接口即可实现

  1. 我向A, B发送Do请求
  2. 如果都成功则正常处理
    如果A / B 出现问题则发送 Compensate 请求补偿

seata会自己解决原子性问题, 这里我懒得深入了解了

基于本地消息的最终一致性方案

  1. 拥有业务表A, 信息表B
  2. 执行业务, 首先启动事务, 事务中执行业务, 同时将消息插入消息表中. commit/rollback 保证业务和消息100%不会丢失
  3. 定时任务扫描信息表, 发送信息到MQ
  4. 消费方读取消息处理

优点: 实现简单, 不依赖外部消息中间件的事务, 本地消息绝对不会丢失

缺点: 业务表和消息表耦合, 定时任务有延迟

基于可靠消息的最终一致性方案

即借用外部消息中间件的事务功能, RocketMQ叫半消息机制, Kafka叫生产者事务(0.11引入), RabbitMQ叫AMQP事务

通用流程 image-20260215233206772

RocketMQ流程

极端情况下, 业务代码在执行commit/rollback时可能会出现网络波动导致没有正确处理, 这时会有事务回查(补偿)机制, RocketMQ 会主动询问此事务是commit还是rollback

Kafka

kafka流程同上, 但没有回查机制, 依靠三种状态: committed, aborted, incomplete和事务协调器兜底.

  1. 事务开启, 消息发送
  2. commit/abort没有送达
  3. 根据transaction.timeout.ms 配置, 超时将事务标记为Aborted
  4. 结束

消费者默认仅读取committed 消息, 可通过isolation.level=read_committed 配置

可以手写补偿机制完善.

RabbitMQ

基于AMQP协议的原生事务性能较差, 生产环境中几乎不用, 此事务流程同上

更多使用发布确认机制(流程不同上):

  • 单个确认:
    image-20260215235747661
  • 批量确认: 流程和单个确认相同, 是堆积到一定数量消息后, 将消息一次性发送过去等待整批响应. 缺点是无法确定批次中哪个消息出现问题
  • 异步确认(性能最优): 但需要手动完善机制才能实现RocketMQ效果
    image-20260216000143922

最大努力通知方案

image-20260216001622681

优点: 实现简单, 最柔, 整体流程无锁

缺点: 一致性最差

Saga

将长事务拆分为多个单步事务, 每个事务对应自己的补偿失误

定义事务为T, 取消为C

  • 正常流程: T1->T2->T3->T4
  • 失败失败:T1->T2->T3失败->C3->C2->C1

两种模式

编排式(生产常用)

由协调器调配: 先做1->再做2, 如果2失败协调器下令回滚1

协作式

服务直接靠消息/其他手段互相通知

A 做完发消息, B 监听做, B 失败发消息, A 自己回滚

结论

优点: 支持长事务, 并发高, 整体流程无锁(即不会像2PC 那样所有T 均上锁)

缺点: 补偿逻辑复杂, 中间状态会暴露给用户

衍生方案

嵌套 Saga

Saga 只允许两个层次的嵌套, 即 父Saga-多个子Saga. 而嵌套 Saga 则是将一个/多个子事务合并成一个独立、完整的 Saga 事务.

  • 子 Saga 拥有自己完整的正向执行流和反向补偿流
  • 子 Saga 对外暴露原子化的事务接口, 父 Saga 将其视为普通事务节点, 不感知其内部实现, 也不能单独调用子 Saga 中的内部某个事务
  • 子 Saga 不能依赖父 Saga 的其他子 Saga 资源
执行流程

Root Saga 流程为 A → B → C,其中 B 是嵌套子 Saga(包含 B1→B2→B3)

  • 正向:A 成功 → 子 Saga B 全流程执行成功 → C 成功,全局事务完成
  • 异常:A 成功 → 子 Saga B 执行到 B2 失败 → 子 Saga B 先补偿 B2→补偿 B1,完成自回滚 → 向 Root Saga 返回失败 → Root Saga 补偿 A,全局回滚完成

基于事件溯源(Event Sourcing, ES)的事务方案

ES指: 不存储实体的当前状态,只存储所有导致状态变更的不可变事件.

比如update set a = 50 where id = 1; 这里我们不存储a=50这个结果, 而是存储 将 id 为 1 的记录, a 字段修改为 50 这个操作.

ES + Outbox Pattern

  1. 收到操作请求, 记录请求的操作到 Event Store 中,状态标记为 待发布.
  2. 按顺序投递存储的操作到 MQ, 状态标记为 已发布
  3. 下游监听 MQ, 执行本地事务并写入到自身 Event Store, 并且通过唯一 id 实现幂等性.

ES + Saga

  1. 订单服务接收请求, 在本地事务中生成订单创建事件写入 Event Store, 发布到事件总线
  2. Saga 编排器监听订单创建成功事件, 触发库存服务扣减库存, 库存服务完成后生成库存扣减成功事件写入自身 Event Store 并发布
  3. 编排器依次监听和触发, 直到整个 saga 事务结束

即使服务宕机也可以通过 ES 重放

CQRS(读写分离) + ES

CQRS: 命令查询职责分离, 将写操作和读操作完全拆分.

写侧(命令端):完全基于 ES 实现,只负责处理业务命令、生成不可变事件、写入 Event Store,保障写事务的原子性, 是整个系统唯一的事实源

读侧(查询端):通过监听写侧的事件流, 更新适配查询场景的读库(MySQL/ES/Redis 等),通过事件的可靠消费保障读写最终一致性

跨服务分布式事务通过「命令→事件→命令」的事件驱动流转实现,全程无锁、无耦合。

  1. 写侧处理:业务请求以 Command 形式提交到写服务,写服务验证命令合法性后,在本地事务中生成业务事件写入 Event Store,完成写侧事务,事件发布到事件总线
  2. 跨服务事务流转:下游服务监听对应事件, 转化为自身的业务 Command, 执行本地写操作并生成新的事件写入 Event Store, 完成跨服务事务的原子性流转
  3. 读侧同步:读侧的投影服务(Projection)监听事件总线,根据事件类型更新对应的读库(如订单列表写入 MySQL、热点数据写入 Redis)。

一致性保障:通过事件唯一 ID 实现消费幂等,确保事件只被处理一次. 读侧更新失败会自动重试,直到成功;若读侧数据出现不一致,可直接重放 Event Store 的事件,重新生成全量读库数据,彻底修复一致性问题。

事务回滚:跨服务事务失败时生成补偿事件,写侧 Event Store 持久化补偿事件,读侧投影服务同步更新读库,完成回滚后的全链路一致性。

参考

[]: https://seata.apache.org/zh-cn/docs/overview "Seata文档"
[]: https://javaguide.cn/distributed-system/protocol/cap-and-base-theorem.html "CAP & BASE理论详解"
[]: https://www.doubao.com/ "豆包"

我来吐槽

*