WES 重构系列(十一):出库链路中的分布式事务 Saga 实践
我在现司的很长一段时间里一直被现场问题工单 oncall 弄得痛不欲生,大概有半年多,我司很多第三方仓,尤其是海外仓,跨时区工作,经常半夜打我电话,一起床又是一个通宵。坦诚来说,工作这么多年,在我司的前半段时间工作,是我职业生涯中最痛苦的一段时间,捂头哈哈~
这段经历让我切身体会到:当核心履约链路缺乏系统性设计时,稳定性问题最终一定会以“人工成本”的形式被偿还。
后来,在一位在公司深耕十年的前辈建议和引荐下,我转入产品线团队,参与 WES 系统的重构工作,也正是在这一过程中,我开始系统性地审视出库链路,并逐步引入 Saga 思想来解决长期存在的履约一致性问题。
收获良多,我很感谢他。
在仓储系统中,出库履约是一条跨系统、跨资源、长生命周期的业务链路,涉及到状态机、库存模型、调度模型、规则配置等等。本文结合我过往在 WES(Warehouse Execution System)中的工程实践,分享出库链路中落地 Saga 的思考、演进过程与经验总结。
一、仓储业务名词解释
为了便于不同业务领域背景的朋友能理解本文的一些内容,加了一些名词说明。

1. 出库单
出库单本质上是上游下发的单据,比如你在淘宝上买了 2 个牙刷,3 件毛巾。那上游(可能是 OMS、WMS)会下发一个出库单到 WES 来,告知 WES 我现在要出货配送 2 个牙刷、3 件毛巾。出库单就是用来表达要出库的商品信息。
在数据模型上,出库单分为主体和明细。
2. 出库作业单
出库作业单某种层面上是根据出库单做的一层解耦单据,将具体的执行层与上游下发的单据层隔离开。
另一个维度来理解的话,是按库区维度对出库单做了一次拆单。
出库作业单单也有主体和明细。比如说上文的出库单,可能会分裂成两个出库单,A 库区出库 2 件牙刷,B 库区出库 3 件毛巾。
3. 出库任务
出库任务业务含义上表示要从某个存储库位(货架库位、料箱、料格)中出库某个包装批次的商品。
WES 会对这个出库任务做一层转换,加上搬运调度层的含义,比如要把这个商品放到哪个工作站的那个槽位上去拣选,搬运的目的地是哪里。
4.二级库存
库区层面的库存,智能仓分为多个库区,二级库存是库区维度商品批次库存。
5.三级库存
库位维度的库存,实际的存储库存,比二级库存会更细粒度一些。
二、背景:为什么出库履约是一个“长事务”问题
在 WES 中,一次出库并非一个同步完成的动作,而是由多个阶段逐步推进:
-
上游系统下发出库单
-
根据规则拆分出库单生成出库作业单,预占二级库存
-
与算法服务交互,分配工作站槽位
-
与算法服务交互,分配三级库存,生成并下发搬运任务
-
将调度任务下发至下游搬运集群
-
小车到站推送拣选实操
-
实操反馈,扣减二三级库存,完结作业单、任务,小车离站

这一过程具备几个典型特征:
-
涉及多个本地事务
-
业务链路长,状态中间态多
-
允许失败、需要回滚和重试
-
对可用性要求高,不追求强一致
这使得传统的强一致分布式事务(如 XA / 2PC)在该场景下并不适用。
三、Saga 原理回顾(结合我个人的工程视角)
名词说明:Saga 从何而来
Saga 并不是一个首字母缩写词(并不存在官方的“全称”),而是来源于北欧语 saga,原意是长篇史诗、连续发生的一系列故事。
在分布式系统语境下,这个词被用来形容一类非常贴切的事务形态:
由一系列本地事务组成、跨越较长时间、可能失败但可以通过补偿回到业务可接受状态的事务过程。
这一概念最早由 Hector Garcia-Molina 等人在数据库领域提出,后来逐步演进为我们今天所说的 Saga Pattern(Saga 模式)。
从工程角度理解,Saga 更像是:
一段允许中间不一致的业务“叙事过程”
每一步都是**可落库、可观测、可回滚(补偿)**的本地事务
最终目标不是强一致,而是业务上的闭环与可控收敛
这一点,与仓储履约这种“长链路、强现实约束”的业务形态高度契合。
1. 什么是 Saga
Saga 是一种用于解决分布式长事务的问题模型,其核心思想是:
-
将一个全局事务拆分为多个可独立提交的本地事务
-
每个本地事务都对应一个补偿操作
-
通过正向执行与必要时的补偿执行,最终达成一致状态
Saga 的目标不是“永不失败”,而是:
在允许中间不一致的前提下,确保系统最终回到一个业务可接受、可解释、可恢复的状态。
2. 编排型 Saga vs 协同型 Saga
Saga 通常分为两类:
-
协同型(Choreography):各参与方通过事件协作推进
-
编排型(Orchestration):由一个中心节点统一编排流程

