这篇文章包含两层内容,先把它们分开。
现有事实:
- 消息会绑定客户端实例、店铺和买家会话;
- 同一店铺与买家的任务按顺序处理;
- 消息使用
mid去重; - 多实例拥有各自的业务上下文;
- 发送后会继续查询数据库确认目标记录;
- 人工接管能够暂停对应业务身份的自动回复。
从这些事实推出的设计扩展:
- 使用
instance_id + shop_id + conversation_id作为完整 queue key; - 为每个 key 建立单消费者 mailbox;
- 使用全局 semaphore 限制总并发;
- 用租约或版本控制人工与旧 worker 的执行权;
- 将
indeterminate与明确失败分开。
后半部分是可以实现和测试的调度模型,当前材料不足以证明它已经完整进入现有系统。
queue key 表达了什么
queue_key = instance_id + shop_id + conversation_id
这不是一种字符串拼接偏好,而是一项并发契约:同一个客户端实例、同一家店铺、同一个会话中的动作必须有序;不同会话可以在总并发上限内并行;身份没有确认时,worker 不得执行发送。
几种更短的键会表达不同错误:
| 队列范围 | 能保证什么 | 会丢掉什么 |
|---|---|---|
| 全局 | 所有任务完全串行 | 一个慢会话阻塞全部店铺 |
| 仅店铺 | 店铺内顺序 | 不同买家互相等待 |
| 仅买家 | 买家维度顺序 | 不同店铺或实例可能被错误合并 |
| 仅消息 ID | 单条任务可独立执行 | 同一会话内的先后顺序 |
| 实例 + 店铺 + 会话 | 业务身份内串行 | 仍需全局并发与恢复控制 |
组合键的作用,是把真正需要串行的业务身份放进同一邮箱。它不自动解决吞吐、恢复或幂等。
一个建议的执行结构是:
dispatcher
→ keyed mailbox
→ single consumer per key
→ global semaphore
→ generator / sender / verifier
这里的 mailbox 与 semaphore 属于设计扩展,不是现有实现事实。
状态决定动作是否可以重做
发送函数没有抛异常,仍然可能停在尚未确认的中间状态。调度层真正需要知道的,是动作是否可能已经发生,以及当前是否允许再做一次。
| 当前状态 | 发送动作是否可能已经发生 | 默认处理 |
|---|---|---|
queued | 否 | 可以重新调度 |
claimed | 否或尚未进入发送 | 先判断执行权是否过期 |
answer_ready | 否 | 可以重新取得发送权限 |
dispatched | 是 | 先查终态,禁止直接重发 |
confirmed | 是,且已有终态证据 | 关闭任务,不再重试 |
manual_hold | 自动化不再拥有发送权 | 阻止新的 dispatch |
transient_failed | 已确认未发送 | 在上限和退避约束下重试 |
permanent_failed | 已确认不可继续 | 转人工或关闭 |
这张表是建议状态模型,不是当前数据库已经采用的 schema。
其中最重要的安全门是:
dispatched + 没有确认
≠ failed
≠ 可以自动重发
进程崩溃、确认查询超时或回调丢失,都可能让系统失去证据,却不代表发送没有发生。此时更安全的顺序是先按消息、会话和时间窗口查询最终记录,再决定是否恢复。
发送何时真正完成,由《自动回复的终点,不是点击“发送”》定义。本文不再重复数据库终态条件,只处理到达终态之前的三个问题:哪些任务必须等待、谁拥有执行权、证据不足时为什么不能自动再做一次。
人工接管是所有权切换
人工接管如果只是界面上的开关,旧 worker 仍可能在已经领取任务后继续发送。更完整的设计需要把 manual_hold 与同一个 queue key 绑定,并为所有权增加版本或租约。
切换后,不同阶段对应不同动作:
- 新消息可以继续被观察和记录;
- 尚未领取的自动发送任务停止推进;
- 已领取但未 dispatch 的任务失去发送权;
- 已 dispatch 的任务继续做终态确认;
- 恢复自动化前,重新确认最新消息位置与会话上下文。
这些规则用于约束未来实现。当前材料只能确认人工接管和业务身份隔离已经存在,不能确认版本租约与上述五步状态转换已经落地。
三组必须执行的故障注入
要把这个设计升级为实现结论,至少需要三组测试:
- **确定性调度:**同一 queue key 永不并行,不同 key 在全局额度内可以并发;
- **崩溃恢复:**分别在
answer_ready、dispatched和confirmed前后终止 worker,确认恢复流程不会越过终态查询直接重发; - **人工接管竞态:**在排队、已领取和发送前切换
manual_hold,确认旧 worker 不能继续取得发送权。
之后才能观察每个 key 的等待时间、热点队列、indeterminate 积压和重试原因。没有这些数据,本篇可以交付的是调度安全模型,不是吞吐提升或故障恢复效果。