6.4 跨上下文一致性:Saga 与最终一致


6.4 跨上下文一致性:Saga 与最终一致

本节摘要:下单要扣库存、占支付额度,两个动作分属不同上下文,不存在能同时罩住双方的事务。Saga 把一次跨上下文的大动作拆成一串本地事务,每步配一个补偿动作:走到哪算哪,失败就从当前位置往回补偿。本节对比编排式与协同式两条路线,给出青柚商城协同式下单链的完整事件与补偿代码,并把 3.5 埋的"库存拒绝后关单"伏笔收线。

先承认:共同事务不存在

第四块骨头要先承认一个理论事实。交易、库存、支付三个上下文,各自有独立的数据库与事务边界(4.3 立的红线:不共享数据库)。传统思路是两阶段提交让三方同一时刻提交或回滚——但在上下文边界上这条路走不通,也不该走:两阶段提交要求参与方锁住资源等协调者号令,任何一方慢一步,其他方全部挂起。库存上下文的锁一旦被跨域事务牵住,大促的吞吐直接塌方。更根本的是,上下文边界本就是自治权的边界——让库存上下文把提交权交给交易侧的协调者,等于 2.2 辛苦谈下来的主权又让了回去。

正确的思路不是把大事务拆小,而是换掉"全有或全无"这个期望本身。Saga 的核心动作是:把一次跨上下文业务写成一系列本地事务 T1、T2、T3,每个 Ti 配一个补偿动作 Ci——Ci 的效果是业务上抵消 Ti,而不是数据库回滚。 下单链就是:T1 交易创建订单(待支付),T2 库存预留,T3 支付授权。若 T2 失败,C1 关闭订单;若 T3 失败,C2 释放库存、C1 关单。任意时刻系统都处于一个"业务上说得通"的状态——要么进行中,要么已补偿——用户看到的是"下单失败已自动关单",而不是数据库错误页。

两条路线:谁来指挥补偿

Saga 有两种指挥风格。编排式(Orchestration):一个中央协调者(如 OrderSaga 对象)依次下令、接收回执、决定下一步与补偿序列——指挥权集中,流程一目了然,代价是协调者本身是个中心件。协同式(Choreography):没有指挥,每个上下文订阅别人的事件、完成自己的本地事务、再广播新事件——链条靠事件自发接力,耦合最小,代价是流程散落在各个订阅关系里,"整条链长什么样"要拼图才看得见。

维度 编排式 协同式
流程知识在哪 集中在协调者 分散在各订阅方
新增一个参与方 改协调者一处 加一个订阅者
排查整条链 看协调者日志 拼各上下文的事件链
适用规模 步骤多、有复杂分支 步骤少、链路稳定

经验尺子:步骤不超过三四步、分支不多,协同式更轻;一旦出现"根据上游结果决定走哪条分支"的判断逻辑,就升级成编排式——分支逻辑散在协同链里是读代码人的噩梦。青柚的下单链只有三步、无分支,选了协同式;退款链有"原路退、人工退、按期拆退"三种分支,用了编排式。

协同式下单链:事件接力与补偿

代码只看交易侧两端——发出与补偿:

// 交易上下文:订阅库存侧的事实,执行补偿 public class InventoryDeniedHandler { private final OrderRepository orders; @EventHandler public void on(StockDenied e) { // 库存拒绝是事实 不是错误 TradeOrder order = orders.findById(e.orderId()); if (order.status() == TradeStatus.WAIT_STOCK) { order.close(CloseReason.STOCK_DENIED); // C1:业务补偿——关单 orders.save(order); // 关单事件随之广播:通知前端与客服 } // 幂等保护:订单已关则什么都不做——事件可能重复投递 } } // 库存上下文:预留超时自动释放——5.1 那条红色疑问贴的答案 public class StockReservation { private final Instant expiresAt; // 预留自带期限 public StockReserved reserve(SkuId sku, int qty, Duration hold) { // 预留成功即写入到期时间,到期任务扫描释放(C2 的自动触发面) return new StockReserved(sku, qty, expiresAt(hold)); } public StockReleased releaseIfExpired(Instant now) { // C2:释放预留 if (now.isBefore(expiresAt)) throw new NotExpiredException(); return new StockReleased(skuId, quantity); } }

两段代码里埋着 Saga 的三条纪律。第一,补偿是业务动作,不是数据库回滚:关单是订单生命周期里的合法状态,带着"因库存不足关闭"的原因码,客服、报表、用户都看得懂;回滚出来的"从来没有过这张单"反而是谎话。第二,所有事件处理器幂等:消息投递是至少一次语义,StockDenied 到两次,关单逻辑必须第二次无声通过——判状态再动作,是幂等的最简实现。第三,预留要带期限:支付超时的订单,库存不能无限挂着,到期自动释放的 C2 是链条的保险丝——5.1 工作坊那条红色疑问贴,答案就是这行 expiresAt

⚠️ 补偿还有一个少有人提的暗坑叫悬空补偿:补偿执行时,它要抵消的那步其实已悄悄完成——关单指令在路上,用户恰好付了款。防御办法是语义锁:订单进入"关单中"这样的中间状态,支付侧看到中间状态就拒绝新动作,把竞态收敛到一个可判定的窗口里。Saga 的一致性是最终一致,最终一致不等于放任竞态,而是把竞态变成显式设计的状态。

最终一致的沟通成本

技术上收线了,还有一笔账容易漏记:最终一致是产品体验的一部分,要跟业务方提前谈。用户在下单瞬间库存成功扣了,页面却显示"处理中",三秒后才变"下单成功"——这中间的状态窗口必须被产品文案与客服口径覆盖。青柚的做法是把 Saga 链的每个中间态画进 2.1 的词汇表:待预留、待支付、关单中,每个态写清"用户会看到什么、多久会变"。谈判谈的从来不只是技术边界,还有用户感知的边界。

带走的判断

  • 跨上下文没有共同事务,也不该有——两阶段提交牺牲吞吐又侵犯主权;Saga 换期望:一串本地事务加业务补偿。
  • 三步内无分支用协同式,出现分支判断升级编排式;青柚下单走协同、退款走编排,判据是分支复杂度。
  • 三条纪律:补偿是业务动作、处理器必须幂等、预留必须带期限;悬空补偿靠语义锁的中间态收口。
  • 最终一致要进词汇表:每个中间态的用户感知与时效,提前跟业务方谈定,别让客服现场发明口径。

四块硬骨头啃完,全册只剩最后一站:怎么让这一切在组织里活下来——第 7 章路线图与误区清单。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U