我在现司的很长一段时间里一直被现场问题工单 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 反而可能引入不必要的复杂度。