分布式事务
为什么需要分布式事务
Section titled “为什么需要分布式事务”在微服务架构中,一个业务操作往往跨越多个服务和数据库,单个数据库的本地事务无法保证跨服务的数据一致性。
电商下单场景: 订单服务:创建订单(订单DB) 库存服务:扣减库存(库存DB) 账户服务:扣减余额(账户DB)
问题:如何保证三个操作要么全部成功,要么全部回滚? → 每个服务有自己的数据库,无法使用同一个 ACID 事务2PC(Two-Phase Commit,两阶段提交)
Section titled “2PC(Two-Phase Commit,两阶段提交)”2PC 是一种经典的分布式事务协议,适用于单个协调者协调多个资源管理器的场景。
参与者: 协调者(Coordinator):事务管理器,如 Java 中的 JTA 参与者(Participant):资源管理器,如数据库
第一阶段:准备(Prepare / Voting) 协调者 → 所有参与者:Prepare(准备提交) 每个参与者: 执行事务操作,写入 undo/redo 日志 但不提交(持有锁) 回复:Yes(可以提交)或 No(出现问题)
第二阶段:提交/回滚(Commit / Rollback) 所有参与者都回复 Yes → 协调者发送 Commit 任意一个参与者回复 No → 协调者发送 Rollback 参与者提交或回滚,释放锁,回复 ACK2PC 的问题
Section titled “2PC 的问题”1. 同步阻塞(Blocking): Prepare 阶段参与者持有锁等待 Commit/Rollback 如果协调者崩溃,参与者永久阻塞
2. 单点故障(Single Point of Failure): 协调者崩溃导致所有参与者阻塞,系统无法继续
3. 脑裂(Split Brain): 协调者发送 Commit 后崩溃,部分参与者提交,部分未收到 → 数据不一致
4. 性能: 持锁时间 = 两个网络往返(至少 2 RTT),加上数据库操作时间 高并发场景下锁竞争严重3PC(Three-Phase Commit)
Section titled “3PC(Three-Phase Commit)”3PC 在 2PC 的基础上增加了一个”预提交”阶段,并引入超时机制,试图解决 2PC 的阻塞问题。
第一阶段:CanCommit 协调者询问参与者"能否提交?" 参与者只做检查(不锁资源),回复 Yes/No
第二阶段:PreCommit 所有 Yes → 协调者发 PreCommit 参与者执行事务,写日志,持有锁 参与者有超时:超时后自动提交(区别于 2PC 超时后阻塞)
第三阶段:DoCommit 协调者发 DoCommit → 参与者提交
改进: 参与者有超时自动提交(减少阻塞) CanCommit 阶段不持有锁
3PC 仍未完全解决脑裂问题,且实现复杂,实际中很少直接使用TCC(Try-Confirm-Cancel)
Section titled “TCC(Try-Confirm-Cancel)”TCC 是一种应用层面的分布式事务方案,通过业务代码自己实现补偿逻辑。
三个阶段: Try(预留):检查并预留资源(不直接操作,只冻结) Confirm(确认):所有 Try 成功后执行实际业务操作 Cancel(取消):Try 失败或超时后释放预留资源
以转账为例: Try: 账户A:检查余额 ≥ 100,冻结 100 元(balance -= 100, frozen += 100) 账户B:检查账户有效性,创建冻结记录
全部 Try 成功 → Confirm: 账户A:解除冻结,余额减少已生效(frozen -= 100) 账户B:增加余额,释放冻结记录(balance += 100)
任意 Try 失败 → Cancel: 账户A:解冻(frozen -= 100, balance += 100) 账户B:删除冻结记录TCC 的关键挑战
Section titled “TCC 的关键挑战”1. 幂等性(Idempotency): 网络重试导致 Confirm/Cancel 可能重复调用 每个操作必须幂等(相同操作执行多次结果不变) 方案:使用全局唯一 txId 记录已处理的请求,重复请求直接返回成功
2. 空回滚(Empty Rollback): Try 由于网络问题未到达,但 Cancel 先到了 Cancel 必须能处理 Try 未执行的情况(直接返回成功) 方案:Try 时写记录,Cancel 时检查记录是否存在
3. 悬挂(Hanging / Suspend): 网络拥塞导致 Cancel 先执行完,Try 才到达 Try 执行后资源会被永久冻结(没有对应的 Cancel 了) 方案:Cancel 时记录"该 txId 已被回滚",Try 到来时检查,若已回滚则直接返回TCC 适用场景:对一致性要求较高、需要强隔离性的场景,如金融交易。代价是需要为每个业务操作实现 Try/Confirm/Cancel 三个接口,开发成本较高。
Saga 模式
Section titled “Saga 模式”Saga 将长事务分解为一系列有序的本地事务,每个本地事务有对应的补偿事务。
Saga 的两种协调模式:
1. 编排(Choreography): 没有中央协调者,服务间通过消息/事件驱动
订单创建 → 发布"订单创建"事件 库存服务消费事件 → 扣减库存 → 发布"库存扣减成功"事件 账户服务消费事件 → 扣减余额 → 发布"余额扣减成功"事件
优点:松耦合;缺点:事务流程分散,难以追踪
2. 编排(Orchestration): 中央 Saga 协调者按顺序调用各服务
SagaOrchestrator: 1. 调用 OrderService.createOrder() → 成功 2. 调用 InventoryService.deductStock() → 成功 3. 调用 AccountService.deductBalance() → 失败! 4. 补偿:调用 InventoryService.restoreStock() 5. 补偿:调用 OrderService.cancelOrder()
优点:流程清晰,便于监控;缺点:协调者可能成为单点
Saga vs TCC: Saga 没有 Try 阶段(直接执行),隔离性较差(中间状态对外可见) TCC 有 Try 预留阶段,隔离性更好,但实现更复杂 Saga 适合:允许中间状态暂时暴露的长流程业务(如电商订单流程) TCC 适合:需要强隔离性的金融场景本地消息表(可靠消息最终一致性)
Section titled “本地消息表(可靠消息最终一致性)”这是生产环境中最常用的分布式事务方案,实现了最终一致性,成本低。
场景:订单创建后发送消息通知积分服务
本地消息表方案:
订单服务数据库: orders 表(主业务) local_messages 表(本地消息,与 orders 在同一数据库)
步骤 1:本地事务(原子) BEGIN TRANSACTION INSERT INTO orders(...) -- 创建订单 INSERT INTO local_messages(...) -- 写入消息记录(status=PENDING) COMMIT
步骤 2:定时任务(或触发器) 查询 local_messages 中 status=PENDING 的记录 发送到消息队列 成功后更新 status=SENT
步骤 3:消费者(积分服务) 消费消息 → 幂等处理(防重复消费)→ 执行业务
步骤 4:消息确认 消费者 ACK → 消息队列删除消息 订单服务定时任务更新 local_messages status=DONE
特点: ✓ 实现简单,与业务代码侵入性小 ✓ 生产可靠,消息不丢失(本地事务保证) ✗ 中间状态对外可见(最终一致性,不是强一致性) ✗ 需要处理消费者的幂等性Seata 框架
Section titled “Seata 框架”Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务框架,提供 AT、TCC、Saga、XA 四种模式。
Seata AT 模式(最常用)
Section titled “Seata AT 模式(最常用)”AT 模式对业务代码无侵入,通过拦截 SQL 自动生成 undo log 实现回滚。
组件: TC(Transaction Coordinator):Seata Server,管理全局事务状态 TM(Transaction Manager):事务发起方,声明全局事务边界 RM(Resource Manager):各微服务,管理分支事务
两阶段: 一阶段(执行业务): 1. TM 向 TC 注册全局事务,获取 XID 2. 每个 RM 执行本地 SQL,同时: - 解析 SQL,查询 before image(数据执行前的快照) - 执行业务 SQL - 查询 after image(数据执行后的快照) - 写入 undo_log(before image + after image) - 向 TC 注册分支事务,提交本地事务(锁释放)
二阶段(提交/回滚): 全部成功 → TC 通知各 RM 删除 undo_log(已提交,无需回滚) 失败 → TC 通知各 RM 用 undo_log 中的 before image 进行数据回滚
特点: ✓ 对业务代码几乎无侵入(只需 @GlobalTransactional 注解) ✓ 性能好(一阶段就释放了数据库锁) ✗ 基于数据库的 undo log,依赖特定数据库 ✗ 全局事务期间若有其他事务修改数据,可能造成脏写(需全局锁配合)如何选择分布式事务方案
Section titled “如何选择分布式事务方案”方案对比:
方案 一致性 性能 侵入性 适用场景─────────────────────────────────────────────────────2PC 强一致性 低 低 数据库 XA 事务TCC 强一致性 中 高 金融高一致场景Saga 最终一致性 高 中 长流程业务本地消息表 最终一致性 高 低 跨服务通知,允许最终一致Seata AT 最终一致性 中 极低 业务改造成本低,一般业务场景
实际建议: 大多数业务(电商、积分、通知)→ 本地消息表 + 消息队列(最终一致) 金融转账、扣款 → TCC(强隔离) 遗留系统改造,无法修改业务代码 → Seata AT 长流程审批(无法回滚只能补偿)→ SagaQ:2PC 有什么问题?
主要有三个:①同步阻塞,Prepare 阶段参与者持有锁等待协调者,协调者崩溃导致所有参与者无限阻塞;②单点故障,协调者是单点,崩溃影响整个事务;③脑裂,协调者发出 Commit 后崩溃,部分参与者提交部分未提交,数据不一致。
Q:TCC 的 Cancel 操作需要注意什么?
需要处理三种特殊情况:①幂等性,Cancel 可能因重试被多次调用,必须保证幂等;②空回滚,Try 未到达但 Cancel 先到,Cancel 要能处理未 Try 的情况直接成功;③悬挂,Cancel 先执行完后 Try 才到达,需要记录”已回滚”状态,阻止后续的 Try 执行。
Q:如何保证消息队列的可靠传递(不丢不重)?
不丢:生产者使用本地消息表 + 定时重发,确认机制(confirm);消息队列持久化;消费者手动 ACK(处理完业务再确认)。不重:消费者实现幂等性,通过唯一消息 ID 或业务 ID 去重(Redis SET、数据库唯一索引)。
Q:Saga 和 TCC 有什么区别?
TCC 有 Try 预留阶段,在预留阶段冻结资源,提供较强的隔离性,中间状态不对外暴露;实现复杂,需要为每个操作写三个方法。Saga 没有预留阶段,直接执行本地事务,中间状态对外可见(最终一致);实现相对简单,适合允许临时不一致的长流程业务。