一、分布式事务概述
为什么需要分布式事务
说实话,这个问题我当年刚接触分布式系统时也困惑过。
单体应用时代,事务很简单。一个 @Transactional 注解,ACID 全搞定。但系统一拆,问题就来了。
举个例子。你在电商平台下单,涉及三个服务:
- 订单服务:创建订单记录
- 库存服务:扣减商品库存
- 账户服务:扣减用户余额
这三个操作,必须全部成功,或者全部失败。不能出现「订单创建了,库存没扣」的情况。
这就是分布式事务要解决的核心问题——跨多个独立数据源,保证数据一致性。
核心矛盾:本地事务只能管自己的数据库。跨服务、跨数据库的操作,需要一套新的协调机制。
我在项目中遇到过最典型的场景:用户支付成功后,订单状态更新了,但积分没加上。用户投诉,运营查半天,最后发现是分布式事务没处理好。嗯,从那以后我再也不敢轻视这个问题。
分布式事务的挑战
CAP 理论——你只能选两个
CAP 理论是分布式系统的「不可能三角」。说白了就是:
- C(一致性):所有节点看到的数据是一样的
- A(可用性):每个请求都能得到响应
- P(分区容错性):网络出问题时系统还能工作
你想想看,网络分区是必然发生的。所以 P 必须选。剩下的 C 和 A,你只能二选一。
| 选择 | 典型场景 | 代表系统 |
|---|---|---|
| CP | 银行转账、证券交易 | ZooKeeper、Etcd |
| AP | 社交动态、商品浏览 | Eureka、Cassandra |
我个人习惯这样判断:跟钱相关的,优先 CP;跟体验相关的,优先 AP。
避坑指南:我曾经在一个支付系统中盲目追求强一致性,结果一次网络抖动导致整个支付链路阻塞了 30 秒。后来改成最终一致性 + 补偿机制,反而更稳定。
BASE 理论——现实的选择
BASE 理论是 CAP 的「妥协方案」。它告诉我们:
- BA(基本可用):系统允许部分功能降级
- S(软状态):中间状态是允许的
- E(最终一致性):经过一段时间后,数据最终会一致
说白了,就是别追求「实时一致」,接受「最终一致」。这在金融系统里其实很常见——你转账后,对方账户不会立刻到账,但最终会到。
我建议这样理解:CAP 是理想,BASE 是现实。99% 的分布式系统都在 BASE 的框架下工作。
分布式事务的应用场景
场景一:电商下单
这是最经典的场景。用户下单时:
1. 订单服务创建订单(状态:待支付)
2. 库存服务预扣库存
3. 优惠券服务锁定优惠券
4. 支付完成后,更新订单状态
5. 库存服务确认扣减
6. 优惠券服务确认使用
这里有个坑:如果支付成功但库存扣减失败怎么办?
我曾经处理过一个线上事故:双十一大促,库存服务压力过大,导致部分订单支付成功但库存没扣。最后靠定时任务 + 人工核对才修复。嗯,后来我们引入了 TCC 模式,才彻底解决这个问题。
场景二:跨行转账
跨行转账是分布式事务的「硬骨头」。
假设 A 行给 B 行转账 1000 元:
- A 行扣减 1000 元
- B 行增加 1000 元
这两个操作必须原子。但 A 行和 B 行是两套独立的系统,甚至可能用不同的数据库。
实际中怎么做的?
- A 行发起转账请求,记录转账流水
- A 行扣款成功,状态标记为「扣款完成」
- 通过清算系统(如央行支付系统)通知 B 行
- B 行收到通知后入账
- 如果 B 行入账失败,A 行发起冲正
你看,这就是典型的 最终一致性 + 补偿机制。
场景三:库存扣减
库存扣减是高频操作,对性能要求极高。
我见过很多团队用「先查库存 → 判断是否足够 → 扣减」这种三步走。但高并发下,这三步之间数据可能已经变了。
正确的做法是:
-- 原子扣减,返回受影响行数
UPDATE inventory
SET stock = stock - #{quantity}
WHERE product_id = #{productId}
AND stock >= #{quantity}
如果返回 0,说明库存不足。一步到位,不需要事务。
我的经验:库存扣减尽量用数据库的原子操作,别用「查-判-改」三步。实在要跨服务,用 Redis 的 Lua 脚本做原子扣减,性能比数据库高一个数量级。
知识体系总览
下面这张图,是我梳理的分布式事务知识体系。建议你保存下来,后面每个章节都会对应到其中的一个模块。
这张图展示了分布式事务的完整脉络。从核心问题出发,经过理论权衡,到具体场景,再到解决方案,最终回归到「高可用 + 最终一致性」这个目标。
后面的章节,我们会逐一深入每个方案。你准备好了吗?