库存管理:低库存提醒与补货判断
低库存提醒可以由库存变更事件触发,再由周期扫描兜底。每次用户登录时遍历所有商品,既无法及时发现变化,也会把重复扫描压力放进请求链路。
示例以“仓库 + SKU”为库存对象,可售库存降到 100 以下触发告警,恢复到 120 以上才解除。两个阈值是示例,用来避免库存围绕一个阈值波动时不断开关告警。
从阈值判断推导告警轮次
假设负责人要求“库存低于 100 就提醒补货”。如果每天只查看一次,小规模内部清单可以用周期摘要;如果需要变化后及时提醒,就应接续库存提交事件。
| 候选方案 | 适合什么需求 | 需要处理的代价 |
|---|---|---|
| 每天扫描并发送摘要 | 允许一天级别响应 | 变化不够及时,摘要需覆盖未处置对象 |
| 每次扣减都发送 | 确实要逐笔订阅库存事件 | 低库存持续期间会产生大量重复信息 |
| 维护低库存状态与告警轮次 | 需要提醒进入风险区间及后续处置 | 并发转换、恢复阈值和升级阶段 |
本篇选择第三种:库存变更事实照常保留,通知只在约定状态转换或升级时产生。需要审计每一次瞬时缺货的业务,应在库存事务中记录转换事实,而不是只靠稍后读取当前库存。
库存变化与告警的执行时序
扣减或预留先形成库存事实,消费者再根据当前库存与规则维护告警。消息重复处理不应再次扣减库存。
图中的通知提交属于独立发送阶段,实际实现应在渠道调用前再次校验通知价值与负责人。采购审批和下单不由这次告警转换自动完成。
需求、状态与责任人
低库存系统的目标是让负责补货的人得到可执行的信息,并保留缺口的处理进度。示例中普通消费者不接收库存告警,仓库负责人收到摘要,采购人员处理补货建议。
| 业务问题 | 示例决策 | 实现落点 |
|---|---|---|
| 什么时候算缺货风险 | 可售数量低于该仓库 SKU 的阈值 | 可版本化的规则记录 |
| 已经提醒后是否继续提醒 | 持续未处理时按升级周期再次提醒 | 告警轮次与升级阶段 |
| 规则或负责人改了怎么办 | 重新评估,之后通知新的有效负责人 | 规则变更事件、负责人查询 |
| 已有在途采购是否再采购 | 提醒可保留,采购建议先扣除可确认的在途量 | 建议与采购订单分离 |
| 什么时候算完成 | 告警恢复或明确处置,采购另有闭环 | 不用发送成功替代库存恢复 |
先确定比较哪种库存
| 指标 | 是否直接用于示例提醒 |
|---|---|
| 实物库存 | 不够,还要考虑已被预留的部分 |
| 预留库存 | 表示已占用、尚未完成出库的数量 |
| 可售库存 | 示例中比较的目标,计算规则需统一 |
| 在途采购 | 影响补货判断,但不应直接当成现货可售 |
低库存告警与自动采购是不同动作。自动补货还要结合预测销量、采购提前期、在途数量、最小起订量和审批规则,不能简单地“低于 100 就下同一笔采购单”。
数据模型与接口
| 记录 | 关键字段与约束 |
|---|---|
inventory_balances | 仓库、SKU、实物、预留、库存版本;仓库与 SKU 唯一 |
stock_reservations | 请求键、订单、预留量、状态;重复请求不重复预留 |
inventory_rules | 阈值、恢复阈值、负责人规则、启用状态、版本 |
inventory_alerts | 仓库 SKU、规则身份、当前状态、轮次、最近评估版本、升级阶段与下次升级时间 |
replenishment_suggestions | 告警轮次、建议量、依据、处理状态、关联采购单 |
扣减、预留释放和入库接口各自按业务流水幂等。规则修改接口保存版本并触发重新评估;告警确认接口记录谁接手处理,不把“已确认”当作库存已经恢复。
通知意图与渠道投递复用共用通知约定。意图保存告警轮次和通知阶段;每个接收目标、渠道的提交时间、结果与重试记录保存在投递层,不用告警表的一个“已通知时间”代替所有渠道的进度。
库存更新的事务
下面用预留操作说明条件更新,quantity 必须为正数,库存对象还需经过仓库权限和启用状态校验:
UPDATE inventory_balances
SET reserved = reserved + :quantity,
stock_version = stock_version + 1
WHERE warehouse_id = :warehouse_id
AND sku_id = :sku_id
AND :quantity > 0
AND on_hand - reserved >= :quantity
RETURNING warehouse_id, sku_id,
on_hand - reserved AS available,
stock_version;请求资格确认、预留记录、库存更新和库存事件在同一事务提交。更新不到行时返回库存不足或对象状态错误;不要仍然建立成功预留记录。
on_hand - reserved >= quantity 将可售约束放在实际写入时检查,stock_version 为后续评估标识新的库存事实。事件处理器不再扣减库存,是为了避免投递重试把同一笔预留重复执行。
释放只允许把未释放的预留转换一次,并在同一事务减少 reserved。事件消费者只处理告警,不再次扣减库存。
怎样触发一次告警
扣减、预留释放、入库等操作提交时,在同一事务保存库存变化事件。消费者收到事件后,读取当前库存与告警状态,按状态机判断是否开启新一轮低库存告警。
正常状态,库存 < 100:开启告警轮次,保存通知意图
低库存状态,仍然 < 120:更新观察值,不重复开启告警
低库存状态,库存 >= 120:解除本轮告警
解除后再次 < 100:开启新的告警轮次消费者并发时,需要条件更新或锁保护告警轮次;状态改变与通知意图一起提交。意图键可采用 warehouse_id + sku + rule_id + alert_episode + stage,渠道投递再按意图、渠道与接收目标去重。stage 区分首次低库存、各次升级和恢复,避免同一轮的后续合法通知被首次提醒的去重键拦住。
升级扫描按下次升级时间发现候选,在事务中复核本轮仍低库存、未被处理且已到升级时间,再推进阶段并保存对应意图。重复扫描命中同一阶段的意图;库存恢复则停止尚未提交的旧升级。升级阶段表示业务决定发送什么,实际是否提交成功仍看渠道记录。
事件与通知的持久化衔接可使用 Outbox 模式。
告警处理器的事务
收到库存变化、规则变化事件,或兜底扫描命中对象
BEGIN
按统一顺序锁定库存、规则所需记录与告警状态
初始告警记录不存在时按唯一业务身份创建
读取当前库存与当前规则,拒绝停用对象的旧动作
根据正常或低库存状态决定开启、维持或恢复
若开启:递增 episode,保存本轮通知意图
若维持且达到升级条件:推进阶段,保存本阶段通知意图
若恢复:关闭当前轮次,按规则保存恢复通知
更新最近评估版本
COMMIT
通知 Worker 发送前核对本轮状态与当前负责人库存版本与规则版本都要参与判断。仅记录最近事件 ID,无法发现“库存没变,但阈值提高后已经低库存”的情况。
1000 次扣减怎样避免发出 1000 条提醒
如果 1000 次变化都属于同一个 SKU,消费者可以更新同一告警状态或合并待检查动作,最后只在阈值状态转换时生成通知。合并过程需要记录新版本或待处理标志,避免 Worker 运行期间到来的变化被提前清除。
如果 1000 个不同 SKU 同时低库存,则每个 SKU 保留独立告警记录,再按仓库、负责人和时间窗口汇总成摘要。摘要里保留明细入口,不能只给一个无法追踪的总数。
通知发送按渠道限速;库存变更本身不能等待整批邮件发送完成。
即时事件与扫描的边界
消费者读取最新库存,可以避免迟到的旧事件把新状态覆盖回去。但如果库存短暂跌破阈值后立刻恢复,读取最新状态可能看不到这一瞬间。
若业务关心“当前是否需要补货”,读取当前状态通常符合目标;若需要记录每次缺货事件,就要在库存事务中保存阈值转换事实,不能依赖稍后读取最新值还原历史。
兜底扫描核对“当前低库存却没有活跃告警”的对象,以及“库存已恢复却仍有告警”的对象。它使用与事件消费者相同的状态转换逻辑。
执行补货动作前再次判断
告警发出后,采购人员可能已经人工下单,或其他仓库完成调拨。自动采购任务开始前应检查在途订单和本轮告警是否仍有效,用业务约束阻止同一缺口重复下单。
通知成功只说明提醒流程完成,补货是否完成由采购与入库记录确认,两者应分别监控。
一轮库存波动的记录演变
假设仓库 W2 的 SKU K9 可售库存为 105,告警处于正常状态:
| 变化 | 告警记录 | 通知行为 |
|---|---|---|
| 预留 10,变为 95 | 开启 episode 12 | 建立本轮低库存意图 |
| 取消部分预留,变为 101 | 仍属于 episode 12 | 不因刚超过 100 就重复关闭和开启 |
| 再次扣减,变为 97 | 仍属于 episode 12 | 更新观察值,不新建同阶段通知 |
| 补货后变为 125 | episode 12 恢复 | 按规则建立恢复通知 |
| 后续再次降到 90 | 开启 episode 13 | 可以产生新一轮低库存提醒 |
库存版本标识每次数据变化,episode 标识一轮业务风险,两个计数不能共用。否则每次扣减都像新告警,或者恢复后的真实新风险被历史去重键永久拦住。
采购任务也不能仅凭 episode 自动下单:执行前要核对当前缺口、已有在途与审批结果,把“提醒存在”与“允许采购”分开判断。
验收与排查依据
| 场景 | 应断言的结果 |
|---|---|
| 最后 5 件库存收到两个各预留 4 件的请求 | 最多一个成功,库存约束不被绕过 |
| 同一扣减事件重复 1000 次 | 不重复扣减,不重复开启同轮告警 |
| 库存在 99 与 101 之间波动 | 本轮告警保持,直到达到恢复条件 |
| 阈值从 100 提到 200,库存未变 | 规则事件或扫描触发重新评估 |
| 负责人变更、旧通知已排队 | 尚未发送的记录按当前规则处理 |
| 补货任务启动前已有采购单 | 重新计算缺口,避免同轮重复采购 |
记录库存版本、规则版本、告警轮次与采购关联。监控低库存未处置年龄和通知延迟,不能只统计发送数量。
淘宝开放平台的低库存事件包含什么
淘宝开放平台公开了低库存预警消息,其中包含商品、SKU、仓库、预警阈值与业务唯一键等属性,并说明部分消息接入有授权和白名单限制。这是一份外部事件协议,能够证明低库存风险可以作为业务事件传递,但没有公开其完整阈值计算、告警恢复或采购实现。
对照这个协议,库存提醒必须保留仓库与 SKU 维度;只按商品 ID 聚合会把不同仓库的缺货混在一起。接入外部预警后,本文建议先保存事件身份与来源,再核对当前库存和本地规则。外部业务键可以帮助接收去重,但不应直接当成本文的库存版本或告警轮次。
对于 1000 条预警,可以先按负责人和仓库形成摘要,同时保留每个 SKU 的独立处置记录;负责人点开摘要后仍能定位具体库存对象。接入权限不足时需采用获授权的查询或自身库存事件,不能默认所有商家都能订阅该消息。自动采购仍需单独审核在途量和采购身份,预警消息到达不构成采购已经获准。
阿里云 TCC 接入:超时释放可能先于库存预留到达
阿里云 SOFAStack 的 TCC 防悬挂与幂等接入说明讨论了二阶段回滚可能先于一阶段 Try 到达,以及同一事务的一阶段处理需要幂等的情况。这是框架接入机制;下面将其应用到库存预留场景,是本文的设计推导。
假设订单请求在网络中延迟,到期释放却先到库存服务。如果释放接口因为“查不到预留”就直接成功返回,迟到的预留随后仍可能扣占库存,形成永远等待确认的占用。业务需要为预留动作建立稳定身份,并用终止记录或框架的防悬挂机制阻止迟到请求重新生效。
| 到达顺序 | 需要守住的结果 |
|---|---|
| 预留成功,再收到取消 | 只释放该笔尚有效的预留 |
| 取消先到,预留后到 | 取消身份被保存,迟到预留不能重新占用 |
| 同一预留重复到达 | 不重复减少可售数量 |
| 同一取消重复到达 | 不重复增加可售数量 |
这里的终止记录应覆盖最长允许的迟到与重放窗口。低库存提醒只是观察已确认的库存事实,不能靠再次执行扣减或释放来“修复”告警。若系统只用本地事务,不必为此引入整套 TCC,但仍需检查异步动作是否存在同样的先后竞争。
参考资料
阿里云 SOFAStack 官方接入说明:TCC 防悬挂与幂等接入
淘宝开放平台官方协议:taobao_inventory_LowQuantityWarning:低库存预警消息
AWS Prescriptive Guidance:Transactional outbox pattern
PostgreSQL 18:并发更新与事务隔离
PostgreSQL 18:UPDATE