分布式事务:2PC、Saga、TCC

60 阅读 2206 字 · 约 8 分钟

共识协议让多个节点就一个值达成一致。但还有一种场景:一个操作涉及多个独立的资源,要么全成功,要么全失败。这就是分布式事务。

单机事务靠数据库的 ACID——一个 BEGINCOMMIT,数据库自己保证原子性。但跨了多个数据库、多个服务,没有谁替你兜底。协调多个独立系统的一致提交,就是分布式事务要解决的问题。

为什么共识之外还要讲事务

共识解决的是「多个节点就同一个值达成一致」。事务解决的是「多个节点就各自的状态达成一致」。区别在于:共识的参与者是对等的,投的是同一件事;事务的参与者各管各的资源,要协调的是「你们各自的操作能不能一起成功」。

场景很日常。下单要扣库存、扣钱、生成订单——三个操作跨三个服务,各自有各自的数据库。如果扣库存成功、扣钱成功、生成订单失败,前面两个得回滚。共识协议帮不上忙——这不是就一个值投票,是三个独立动作的原子性。

2PC:最朴素的思路

两阶段提交(Two-Phase Commit)是最直接的想法。

一个协调者,多个参与者。第一阶段(Prepare):协调者问所有参与者——你们能不能提交?参与者把事务日志写到本地磁盘,回复 ready。第二阶段(Commit):协调者收到所有 ready 后,通知所有人提交。任何一个参与者回复 no,协调者通知所有人回滚。

2PC 的朴素就朴素在:它把「全做或全不做」直接翻译成「先问,再做」。MySQL XA 事务就是 2PC 实现——跨多个 MySQL 实例的分布式事务,底层走的就是两阶段提交。

问题也明显。协调者是单点——它挂了,参与者就悬着。如果协调者在通知提交之前挂了,参与者已经锁了资源,等不到通知,只能干等。这是 2PC 最致命的问题:阻塞。参与者 prepare 之后,资源就锁住了,协调者不回来,谁也不敢动。生产环境里,2PC 的悬挂事务是运维的噩梦——锁住的资源只能手动介入。

第二个问题:2PC 假设参与者能可靠回复。一个参与者网络断了或者超时,协调者就要等,等不到就阻塞。同步阻塞让 2PC 在高延迟场景下几乎不可用——所有参与者串行等待,一个慢全慢。

3PC:加超时的尝试

三阶段提交(Three-Phase Commit)在 2PC 中间插了一环:预提交。

第一阶段还是问。第二阶段(Pre-Commit):协调者发预提交,告诉参与者「马上要提交了,准备一下」。参与者收到后知道马上就要决定。第三阶段才是真正提交。

关键是:3PC 给参与者加了超时。如果参与者等了太久没收到协调者的通知,它会自己决定——超时后自动提交或回滚。这减轻了阻塞:协调者挂了,参与者不会再无限等待。

但 3PC 没根治。网络分区时——协调者和部分参与者通了,另一部分不通——先收到预提交的参与者超时后自动提交,没收到预提交的参与者超时后自动回滚,数据不一致了。所以 3PC 在生产环境里很少真正用——它把阻塞换成了脑裂,代价不见得更小。

Saga:拆成小事务

Saga 换了个思路:大事务拆成串行的小事务,每个小事务在自己的数据库里正常提交。失败了怎么办?反方向跑补偿——后面的小事务把前面的小事务逆向操作掉。

核心想法:长事务不要锁资源。每个步骤独立提交,失败不是回滚是补偿。

订单、库存、支付三个服务的 Saga:1) 创建订单(独立提交),2) 扣库存(独立提交),3) 扣钱(独立提交)。如果 3 失败,启动补偿链——退钱、退库存、取消订单。每一步都是独立事务,没有全局锁。

Saga 的问题是:中间状态是暴露的。步骤 1 之后,订单已经是「已创建」状态,步骤 2 还没跑。如果这时候有人去查订单、查库存,看到的是不一致的数据。Saga 用隔离性换可用性——接受中间状态暴露,换来不用锁资源。

Seata 的 Saga 模式是这个思路。事件驱动的微服务里,Saga 是常见选择。

TCC:预留资源

TCC(Try-Confirm-Cancel)比 Saga 更严格。每个参与者提供三个接口:Try(预留资源,不真正执行),Confirm(确认执行),Cancel(释放资源)。

和 Saga 的关键区别:Try 阶段资源就锁住了,只是没真正扣。Confirm 阶段是真正扣。中间状态虽然没执行,但资源已经预留——不会出现 Saga 那种「中间状态数据不一致」。

Try 阶段,库存服务把库存冻结(不是扣减),支付服务把额度冻结。Confirm 阶段,真正扣库存、扣钱。Cancel 阶段,释放所有冻结的资源。

TCC 要求每个服务提供三个接口,实现成本比 Saga 高。但换回来的是:没有中间状态的脏读,资源是预留的。银行转账、支付场景里,TCC 是标准选择——额度冻结和确认扣钱之间的窗口,数据是一致的。

怎么选

分布式事务三方案:2PC 先问再做(协调者单点、阻塞)、Saga 拆小事务加补偿(中间状态暴露)、TCC 预留加确认(实现成本高)

方案核心思路代价
2PC先问再做协调者单点、阻塞
3PC加超时脑裂风险,很少用
Saga拆小事务+补偿中间状态暴露
TCC预留+确认实现成本高

选哪个看业务。能接受短锁的用 2PC——事务量小、可靠性高。不能接受锁的用 Saga——长流程、跨服务。对一致性要求高的用 TCC——钱相关的场景。

小结

分布式事务是共识问题的另一种形态——不是让节点就一个值投票,是让多个独立操作原子完成。2PC 最朴素,协调者问所有人能不能做,做了就不回头。Saga 把长锁拆成短事务链,失败靠补偿,换来了可用性丢了隔离性。TCC 把资源先锁再扣,比 Saga 更严格但实现更重。

没有哪个方案是免费的。2PC 的锁、Saga 的补偿、TCC 的三接口——每个都是「保证一致性」换来的「增加了复杂度」。选哪个,取决于你的业务能承受哪种代价。