当"阻塞"本身成为最大的敌人时,补偿型方案给出了另一条路:不追求"全或无"的强原子,而是把长事务拆成一串短事务,允许它们先各自成功,一旦后面某一步失败,就沿原路用"补偿动作"把前面已生效的改回来。本节讲 Saga 的编排方式、TCC 的三段式,以及"补偿"这件事到底由谁、按什么顺序来执行。
阅读完本节,你应当能够:
如果你运营的是一个高峰时下几万单的电商,用 2PC 锁着库存等你跨两个服务确认,很可能等来的不是"全或无",而是"全部超时+用户差评"。阻塞在低并发的金融核心库或许能忍,可在互联网的最前线,它是要命的。所以反思任务来了:能不能我干脆不锁,先把业务放行,等它走几步,如果后面发现走错了,再回头把它改回来?—这就引出了补偿型方案。
补偿的思想极朴素:把一个大动作拆成一串小动作,每个小动作成功之后,给它记一个"撤销指南"。当某个小动作失败时,就按"撤销指南"逆序执行前面那些成功的小动作的补偿动作,把系统恢复到开始之前。它放弃的是"强原子"(你可能会短暂看到做了前半段),换到的是"绝不阻塞 + 最后总能改回原样"。对很多"允许短暂中间态"的高并发业务来说,这个交易香甜得过头。
编排式 Saga:单独的编排器负责发号:先让 A 服务干活,成功后吹哨让 B 服务干,失败时由编排器按记录一步步指挥前面的服务回滚。好处是全局流程一眼可见、好调试;坏处是编排器又成了中心节点。
协同式 Saga:没有中心编排器,每个服务干完自己的活,就把"下一步该谁干"通过事件/消息告诉下一个服务;失败时也是靠事件链逆序传播去触发各家的补偿。好处是去中心化、扩展性好、更能扛故障;缺点是全局流程散落各处、出了错不好定位。
TCC(Try-Confirm-Cancel)把每个业务动作拆成三个可调用的动作:Try 先申请资源/预扣(不真生效);Confirm 确认执行(真的落定);Cancel 取消/回滚(释放预扣)。它比 Saga 更"细":Saga 把事务切成"服务级小步骤",TCC 把它切成"资源级的三段动作"。适用上,TCC 更适合那些"要先冻结资源、后续再落定"的典型业务(支付预授权、库存预占)。它最大的代价是侵入性强——每个参与的接口都要为三个动作单独编码,代码量直线上升。
这是补偿型方案最容易踩坑三件事,我逐一提醒:一,补偿必须是"逆序"执行,先成功的最晚被补偿,否则会踩出空指针式的边界——想象一下,先扣了库存再扣钱,如果失败了先补偿库存,钱还在已经扣的状态,你会发现用户被免费发了货。二,补偿动作必须幂等——因为网络会重试,重复执行 Cancel 要能安全地"多做一遍也没事",否则冷启动重试就把补偿做糊了——一次重复补偿可能把已经扣了金额再回滚一次,变成用户拿到了免单。三,补偿的可靠性要高于普通业务——补偿本身也可能失败,得配重试队列、告警,必要时让人工介入。很多系统为了保证这个可靠性,把补偿任务存在专门的持久化队列里,没执行成功就反复重试、直到人工干预。记住这三个提醒,补偿方案才不是"给自己挖坑"。
什么时候该走哪条路,用三问就能对号入座:一问你的业务允不允许短暂不一致——能,上补偿/Saga;不能,必须一步到位,老老实实回 2PC;二问你的事务是不是横跨多个长链路服务——是的话,2PC 把资源锁太久顶不住,上补偿;三问你能不能忍受"最后总能对",而不是"每个时刻都对"——用户下单扣个优惠,晚个一分钟最终一致完全没问题,Saga 轻装上阵;金融转账一分都错不得,老老实实上 2PC。用这三问比你读三页论文管用。
把"补偿能不能接得住"落到一张审查清单上,新项目上 Saga 前过一遍,能少踩雷:
一问,每一步有没有配得上一个明确的"撤销动作"? 比如"扣库存"的撤销就是"回库存"、"扣款"的撤销就是"退款"。没有逆动作的步骤(像"发短信"补不了)就要么不放进 Saga,要么接受它不可逆。
二问,撤销动作是否写对了方向? 最容易写反的是"先扣库存再扣款"这件事——失败时必须按原序从后往前补偿,一步逆着补,就会把用户送到"钱付了库存却没了"或"免单当拣到"的坑里。
三问,补偿是不是幂等的? 网络会重试,同一个 Cancel 被触发两遍也必须安全,否则一次重试就多补偿一次、把账做糊。
四问,补偿本身失败了怎么办? 光有补偿动作不够,还要配持久化的补偿队列、超时告警与人工兜底入口,保证"补不动"也有人管。
把四问钉进方案评审,Saga 从"纸上的浪漫"落地成"能扛真实一路重试"的工程,就不远了。
把 Saga 压缩成一张 A4 的几行字:步骤带上可逆动作、失败按逆序回补、动作本身幂等、补偿要配可靠重试与人工兜底,这四行就是完整心法。把这四行贴到方案评审白板上,接得住长事务的补偿方案才不是纸上的浪漫,而是能扛住真实网络重试、超时与故障的工程底座——这也是"先放行、错再补"和"一步到位锁死"本质不同、却同样严肃的地方。
Saga 用"先放行、错再补"给高并发长事务留了条活路。可这么多事务同时在一个"多版本"或"可重排"的世界里跑,怎么让读不挡写、写也不互相排队?下一节 5.5 我们用 MVCC 收官本章——多版本并发控制是怎么让并发更丝滑的。