分布式事务:跨服务的数据一致性
难度:高级 | 预计时间:55 分钟 | 前置:消息队列
📖 下单成功,扣款也成功了,库存没减
故事背景
你的微服务电商平台出了一起严重事故:用户下单成功→订单服务创建了订单→支付服务扣了款→调用库存服务减库存时超时了。结果:钱扣了,但库存没减,订单状态还是"待支付"。用户支付了钱却显示未付款——投诉电话打爆了。
这就是分布式事务要解决的核心问题——在多个独立的服务之间保证数据一致性。单机事务(@Transactional)只在本服务的数据库内有效——跨了服务边界就失控了。这节课教你 CAP 定理、BASE 理论、以及业界主流方案 Seata(阿里开源的分布式事务框架)。
💻 代码演示
一个订单创建流程涉及 3 个服务——传统方案 vs Seata AT 模式的对比:
// ===== 问题场景:下单 = 订单服务 + 库存服务 + 账户服务 =====
// 订单服务创建订单 → 库存服务减库存 → 账户服务扣款
// 如果库存减成功了,但扣款失败——库存必须回滚(补偿)
// ===== 方案1:Seata AT 模式(对业务代码几乎无侵入)=====
@GlobalTransactional // 只需加这个注解!Seata 自动管理全局事务
public void createOrder(OrderRequest req) {
orderService.create(req); // 本地事务
stockClient.deduct(req.productId()); // 远程调用:Seata 自动管理 Undo Log
accountClient.debit(req.userId(), req.amount()); // 远程调用
// 任一环节失败,Seata 自动回滚所有服务的操作
}
// ===== 方案2:最终一致性 + 本地消息表(不用 Seata)=====
@Transactional
public void createOrder(OrderRequest req) {
// 1. 创建订单 + 本地消息表(同一事务)
orderMapper.insert(order);
messageMapper.insert(new Message("stock_deduct", order.toJSON()));
// 2. 定时任务扫描消息表,发送 MQ → 库存服务消费 → 扣款服务消费
// 3. 每个服务处理完发"完成"消息 → 下游继续,或发"回滚"消息
}
// 全局回滚逻辑:
// 库存服务回滚 = 加回库存
// 账户服务回滚 = 退款
// 订单服务回滚 = 标记订单为"已取消"运行输出:
Seata AT 模式原理:
1. 每个服务的本地事务提交前,Seata 记录 Undo Log(反向 SQL)
2. 全局事务提交 → 各服务 Undo Log 删除
3. 全局事务回滚 → 各服务执行 Undo Log 中的反向 SQL(delete→insert, update→反向update)🎯 三个核心问题
这是什么?
CAP 定理:分布式系统无法同时满足 Consistency(强一致性)、Availability(高可用)、Partition Tolerance(分区容错)三者。实际中 P 必须保证(网络一定会分区),所以在 CP(强一致+分区容错)和 AP(高可用+分区容错)之间选择。
BASE 理论:Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性)——放弃强一致性,用最终一致性换取高可用。
Seata:阿里开源方案。AT 模式(自动补偿,基于 Undo Log)、TCC 模式(Try-Confirm-Cancel,手动编码)、Saga 模式(长事务编排)。
为什么需要它?
为什么分布式事务这么难?单机事务依赖数据库的 ACID——一个连接、一个数据库、一个 undo log。跨服务后没有"全局 undo log"——每个服务的数据库是独立的。要让多个独立的数据库原子性地提交或回滚,需要一个协调者(Seata TC / 消息队列 / 工作流引擎)来统一决策。
大多数互联网场景不需要强一致性——"下单后几秒钟库存才更新"是可接受的(最终一致性)。只有金融交易等场景需要强一致性。
如果没有它会怎样?
如果没有分布式事务方案——跨服务数据不一致会成为常态。网上常见的"钱扣了但订单没生成"式的 bug,根本原因就是缺少分布式事务保护。你手写补偿逻辑容易遗漏——正交易写了 3 个服务,但反向补偿只写了 2 个,第 3 个就成了永远的脏数据。
📝 原理讲解
分布式事务方案选型速查:
Seata AT 模式:对业务代码几乎零侵入(只需 @GlobalTransactional),自动生成 Undo Log。适合大多数场景。局限:依赖数据库的 ACID,不支持 Redis/Mongo 等非关系型数据库的自动回滚。
Seata TCC 模式:需要手动实现 Try(预留资源)、Confirm(提交)、Cancel(释放资源)三个方法。侵入性高但性能好、不依赖数据库 Undo Log。
本地消息表 + 定时任务:最简方案——本地事务同时写业务数据和消息表;定时任务扫描未发送的消息投递到 MQ;下游做幂等消费。适合对一致性要求不极端的场景。
事务消息(RocketMQ):RocketMQ 支持事务消息——先发 half 消息,执行本地事务后 commit 或 rollback。比本地消息表更实时。
Saga 模式:长流程事务——每个步骤都有对应的补偿操作(正向:创建订单,反向:取消订单)。适合流程长、涉及第三方接口的场景。
核心设计原则:
幂等性:所有远程调用必须是幂等的——重试不会产生副作用。
补偿 > 回滚:分布式场景下"回滚"几乎不可能(已经发出去的短信、已经推送的通知无法撤回)——用补偿操作(退款、状态标记)达成业务上的"回滚"。
最终一致性 ≠ 不一致:最终一致性是说"如果没有新的更新,最终所有副本都会一致"——不一致窗口通常秒级。
🎨 生活类比
类比理解
单机事务(@Transactional)像在一个保险箱里操作——要么全部放进去锁好,要么全部拿出来,没有中间状态。 分布式事务像多个银行之间转账——A 行扣 100 块,B 行加 100 块。这两步不可能"同时"完成——中间总会有一个时间窗口 A 扣了 B 还没加(钱"消失"了一瞬间)。分布式事务的目标不是消除这个窗口,而是保证它最终闭合。 CAP 定理像"快、好、省"不可能三角——你只能选两个。CP 模式(强一致)牺牲可用性——银行转账宁可暂时不可用也要保证钱不错。AP 模式(高可用)牺牲强一致——电商下单先给你个"受理中",后台慢慢对账。 补偿(Compensation)像围棋的"悔棋"——不是真的让时间倒流,而是通过一系列反向操作把棋盘恢复到之前的状态。
✏️ 动手练习
练习 1
用本地消息表实现一个"下单→扣库存"的最终一致性方案:订单服务创建订单时同时写消息表,定时任务发 MQ,库存服务消费并扣库存。
<details> <summary>💡 查看提示</summary>
消息表字段:id, topic, body, status(PENDING/SENT), create_time。定时任务每秒扫 status=PENDING 的记录,发 MQ 后更新 status=SENT。库存服务做幂等——消息 ID 已处理则跳过。
</details>
练习 2
设计一个 TCC 模式的转账接口:A 转 100 元给 B。实现 Try(冻结A的100元)、Confirm(扣A加B)、Cancel(解冻A的100元)。
<details> <summary>💡 查看提示</summary>
Try 阶段:A 账户余额减 100、冻结金额加 100;Confirm:A 冻结金额减 100、B 余额加 100;Cancel:A 冻结金额减 100、余额加 100。所有操作需要在接口里做状态检查(防止重复 Confirm/Cancel)。
</details>
练习 3
画出 CAP 的三者关系图,分析你做的项目应该选 CP 还是 AP,为什么。
<details> <summary>💡 查看提示</summary>
这是一个设计思考题。金融/支付场景→CP(宁可短暂不可用也要数据正确);社交/内容/推荐场景→AP(短暂不一致可接受,但不能让用户等待);大多数互联网业务→AP + 最终一致性。
</details>
✅ 自检站
<details> <summary><strong>CAP 定理的三者分别是什么?为什么互联网场景通常选 AP?</strong></summary>
C(强一致性):所有节点同时看到相同数据。A(可用性):每个请求都能得到响应(不一定是"正确"数据)。P(分区容错):网络分区(节点间通信中断)时系统仍能工作。P 是分布式系统的物理现实——网络一定会断。所以在 CP(断网时拒绝请求,保证数据一致)和 AP(断网时返回可能过期的数据,保证服务可用)之间选。互联网场景用户体验优先→AP 牺牲一致性换来随时可用。
</details>
<details> <summary><strong>Seata AT 模式的 Undo Log 是怎么工作的?</strong></summary>
Seata 在本地事务提交前,解析 SQL 生成反向 SQL 作为 Undo Log(INSERT → DELETE, UPDATE → 反向 UPDATE, DELETE → INSERT)。全局事务提交成功→删除 Undo Log。全局事务回滚→执行 Undo Log 中的反向 SQL。这个机制依赖数据库的 ACID 特性——反向 SQL 必须和原 SQL 在同一个数据库上执行才能保证一致性。
</details>
<details> <summary><strong>最终一致性和强一致性的区别是什么?什么场景只能用强一致性?</strong></summary>
强一致性:操作完成后所有节点立即看到一致数据(单机事务就是强一致)。最终一致性:操作完成后有一段时间不一致,但最终会达到一致(秒级到分钟级)。强一致性场景:银行转账(钱不能凭空消失一会)、库存扣减(不能超卖)。最终一致性场景:用户点赞数更新(晚几秒显示无所谓)、订单状态同步("支付中"→"已支付"有几秒延迟可接受)。
</details>