幂等与外部回写日志
L0 · 写出去的每一笔都必须可重复执行而不产生副作用:按业务键 upsert、重复写入拦截可见、at-least-once 语义下的失败重放队列与状态机。
已声明幂等键
9类
覆盖建议/单据/回写/事件/缓存
重复写入拦截
6次
近 24h,全部返回首次结果
外部回写成功率
83.3%
6 笔中 5 笔成功、1 笔失败
重放队列
5条
2 条已恢复、1 条待窗口、1 条需人工、1 条终止
最大重试次数
5次
超过后转人工,不再自动重放
重复开单事故
0起
幂等键 + 接收侧 upsert 双重保障
幂等键约定(按业务键 upsert,X13)
平台内部可以 exactly-once;一旦写外部系统就退回 at-least-once,必须由接收侧按业务键 upsert| 写入对象 | 幂等业务键 | 业务键含义 | 写入语义 | 防重效果 | 状态 |
|---|---|---|---|---|---|
| 决策建议行 | decision_date + scene + skc + store_id + size | 决策日 + 场景 + SKC + 门店 + 尺码 | upsert(同键覆盖 suggest_qty / actual_qty) | 重复导入 Excel 不会产生重复建议行 | 启用 |
| 补货单 | doc_date + scene + from_wh + to_store + skc | 单据日 + 场景 + 调出仓 + 调入门店 + SKC | upsert(同键合并行,重算数量) | 重跑批次不会重复开单 | 启用 |
| 调拨单 | doc_date + from_store + to_store + skc + size | 单据日 + 调出店 + 调入店 + SKC + 尺码 | upsert(同键覆盖,已执行单据拒写) | 已发货单据只允许新增红冲单 | 启用 |
| 调整单 | stocktake_no + skc + store_id + size | 差异单号 + SKC + 门店 + 尺码 | insert-only(业务键唯一约束) | 同一差异行不可重复记账 | 启用 |
| ERP 回写 | sys + doc_type + doc_no + line_no | 系统 + 单据类型 + 单据号 + 行号 | upsert(接收侧保证) | at-least-once 下的重复投递被接收侧幂等吸收 | 启用 |
| Kafka 事件 | topic + partition + offset 或 event_id | 事件 ID(业务主键 + 版本号) | 消费侧去重表(保留 7 天) | 重放不产生二次库存动作 | 启用 |
| WMS 下发 | task_id + skc + size + from_loc | 任务号 + SKC + 尺码 + 源库位 | upsert(任务号唯一) | 断点续跑不会重复下发拣货任务 | 启用 |
| 盘点差异单 | stocktake_no + skc + size | 盘点单号 + SKC + 尺码 | insert-only | 同一次盘点不产生两条差异 | 启用 |
| ATP 缓存扣减 | skc + store_id + size + biz_key | SKC + 门店 + 尺码 + 业务键(单号) | 原子扣减 + 业务键去重表(24h) | 重复扣减被业务键拦截 | 启用 |
共 9 类写入对象 · 幂等键必须落唯一索引,不能只靠应用层判重每页
!核心结论(Databricks 处理保障):写外部系统(ERP 单据 / Kafka / WMS)只保证 at-least-once。这意味着发送方重试、消费位点回退、网络超时都会造成重复投递。必须在接收侧按业务键 upsert,否则重跑一次批次就是重复开一次单。系统侧能做的是:① 业务键设计得足够表达业务唯一性;② 留出拦截记录,让"重复"可见;③ 把不可 upsert 的场景(已执行单据)改为"拒写 + 生成红冲"。
重复写入拦截记录
拦截记录同时也是"幂等键是否设计正确"的验证依据:频繁拦截说明上游在无意义重试| 时间 | 写入对象 | 业务键取值 | 幂等键规则 | 动作 | 触发原因 | 结果 |
|---|---|---|---|---|---|---|
| 2025-10-21 09:41:12 | 调拨单 | TR20251021-003 / 24AW-3714 / M | 决策日+调出店+调入店+SKC+尺码 | 拦截 | 批次重跑第 2 次投递 | 已忽略,返回首次结果 |
| 2025-10-21 09:36:44 | ERP 回写 | ALLOC-20251021-D01 / 行 218 | sys+doc_type+doc_no+line_no | 拦截 | 网络超时后发送方重试 | 已忽略,回执返回首次单号 |
| 2025-10-21 03:12:06 | 补货单 | RC20251021-014 / 24AW-3405 | 单据日+场景+调出仓+调入门店+SKC | 拦截 | 手工重复导入 Excel | 已忽略,页面提示"3 行与现有单据重复" |
| 2025-10-21 02:41:52 | ATP 缓存 | 24AW-3890 / 浙江杭州奥莱店 / F | SKC+门店+尺码+业务键 | 拦截 | CDC 事件重放 | 已忽略,扣减未二次执行 |
| 2025-10-20 22:18:30 | 调整单 | ST20251020-006 / 24AW-3890 / F | 盘点单号+SKC+尺码 | 拦截 | 重复提交记账 | 已忽略,返回首次记账凭证号 |
| 2025-10-20 02:44:19 | Kafka 事件 | inventory.changed / 25SS-1377 | event_id | 拦截 | 消费位点回退导致重放 | 已忽略,去重表命中 |
共 6 条拦截记录 · 拦截 = 返回首次写入结果,不是报错(保证调用方无感知)每页
外部回写投递台账
at-least-once 语义下,"重试成功"是正常路径,不是异常| 投递 ID | 回写目标 | 内容 | 语义 | 重试次数 | 结果 | 最近时间 | 备注 |
|---|---|---|---|---|---|---|---|
| WB-20251021-0218 | ERP 单据回写 | 补货单 ALLOC-20251021-D01(18 行) | at-least-once | 1 | 成功 | 2025-10-21 02:52 | — |
| WB-20251021-0219 | ERP 单据回写 | 调拨单 TR20251021-003(2 行) | at-least-once | 2 | 成功(重试 1 次) | 2025-10-21 02:53 | 首次 504 超时,重试幂等命中 |
| WB-20251021-0220 | WMS 下发 | 拣货任务 PK-20251021-014(6 行) | at-least-once | 1 | 成功 | 2025-10-21 02:54 | — |
| WB-20251021-0221 | Kafka 事件 | inventory.changed × 474 | at-least-once | 1 | 成功 | 2025-10-21 02:55 | — |
| WB-20251021-0222 | ERP 单据回写 | 调整单 ADJ-20251020-006(1 行) | at-least-once | 3 | 失败 | 2025-10-21 02:56 | ERP 会计期间未开账,进入重放队列 |
| WB-20251021-0223 | WMS 下发 | 拣货任务 PK-20251021-015(2 行) | at-least-once | 2 | 成功(重试 1 次) | 2025-10-21 03:02 | 门店库位编码不存在,修正后成功 |
共 6 笔回写 · 5 成功 / 1 失败(已进重放队列)每页
回写失败重放队列
| 队列 ID | 对象 | 已重试 | 失败原因 | 下次重放 | 状态 | 处理说明 |
|---|---|---|---|---|---|---|
| RP-0031 | 调整单 ADJ-20251020-006 → ERP | 3 次 | ERP 会计期间未开账(FI_PERIOD_CLOSED) | 2025-10-21 07:30 | 等待窗口 | 10-22 09:00 会计期间开启后自动重放 |
| RP-0030 | Kafka 事件 inventory.changed(华中 12 条) | 2 次 | Broker 分区不可用 | 2025-10-21 07:00 | 已恢复 | 07:04 重放成功,未产生重复扣减 |
| RP-0029 | 拣货任务 PK-20251021-012 → WMS | 5 次 | 门店库位编码不存在 | 2025-10-21 06:30 | 需人工 | 已修正库位映射,等待人工确认后重放 |
| RP-0028 | 调拨单 TR20251020-011 → ERP | 1 次 | 对方系统 502 | 2025-10-21 03:15 | 已恢复 | 03:17 重放成功 |
| RP-0027 | 补货单 ALLOC-20251019-S01 → ERP | 5 次 | 门店主数据校验失败(已停用门店) | 2025-10-19 05:00 | 已终止 | 挂起单据已作废,不再重放 |
共 5 条 · 指数退避重放,超过 5 次转人工,绝不静默丢弃每页
回写状态机(5 态)
状态流转必须落库可查,禁止"内存里重试"① 待投递 · PENDING
批次内生成回写任务,落库并绑定幂等键;同一幂等键已存在则直接返回历史结果
② 投递中 · SENDING
调用外部接口,超时时间 30s;超时不判定失败,进入待重试(因外部可能已成功)
③ 待重试 · RETRYING
指数退避(1→3→9→27→81 min),每次携带同一幂等键,接收侧自然去重
④ 成功 · SUCCESS
收到外部回执(含外部单号),写回本地并登记 traceId,供跨系统链路追踪
⑤ 失败终止 · FAILED
重试 5 次仍失败或遇不可重试错误(如会计期间未开账、主数据校验失败)→ 转人工,保留全部上下文
重放策略参数
回写成功率趋势(近 14 天)
回写成功率(%) 目标 ≥ 99%(含重试后)
幂等设计自检清单
| 检查项 | 结论 |
|---|---|
| 业务键落唯一索引 | 9 / 9 |
| 写入使用 upsert | 9 / 9 |
| 缓存扣减带业务键去重 | 1 / 1 |
| 事件消费有去重表 | 1 / 1 |
| 已执行单据拒写 | 1 / 1 |
| 重放携带原幂等键 | 1 / 1 |
| 拦截记录可查询 | 1 / 1 |
为什么这是上线硬门槛(G-16)
调研报告把"写入幂等"列为 P0 高优先级缺口,原因是现状"无任何幂等设计"。在"每天凌晨自动算 → 建单 → 审 → 执行"的批量模式下,只要批次失败重跑一次,就会重复开单;重复单据进入 ERP 后必须人工红冲,代价极高。本页把幂等键约定固化到系统层,并让每一次重复投递都在日志里可见。
i互链:下游下发与回执见 下游下发与回执 集成配置见 外部集成与开放 API 跨系统追踪见 跨系统链路追踪