在 WES 出库场景中,我们选择的是:
以出库单为业务目标的编排型 Saga
原因很简单:
-
WES 本身就是履约编排中枢
-
下游系统(如 WCS)不具备事务补偿能力
-
需要明确、可控的流程推进与回滚逻辑
四、WES 出库 Saga 的业务建模
1. Saga 的业务边界
-
Saga 实例粒度:出库单
-
Saga 目标:完成一次出库履约(成功或可控失败)
出库单天然就是一个 Saga 的生命周期边界。
2. 核心业务对象
-
出库单(Outbound Order)
-
出库作业单(Work )
-
出库任务(Job)
-
二级库存(L2,逻辑库存)
-
三级库存(L3,具体到库位库存)
-
工作站 / WCS
这些对象的状态变化,共同构成了 Saga 的执行轨迹。
五、Saga 的正向履约流程

几个关键点:
-
Saga 并不是同步完成,而是被定时器持续推进
-
每一步都是独立本地事务
-
任意一步失败,都不会影响已提交步骤的可见性
六、外部事件驱动的 Saga:工作站下线
在真实仓储环境中,资源变化是常态。
1. 典型场景:工作站下线
当某个工作站下线时:
-
已生成但未执行的任务不可继续
-
已预占的三级库存需要释放
-
原有作业规划失效,需要重新规划
2. 我们的处理方式
在 Saga 视角下,从实现上看,这并不是一次“异常处理逻辑”,而是 Saga 在面对外部环境变化时的一次显式补偿与重编排,也是我们在仓储系统中最常见、也最重要的一类 Saga 触发方式。
具体行为:

Saga 在这里体现的是业务韧性。
七、Saga 的工程落地方式

1. 状态机 + 定时器:轻量级 Saga Driver
我们没有引入专门的工作流或 Saga 框架,而是采用:
-
状态机:描述“当前处于哪一步”
-
定时器:负责推进下一步执行
这种方式:
-
实现简单
-
易于调试
-
与业务模型高度贴合
2. 工作站维度的分布式锁
我们在工程上引入了:
以工作站为粒度的分布式锁
使用原则非常明确:
-
锁只用于并发收敛
-
不承担 Saga 一致性语义
典型使用场景:
-
生成任务
-
回滚任务
-
下发任务
-
处理工作站下线事件
并且严格遵循:
锁内逻辑原子化:先提交事务,再释放锁
八、Saga 的可观测性:任务流转树监控
Saga 最大的工程风险,不是失败,而是:
流程卡死却不可见。
一旦 Saga 失去可观测性,工程团队就只能依赖人工介入和经验判断,这往往是系统稳定性持续恶化的开始。
为此,我构建了一套任务流转树监控系统。
能看到什么?
-
一个出库单下:
- 出库明细的信息
- 分裂成了多少作业单
- 每个作业单生成了多少任务
-
每个 Work / Task:
-
当前状态
-
状态的业务含义
-
在当前状态停留的时长
-

这使得 Saga 从“黑盒流程”变成了“白盒履约”。
九、我们踩过的坑 & 为什么没选 TCC
1. 我们踩过的几个坑
-
把异常当成少数情况:在仓储系统中,异常本身就是常态
-
过早追求强一致:导致系统复杂度和耦合度急剧上升
-
缺乏可观测性:问题只能靠日志和人工猜测
2. 为什么没选 TCC
我们认真评估过 TCC,但最终没有采用,原因包括:
-
Try 阶段需要资源强锁定,不适合长时间履约
-
下游系统(如 WCS)不具备 Confirm / Cancel 能力
-
TCC 对接口侵入性极强,演进成本高
相比之下,Saga:
-
更符合“最终一致”的业务现实
-
对下游侵入小
-
更易与现有系统演进融合
十、总结
我们并不是“为了 Saga 而 Saga”,而是在解决 WES 出库问题的过程中,逐步演进出了一套 Saga 实践方案。
-
以出库单为业务目标
-
以状态机 + 定时器驱动
-
支持外部事件补偿
-
强调最终一致性与可观测性
这套方案没有追求概念上的完美,但在真实复杂业务中,稳定、可控、可演进。
这,才是 Saga 在工程实践中的真正价值。
需要强调的是,这套 Saga 实践并不是通用银弹。
它适用于履约链路长、资源频繁变化、允许中间态存在的业务场景;
对于强一致、短事务、高实时性要求的场景,Saga 反而可能引入不必要的复杂度。