MQ(消息队列)可靠性语义、幂等、顺序与重试
知识图谱编号:
2.1.5。本篇是跨产品可靠性治理的唯一完整正文,产品内部确认机制请结合 Kafka(分布式日志消息系统)架构、RocketMQ(分布式消息队列)与 RabbitMQ(消息队列)架构 阅读。
学习目标与面试主线
面试时先说结论:消息可靠性不是一个开关,而是“业务事务产生事件、生产确认、Broker(代理节点)持久化与复制、消费确认、业务幂等、补偿与对账”的组合性质。工程上通常选择 At-least-once(至少一次)传递,用可观察的重复换取不静默丢失,再由唯一约束、状态机和对账把业务结果收敛。任何 Exactly-once(恰好一次)承诺都必须声明作用域,不能覆盖数据库之外的支付渠道、短信、仓库设备等外部副作用。
flowchart LR
A["业务事务"] --> B["事件产生"]
B --> C["生产确认"]
C --> D["Broker(代理节点)落盘与复制"]
D --> E["投递与消费确认"]
E --> F["幂等业务事务"]
F --> G["对账与补偿"]
C -. "结果未知" .-> H["按事件标识查证"]
E -. "重复投递" .-> F
F -. "外部副作用失败" .-> G图中实线表示正常证据链,虚线表示无法靠 MQ(消息队列)自身消除的失败窗口。真正的完成条件是业务不变量通过对账,而不是某个 ACK(确认)返回成功。
1. 可靠性的端到端证据链
可靠投递至少包含五个问题:事件是否与业务事实一同产生、生产者是否知道写入结果、Broker(代理节点)是否把消息放到承诺的持久化边界、消费者是否在业务事务完成后确认、失败后是否能定位并补偿。每层只能证明自己的局部事实。例如发布确认能证明 RabbitMQ(消息队列)接收到了发布结果,却不能证明库存数据库已经更新。
| 证据面 | 可证明 | 不能证明 | 关键记录 |
|---|---|---|---|
| 业务事务 | 订单或支付事实已提交 | 消息已进入队列 | 业务主键、版本号 |
| 生产确认 | Broker(代理节点)按配置接收 | 下游副作用完成 | 事件标识、发送结果 |
| 存储复制 | 消息达到产品承诺边界 | 永不丢失 | 分区或队列、复制状态 |
| 消费确认 | 消费位置可前移 | 重复不会产生副作用 | 消费组、确认位置 |
| 对账补偿 | 业务不变量最终收敛 | 实时强一致 | 差异单、补偿批次 |
sequenceDiagram
participant D as 业务数据库
participant P as Producer(生产者)
participant B as Broker(代理节点)
participant C as Consumer(消费者)
participant R as 接收方数据库
D->>P: 业务事实与事件就绪
P->>B: 发布稳定事件标识
B-->>P: 生产确认
B->>C: 投递事件
C->>R: 去重、状态机与业务事务
R-->>C: 提交结果
C-->>B: 消费确认
Note over D,R: 任一跨边界响应丢失都需要查证、幂等或补偿这张时序图把生产确认和消费确认放到各自边界:前者不能越权证明接收方数据库成功,后者也不能反向证明上游业务事实正确;跨边界的未知结果要靠稳定事件标识贯通。
数据演绎 1:五层证据对齐。 输入订单 O-9001、事件 E-9001-1。10:00:00.010 订单事务提交;10:00:00.018 发送超时;10:00:00.020 Broker(代理节点)其实已落盘;10:00:01.100 消费者写入履约任务并确认。发送日志显示“未知”,代理节点与履约表分别显示“存在”。结论是按 event_id=E-9001-1 查证并幂等补发,不能因客户端超时创建 E-9001-2,否则同一业务事实变成两个语义事件。
热门面试题
问题(基础题):什么叫端到端消息可靠性? 考点:局部确认与业务结果的边界。 回答思路:按事件产生、传输、消费、副作用、补偿分层。 详细答案:端到端可靠性是多层协议共同实现的可验证结果。业务库与事件要避免双写缺口;生产端要保存事件标识和未知结果;Broker(代理节点)要按持久化与复制配置确认;消费端应在本地事务成功后提交 ACK(确认)或 Offset(位移);重复必须由幂等吸收;外部副作用还需查询、对账和补偿。任何一层缺失,都可能出现静默丢失、重复扣款或状态长期悬挂。 进阶追问:Broker(代理节点)返回成功是否等于订单履约成功? 进阶回答:不等于。它只证明消息达到代理节点承诺的边界;履约写库、仓库接口和回执各有独立失败窗口,必须有消费事务、状态机和对账证据。
问题(原理题):为什么不能用一个 ACK(确认)概括可靠性? 考点:确认对象与时间边界。 回答思路:说明生产确认和消费确认回答的是不同问题。 详细答案:生产 ACK(确认)回答“代理节点是否接受发布”,消费 ACK(确认)回答“代理节点是否可以推进投递位置”。两者都不知道业务数据库是否提交,也不知道支付渠道是否真正扣款。即使消费逻辑先提交数据库再确认,确认包丢失仍会重投;若先确认再提交,进程崩溃则会丢业务。因此确认顺序必须和幂等、事务及补偿一起设计。 进阶追问:可靠性验收最小需要哪些指标? 进阶回答:至少要有发送未知数、发布失败数、重复命中数、消费失败数、重试次数、死信数、业务差异数和补偿成功率,并能按事件标识贯通查询。
问题(项目题):库存通知链路怎样证明没有静默丢失? 考点:权威数据源与对账。 回答思路:数据库条件扣减为事实,消息负责传播。 详细答案:WMS(仓储管理系统)库存表用
available >= qty的条件更新完成扣减,同时写 Outbox(发件箱)事件;发布器重试发送,消费者以业务流水唯一键入库并更新下游视图。定时对账比较库存流水、发件箱发布状态和各订阅方版本,发现缺口后按原事件标识补发。这样库存正确性由数据库约束保证,MQ(消息队列)只负责削峰和状态传播。 进阶追问:如果消费者停机两小时怎么办? 进阶回答:保持权威库存写入,暂停非关键订阅或降级展示;恢复后限速追赶,按版本号拒绝旧事件,并对扣减流水与下游版本做差异校验。
2. At-most-once(至多一次)、At-least-once(至少一次)与 Exactly-once(恰好一次)
三种语义描述的是特定边界内的处理次数,不是天然的业务正确性。At-most-once(至多一次)允许丢失但不重投;At-least-once(至少一次)避免静默丢失但可能重复;Exactly-once(恰好一次)通常依赖受控事务域内的原子提交与去重。跨越独立数据库或外部渠道后,只能用幂等和补偿构造“效果上的一次”。
flowchart TD
A["选择语义"] --> B{"能否容忍丢失"}
B -- "能" --> C["At-most-once(至多一次)"]
B -- "不能" --> D{"副作用能否幂等"}
D -- "能" --> E["At-least-once(至少一次)+ 幂等"]
D -- "不能" --> F{"是否处于同一事务域"}
F -- "是" --> G["受限 Exactly-once(恰好一次)"]
F -- "否" --> H["状态机 + 查询 + 人工补偿"]| 语义 | 典型顺序 | 主要风险 | 适用场景 |
|---|---|---|---|
| At-most-once(至多一次) | 先确认再处理 | 崩溃后丢失 | 可丢遥测、可重采样数据 |
| At-least-once(至少一次) | 先处理再确认 | 重复副作用 | 订单、库存、支付通知 |
| Exactly-once(恰好一次) | 原子提交结果和位置 | 事务域受限、成本高 | 单一受控日志与状态存储 |
数据演绎 2:消费顺序对语义的影响。 消息 E-42 在位移 108。方案甲先提交位移 109,随后写库前宕机,重启从 109 开始,E-42 永久遗漏,属于 At-most-once(至多一次)风险。方案乙先写业务流水再提交位移,确认前宕机,重启再次读到 E-42,唯一约束返回“已处理”,业务效果仍为一次。
数据演绎 3:受限的 Exactly-once(恰好一次)。 流处理事务把输入位移 500、输出记录 R-500 和内部状态 S=91 原子提交,在该日志事务域内失败后要么都可见、要么都不可见。但若事务完成后调用第三方短信成功而响应丢失,重试仍可能发送两条短信,说明语义没有自动覆盖外部副作用。
热门面试题
问题(基础题):三种投递语义如何选择? 考点:丢失、重复与成本权衡。 回答思路:先看业务不变量,再看幂等能力。 详细答案:非关键遥测可选择 At-most-once(至多一次)以换取低延迟;订单、库存、支付事件通常采用 At-least-once(至少一次),通过唯一键和状态机消化重复;Exactly-once(恰好一次)只在日志、状态和提交位置受同一事务协议控制时成立。选择依据不是产品宣传,而是丢一条和重复一次分别造成什么损失、能否查证以及补偿成本。 进阶追问:为什么支付也常用 At-least-once(至少一次)? 进阶回答:支付不能接受静默遗漏,而渠道回调天然可能重复。使用渠道流水唯一键、支付状态机和主动查询,可以把重复变成可观察事件,再通过对账保证资金结果。
问题(原理题):Exactly-once(恰好一次)为什么不能无条件端到端? 考点:事务域与外部副作用。 回答思路:构造“事务提交后响应丢失”的反例。 详细答案:精确一次需要把输入消费位置、状态变化和输出原子提交,或者由接收方永久记住请求标识。独立数据库、支付渠道和设备通常不参加同一原子协议。调用成功但响应丢失时,调用方无法区分“未执行”和“已执行”,重试可能重复,不重试可能遗漏。因此只能要求对方提供幂等键或查询接口,再用状态机、对账和补偿收敛。 进阶追问:幂等是否就等于 Exactly-once(恰好一次)? 进阶回答:不等于。幂等允许执行多次但效果相同;Exactly-once(恰好一次)描述观察边界内只提交一次。幂等是实现业务效果一次的重要手段,却不消除重复投递本身。
问题(项目题):IoT(物联网)告警适合哪种语义? 考点:按等级差异化治理。 回答思路:普通抖动与紧急停机告警分层。 详细答案:普通高频测点可聚合、采样,允许少量 At-most-once(至多一次)丢失;设备离线、温度越限和紧急停机应使用 At-least-once(至少一次),以
device_id + alarm_code + window_start为幂等键,窗口内合并次数并保留首末时间。极高等级告警还要旁路通知、超时升级和人工确认,不能仅依赖消息成功。 进阶追问:同一告警反复恢复又触发如何区分? 进阶回答:引入告警实例号或状态版本;从“正常到触发”创建新实例,“触发到恢复”关闭该实例,后续再次触发使用新的实例号,而不是永远按设备和告警码去重。
3. 发送超时、落盘成功与结果未知
生产者看到超时只代表在等待窗口内没有拿到确定响应。消息可能未到达、已到达但未落盘、已按承诺落盘但响应丢失。因为客户端无法区分这三种情况,可靠策略必须保存稳定事件标识,并把重试设计成“同一语义事件再次发送”,而不是生成新标识。
sequenceDiagram
participant P as Producer(生产者)
participant B as Broker(代理节点)
participant S as 持久化存储
P->>B: 发送 E-100
B->>S: 落盘 E-100
S-->>B: 成功
B--xP: 确认响应丢失
Note over P: 超时,结果未知
P->>B: 以同一事件标识重试
B-->>P: 已存在或再次接收| 观察结果 | 真实可能 | 正确动作 | 错误动作 |
|---|---|---|---|
| 明确失败 | 未被接受 | 退避重试 | 立即无限重试 |
| 明确成功 | 达到确认边界 | 记录成功 | 宣称业务完成 |
| 超时未知 | 未写或已写 | 同标识查证、幂等重试 | 生成新事件标识 |
数据演绎 4:发送超时但已落盘。 E-PAY-77 在 12:00:00.000 发出,超时阈值 300ms;Broker(代理节点)于 180ms 落盘,确认包在网络抖动中丢失。生产者 300ms 标记 UNKNOWN,在 800ms + 0~200ms 抖动后以同一标识重发。消费者最终收到两次,但账务表 UNIQUE(event_id) 只插入一次;发送未知指标加一,次日对账无金额差异。
数据演绎 5:超时且未落盘。 E-INV-88 在连接建立前失败,Broker(代理节点)无该事件。生产者仍按同一标识第 2 次发送并成功;若系统错误地把首次超时当成功而不查证,Outbox(发件箱)会长期停在未知状态。结论是未知必须进入可重试状态,并由查询或消费回执闭环。
热门面试题
问题(基础题):发送超时是否意味着消息丢失? 考点:三态结果模型。 回答思路:区分成功、失败和未知。 详细答案:不意味着丢失。超时只说明生产者没有在截止时间前获得响应,消息可能已经落盘且确认包丢失。生产端应记录事件标识、尝试次数、最后错误和下一次重试时间,把状态设为未知或待确认。后续以同一事件标识查证或重发,由消费端幂等吸收可能的重复。 进阶追问:为什么不能换一个事件标识重发? 进阶回答:新标识代表新的语义事实,会绕过去重约束,让同一支付或库存动作被当成两次合法业务执行。
问题(原理题):生产端重试怎样避免重试风暴? 考点:退避、抖动与上限。 回答思路:将错误分为可重试、不可重试和结果未知。 详细答案:参数错误、权限错误应立即失败;网络超时和暂时无可用节点可按指数退避重试,并加入随机抖动,让实例错峰;超过次数进入待补偿而非无限阻塞。重试预算还要限制全局并发和每个业务键的频率,否则 Broker(代理节点)恢复瞬间会被旧请求再次压垮。 进阶追问:重试期间业务接口返回什么? 进阶回答:对已提交的业务事实返回“已受理”及可查询业务号,不把消息确认延迟冒充业务失败;只有业务事务本身未提交时才返回明确失败。
问题(项目题):支付成功事件发送未知如何处理? 考点:资金事实与消息状态分离。 回答思路:支付单和发件箱同事务,发送异步重试。 详细答案:渠道成功回调通过验签后,本地事务将支付单从处理中改为成功,并插入稳定
event_id的 Outbox(发件箱)记录。发布超时只把发件箱标为未知,不回滚已经确认的渠道资金事实。后台发布器查证或重发;下游账务以事件标识和支付单号去重,定时对账比较渠道流水、支付单、账务流水和发件箱状态。 进阶追问:如果渠道回调本身也重复呢? 进阶回答:先以渠道交易号唯一约束去重,再用支付状态机只允许处理中到成功;重复回调返回已有结果,不重复创建发件箱事件。
4. 消费提交、确认丢失与重复投递
消费端最危险的窗口位于“业务事务”和“确认位置”之间。先确认后处理会在宕机时静默丢失;先处理后确认会在确认丢失时重复。因此关键业务通常接受 At-least-once(至少一次),把业务写入和去重记录放进同一本地事务,成功后再确认。
sequenceDiagram
participant B as Broker(代理节点)
participant C as Consumer(消费者)
participant D as 业务数据库
B->>C: 投递 E-200
C->>D: 本地事务:去重 + 履约写入
D-->>C: 提交成功
C--xB: ACK(确认)丢失
B->>C: 再次投递 E-200
C->>D: 唯一键命中,读取已有结果
C-->>B: ACK(确认)成功| 顺序 | 宕机窗口 | 结果 | 治理方式 |
|---|---|---|---|
| 先确认后业务 | 确认后、写库前 | 静默丢失 | 仅用于可丢消息 |
| 先业务后确认 | 写库后、确认前 | 重复投递 | 唯一键与状态机 |
| 业务和位置同事务 | 提交前后原子 | 受限精确一次 | 事务域和吞吐受限 |
数据演绎 6:业务已提交但确认丢失。 轨迹事件 TRACK-9#17 首次消费后把包裹版本从 16 更新到 17,ACK(确认)包丢失。第二次投递时条件 current_version=16 不成立,消费者查询到版本已为 17,记录重复命中并确认。最终只生成一条有效轨迹,重复次数为 1。
热门面试题
问题(基础题):为什么消费成功后还会重复? 考点:业务成功与确认成功不是同一原子操作。 回答思路:指出确认丢失和进程崩溃窗口。 详细答案:消费者可能已经提交数据库,但在发送 ACK(确认)或提交 Offset(位移)前宕机,也可能确认请求到达代理节点但响应丢失。代理节点不能根据业务库推断是否成功,只能再次投递。故重复不是异常偶发,而是 At-least-once(至少一次)的正常组成部分,消费逻辑从设计起就要支持重放。 进阶追问:内存缓存事件标识能否去重? 进阶回答:不能作为正确性保障。实例重启、扩容或缓存淘汰会丢记录;关键去重必须落在共享持久化存储或业务唯一约束中。
问题(原理题):业务事务和消费确认怎样排序? 考点:丢失与重复的取舍。 回答思路:关键业务选择先事务后确认。 详细答案:关键业务先在本地事务中写去重凭证、校验状态并提交业务结果,事务成功后才确认。这样确认前崩溃只会重投,唯一键可吸收重复。若业务事务失败则不确认,进入受控重试。只有可重建、可丢弃的遥测才可能先确认,以降低重复和延迟。 进阶追问:数据库成功但确认持续失败怎么办? 进阶回答:重复投递继续命中去重记录并快速返回;同时告警确认失败率。不能删除去重记录,也不能重复调用不可幂等的外部接口。
问题(项目题):异步导出任务重复领取怎么处理? 考点:任务状态机和租约。 回答思路:消息只触发,任务表才是权威。 详细答案:导出任务有
PENDING、RUNNING、SUCCESS、FAILED状态和租约截止时间。消费者用task_id条件更新PENDING -> RUNNING,受影响行为零说明已被领取;执行结果以分片号唯一约束写入。消费者宕机后租约到期,补偿器可重新投递;重复消息不会并行生成两个最终文件,文件发布也通过版本化路径原子切换。 进阶追问:长任务超过租约如何避免被抢走? 进阶回答:执行器周期续租并记录心跳;续租使用任务版本号,失去租约的旧执行器必须停止提交结果,最终发布以当前租约令牌校验。
5. 幂等键、唯一约束与去重表
幂等键要标识“同一业务意图”,而不是每次请求。优先复用支付单号、订单操作号、库存预占流水、轨迹来源序号;若一个业务对象允许多次同类动作,则组合动作类型和业务版本。数据库唯一约束负责并发裁决,查询后插入的两步写法存在竞争窗口。
flowchart TD
A["收到事件"] --> B{"幂等键是否稳定"}
B -- "否" --> C["拒绝或映射业务意图"]
B -- "是" --> D["尝试插入去重记录"]
D --> E{"唯一约束是否命中"}
E -- "首次" --> F["同事务执行业务"]
E -- "重复" --> G["读取已有结果"]
F --> H["提交后确认"]
G --> H| 方案 | 并发安全 | 能保存结果 | 适用边界 |
|---|---|---|---|
| 内存集合 | 否 | 临时 | 性能优化,不做正确性 |
| Redis(远程字典服务)键 | 需设计原子性与过期 | 有限 | 短期防抖 |
| 数据库唯一键 | 是 | 是 | 关键业务首选 |
| 业务状态条件更新 | 是 | 当前状态 | 有明确状态机的对象 |
| 独立去重表 | 是 | 可扩展 | 多消费者、多副作用 |
数据演绎 7:并发重复消费。 两个消费者同时处理 event_id=E-301。二者都先查询“无记录”,若随后直接扣库存会执行两次;改为先插入 consumer_group + event_id 唯一键,消费者甲插入成功并扣减 5,消费者乙收到唯一冲突并返回已有结果。库存从 100 到 95,而不是 90。
热门面试题
问题(基础题):怎样设计幂等键? 考点:业务意图而非传输次数。 回答思路:选择跨重试稳定、可持久查询的标识。 详细答案:幂等键必须在客户端重试、消息重投和实例切换时保持不变。支付用渠道交易号或商户支付单号,库存用订单行与预占动作号,物流轨迹用承运商、运单号和源序号。不能使用消费时间或每次生成的随机数。若同一订单允许多次退款,应使用退款单号而不是订单号,避免把合法的第二次退款误判为重复。 进阶追问:不同消费者能共用同一去重键吗? 进阶回答:通常要加入消费职责或消费者组维度,因为账务、通知和履约都应各处理一次;只用事件标识会让一个订阅方误阻断另一个订阅方。
问题(原理题):为什么“先查再插”不能保证幂等? 考点:检查与执行之间的竞态。 回答思路:用两个并发事务演绎。 详细答案:两个事务可以同时查询到不存在,然后都执行副作用。只有数据库唯一约束、带版本条件的更新或串行化事务才能在写入点裁决。常见做法是在与业务更新相同的本地事务中插入唯一去重记录,唯一冲突时读取历史结果;这样去重凭证不会在业务回滚后虚假存在。 进阶追问:去重记录先提交、业务后提交有什么问题? 进阶回答:业务失败时去重记录已存在,重试会被误判为成功,造成永久遗漏。因此两者应在同一本地事务,或记录可恢复的处理中状态并由补偿器推进。
问题(项目题):支付退款消费如何实现幂等? 考点:多次合法动作和外部副作用。 回答思路:退款单号唯一、状态机、渠道幂等请求号。 详细答案:每次退款先创建唯一退款单号,消费事件以退款单号和动作类型去重;本地状态从待退款到处理中,调用渠道时把退款单号作为渠道幂等请求号。响应未知则主动查询,不新建退款单;渠道成功后通过条件更新进入成功并写资金流水。订单允许多笔部分退款,因此不能仅用订单号去重。 进阶追问:渠道不支持幂等请求号怎么办? 进阶回答:将调用串行化,响应未知先查询渠道订单;仍无法确定时转人工,不能自动盲重试资金副作用。
6. 状态机、版本号与乐观条件更新
去重只回答“是否见过事件”,状态机还回答“当前能否执行该迁移”。版本号可以拒绝迟到事件,条件更新可以在数据库写入点原子验证前置状态。三者组合比单纯事件去重更适合订单履约、支付和跨境轨迹,因为合法事件可能重复、乱序甚至撤销。
stateDiagram-v2
[*] --> PENDING
PENDING --> PROCESSING: 合法请求
PROCESSING --> SUCCESS: 权威成功回执
PROCESSING --> FAILED: 明确失败
PROCESSING --> UNKNOWN: 响应未知
UNKNOWN --> SUCCESS: 主动查询成功
UNKNOWN --> FAILED: 主动查询失败
SUCCESS --> SUCCESS: 重复成功事件幂等返回
FAILED --> PROCESSING: 新补偿版本| 控制手段 | 防止的问题 | 典型条件 | 局限 |
|---|---|---|---|
| 事件去重 | 同一事件重复 | event_id 唯一 | 不识别不同标识的重复意图 |
| 状态机 | 非法跃迁 | old_state -> new_state | 需定义异常态 |
| 版本号 | 旧事件覆盖新状态 | version = expected | 要处理版本缺口 |
| 条件更新 | 并发竞争 | stock >= qty | 需检查受影响行数 |
数据演绎 8:轨迹乱序。 运单 W-19 当前源版本 41,先收到版本 43=已出库,记录版本 43 并把缺口 42 标为待查;随后版本 42=已拣货 到达,历史轨迹可补录,但聚合状态不允许从已出库倒退为已拣货。若版本 44=取消出库 是合法补偿,则必须通过事件类型和状态迁移表显式允许,不能简单按状态大小比较。
热门面试题
问题(基础题):有去重表为什么还需要状态机? 考点:重复和非法迁移是两个问题。 回答思路:不同事件标识也可能表达冲突动作。 详细答案:去重表只能阻止同一事件再次执行,两个不同事件可能分别要求“支付成功”和“支付关闭”。若关闭事件迟到,仅靠去重会把成功订单错误关闭。状态机通过当前状态和允许迁移表判断动作是否合法;版本号进一步阻止旧事件覆盖新状态。关键更新应写成带前置状态或版本的条件语句,并检查受影响行数。 进阶追问:状态更新受影响行为零如何处理? 进阶回答:读取当前状态判断是重复、乱序还是业务冲突。重复可确认,乱序可延迟或补查,冲突进入异常队列,不能统一当成功吞掉。
问题(原理题):版本号怎样处理跨境轨迹乱序? 考点:聚合状态与历史明细分离。 回答思路:明细可补录,聚合只接受新版本或合法补偿。 详细答案:每个来源为轨迹提供单调序号或可比较时间与事件标识。明细表对来源序号唯一,重复直接忽略;聚合表使用
source_version < incoming_version条件更新。发现版本跳跃时记录缺口并主动拉取,不阻塞更晚的关键状态。撤销类事件不能靠版本倒退表达,而要作为更高版本的显式补偿动作。 进阶追问:上游没有可靠序号怎么办? 进阶回答:组合承运商事件码、发生时间和稳定标识,建立偏序规则;无法判定的冲突保留原始事件并人工核验,不能伪造全序。问题(项目题):库存防超卖中状态机和条件更新各做什么? 考点:业务流程正确性与数量不变量。 回答思路:预占状态机管理生命周期,条件更新守住数量底线。 详细答案:预占单状态从待处理到已预占,再到已确认或已释放;重复确认和重复释放由状态机拒绝。真正扣减使用
available >= qty的条件更新,受影响行为一才算成功,防止并发超卖。消息负责触发履约和超时释放,不能替代库存表约束;对账以库存流水求和验证可用、占用和实物数量关系。 进阶追问:释放消息早于确认消息怎么办? 进阶回答:按预占版本或订单键串行,并用状态机判断;已确认不得被普通超时释放,冲突事件进入补偿核验。
7. Inbox(收件箱)与 Outbox(发件箱)
Outbox(发件箱)解决“业务数据库提交了,但消息没发出”的双写缺口:业务事实与待发送事件写入同一本地事务,再由独立发布器发送。Inbox(收件箱)解决接收方“消息重复与业务提交”的原子性:先以事件标识插入收件记录,再在同一事务执行业务。两者都带来表增长、清理、扫描与状态恢复成本。
flowchart LR
subgraph S["发送服务"]
A["业务表"] --> T["本地事务"]
O["Outbox(发件箱)"] --> T
O --> P["发布器"]
end
P --> B["Broker(代理节点)"]
subgraph R["接收服务"]
B --> I["Inbox(收件箱)"]
I --> U["业务更新"]
end| 模式 | 原子边界 | 核心状态 | 常见故障 |
|---|---|---|---|
| Outbox(发件箱) | 业务写与事件写 | 待发、发送中、已发、未知 | 扫描延迟、重复发布 |
| Inbox(收件箱) | 去重与消费业务写 | 处理中、成功、失败 | 表膨胀、长期处理中 |
| 两者组合 | 服务两端各自本地事务 | 端到端可追踪 | 运维成本增加 |
数据演绎 9:发件箱恢复。 订单事务同时写 O-501 和 E-501,发布器实例甲把记录从待发改为发送中后宕机,租约 30s 到期。实例乙取得租约,以同一 event_id 发送成功,但确认丢失,记录进入未知。消费者已处理后写入消费回执;对账器据此把发件箱收敛为已发。即使没有消费回执,也可同标识重发,由 Inbox(收件箱)吸收重复。
热门面试题
问题(基础题):Outbox(发件箱)解决什么问题? 考点:本地事务与消息发送的双写一致性。 回答思路:先落业务事实和事件,再异步发布。 详细答案:应用无法原子地提交本地数据库并确认远端代理节点。Outbox(发件箱)把待发事件作为本地事务的一部分,只要业务提交,事件就可恢复;发布失败、超时或实例宕机都能扫描重试。它保证事件最终有机会发出,但仍可能重复,因此接收方要幂等,发布端还要监控滞留和未知状态。 进阶追问:发件箱已标记发送成功后消息仍可能丢吗? 进阶回答:取决于代理节点确认和持久化配置;即使传输可靠,下游仍可能失败。因此还要消费监控、业务回执或跨表对账,不能只看发件箱状态。
问题(原理题):Inbox(收件箱)为什么要和业务更新同事务? 考点:去重凭证与业务结果一致。 回答思路:分别构造先后提交的失败窗口。 详细答案:若先提交 Inbox(收件箱)再更新业务,业务失败后重试会被去重记录挡住;若先业务后单独写收件记录,崩溃后会重复执行。放进同一本地事务后,要么二者都提交,要么都回滚。重复事件命中唯一键时读取既有处理结果,并安全确认。 进阶追问:Inbox(收件箱)记录可以多久删除? 进阶回答:至少覆盖代理节点最大保留、最大重试和人工重放窗口;资金类还要考虑审计期限。删除前归档关键字段,避免旧消息重放时失去去重证据。
问题(项目题):跨境履约上下游如何组合两种模式? 考点:服务间最终一致模板。 回答思路:订单侧发件箱、仓储侧收件箱、回执反向事件。 详细答案:订单服务提交出库单时同事务写 Outbox(发件箱);发布器把
shipment_id + version事件送到仓储服务。仓储服务用 Inbox(收件箱)唯一键和出库任务同事务落库,重复只返回已有任务。状态变化再通过仓储发件箱向订单侧回传,双方按版本更新。定时对账比较订单版本、仓储任务和面单轨迹,差异进入补偿队列。 进阶追问:上下游都显示成功但状态不同怎么办? 进阶回答:以预先定义的权威字段和版本规则裁决;保留原始事件、处理日志和回执,生成差异单,不直接互相覆盖。
8. 局部顺序、分区路由与扩容破序
全局顺序意味着所有消息通过单一串行点,吞吐、可用性和扩展性代价很高。工程上通常只要求同一订单、支付单、库存单元或设备的局部顺序:稳定业务键路由到同一分区或队列,在该键范围内串行处理。顺序还会被生产重试、并发消费、分区扩容、延迟重投和外部接口异步完成破坏。
flowchart LR
A1["订单 O1 v1"] --> H["按 order_id 路由"]
A2["订单 O1 v2"] --> H
B1["订单 O2 v1"] --> H
H --> P0["分区 0:O1 v1 -> O1 v2"]
H --> P1["分区 1:O2 v1"]
P0 --> C0["键内串行消费"]
P1 --> C1["并行消费"]| 方案 | 顺序范围 | 吞吐 | 主要风险 |
|---|---|---|---|
| 单队列单消费者 | 近似全局 | 低 | 单点、头阻塞 |
| 按业务键分区 | 键内局部 | 高 | 热键、扩容重映射 |
| 并发消费后版本裁决 | 结果单调 | 更高 | 补偿逻辑复杂 |
| 每键独立队列 | 键内局部 | 运维成本高 | 队列数量爆炸 |
数据演绎 10:分区扩容破序。 原有 8 个分区时 order_id=O-77 映射到分区 3,扩到 12 个分区后简单取模映射到 11。旧分区仍有版本 8 未消费,新分区的版本 9 先到,聚合状态先变为已出库;版本 8 后到时必须由版本条件拒绝倒退。安全迁移可在路由版本切换前排空旧分区,或让新旧路由并行并以业务版本裁决。
热门面试题
问题(基础题):MQ(消息队列)能保证全局顺序吗? 考点:顺序范围与成本。 回答思路:先定义顺序对象,再说明串行瓶颈。 详细答案:单一队列、单一生产序列和单消费者可接近全局顺序,但故障重试、外部副作用和多节点切换仍要处理。多数系统只保证分区或队列内的存储与投递顺序。真正业务需要通常是同一订单或设备内有序,按业务键路由并串行消费即可,不应为无关对象付出全局串行成本。 进阶追问:单消费者为什么还不是绝对端到端顺序? 进阶回答:消费者可能异步调用外部服务,后发请求先完成;失败消息延迟重投时后续消息也可能越过。必须控制业务提交顺序或用版本号裁决。
问题(原理题):重试为什么会破坏顺序? 考点:失败消息与后续消息的调度选择。 回答思路:比较阻塞重试和旁路重试。 详细答案:若版本
v2失败后进入延迟队列,而主队列继续处理v3,则v3会先提交;若原地阻塞重试,则该键甚至整个分区发生头阻塞。常见折中是按业务键暂停、把该键事件转入有序重试通道,其他键继续;同时以版本号拒绝倒退,并在超限后进入人工补偿。 进阶追问:怎样避免一个毒消息阻塞整个分区? 进阶回答:识别业务键后隔离该键,将其事件序列转入专属重试流;其他键恢复消费。隔离前要保存位移和版本,避免同键事件越过。问题(项目题):物流轨迹如何兼顾吞吐和顺序? 考点:运单键路由、版本与缺口补查。 回答思路:不同运单并行,同一运单按源序号处理。 详细答案:用运单号作为路由键,让同一运单进入同一分区;消费者在运单维度串行,明细表对来源序号唯一,聚合状态按版本条件更新。发生跳号时不必阻塞整个分区,可记录缺口并调用承运商查询;迟到旧版本只补历史明细,不回退聚合状态。热点大客户可加入稳定子键,但必须维持单运单映射。 进阶追问:分区扩容时怎么迁移? 进阶回答:引入路由版本,先停止或双写旧路由、排空旧分区,再切换;迁移窗口依赖版本条件兜底并做逐运单差异校验。
9. 重试分类、指数退避与随机抖动
重试只对暂时性错误有价值。参数非法、签名失败、状态冲突属于不可重试;超时、限流、临时网络故障可重试;响应未知则先查证。指数退避降低持续压力,Jitter(随机抖动)避免大量实例在同一时刻醒来,最大次数和总时长保证故障不会无限占用资源。
flowchart TD
A["处理失败"] --> B{"失败分类"}
B -- "永久错误" --> C["死信或人工修复"]
B -- "结果未知" --> D["查询权威状态"]
B -- "暂时错误" --> E["指数退避 + Jitter(随机抖动)"]
D --> F{"已成功"}
F -- "是" --> G["幂等收敛"]
F -- "否或未知" --> E
E --> H{"超过预算"}
H -- "否" --> A
H -- "是" --> C| 策略 | 示例 | 优点 | 风险 |
|---|---|---|---|
| 固定间隔 | 每 5s | 简单 | 同步重试、持续施压 |
| 指数退避 | 1s,2s,4s,8s | 快速降压 | 恢复响应变慢 |
| 全抖动 | 0~base*2^n | 实例错峰 | 延迟不稳定 |
| 有上限退避 | 最大 5min | 控制最坏等待 | 需死信与补偿 |
数据演绎 11:重试风暴。 10,000 条失败消息固定每秒重试一次,会额外产生 10,000 msg/s,而下游恢复能力仅 2,000 msg/s,永远无法追平。改为 1s、2s、4s、8s、16s 指数退避并加全抖动,设置每租户 200 msg/s 上限;第 5 次仍失败转死信。恢复后按 1,500 msg/s 受控重放,给实时流保留 500 msg/s 容量。
热门面试题
问题(基础题):哪些错误不应该重试? 考点:错误分类。 回答思路:永久错误、业务冲突和未知结果分别处理。 详细答案:参数格式错误、验签失败、权限拒绝、目标不存在、状态机非法迁移通常不会随时间自愈,应直接进入修复或死信。超时与限流可能暂时恢复,但响应未知要先查询,尤其是扣款、退款和面单购买。盲目重试永久错误只会放大流量并掩盖数据质量问题。 进阶追问:数据库死锁可重试吗? 进阶回答:可以在短退避后有限重试,但要保持同一幂等键,并调查锁顺序和事务范围;不能把长期热点用无限重试掩盖。
问题(原理题):为什么既要指数退避又要随机抖动? 考点:降压与去同步。 回答思路:说明单纯退避仍可能同频。 详细答案:指数退避让单个请求的重试频率逐步下降,但同一故障时刻失败的成千实例仍会在
1s、2s、4s同时醒来,形成周期尖峰。随机抖动把每轮重试分散在时间窗口内。再配合全局并发、租户配额和最大总时长,才能避免重试流量挤占正常消息。 进阶追问:恢复后是否立即放开重试? 进阶回答:不应。先小流量探测,再按下游错误率和延迟逐级提升;实时流与补偿流使用独立配额,防止旧积压再次压垮服务。问题(项目题):第三方面单接口限流如何重试? 考点:外部依赖保护和结果查证。 回答思路:租户与承运商双重限速,未知先查单。 详细答案:按承运商和客户建立令牌配额,收到限流响应后读取建议等待时间或指数退避;超时先用业务请求号查询面单是否已创建,确认不存在才重试。超过预算转人工或延迟任务,不阻塞其他承运商。面单号落库用唯一约束,观测每个渠道的成功率、未知数和重试放大倍数。 进阶追问:一个承运商故障会不会拖垮全部订单? 进阶回答:队列、线程池和重试预算按承运商隔离;故障渠道熔断并降级为待处理,其他渠道继续消费。
10. 死信队列、毒消息隔离与人工重放
DLQ(死信队列)保存超过重试预算、明确不可处理或过期的消息证据。死信不是垃圾桶,更不是处理成功。重放前要检查原始负载、失败原因、幂等证据、当前业务版本、消息时效和外部副作用;修复程序后应使用新重放批次记录操作者、范围、速率和结果。
flowchart LR
A["失败消息"] --> B["有限重试"]
B --> C{"达到上限"}
C -- "否" --> A
C -- "是" --> D["DLQ(死信队列)"]
D --> E["原因修复与影响评估"]
E --> F["幂等/版本/时效预检"]
F --> G["小批量重放"]
G --> H["对账验证"]
H -. "仍失败" .-> D| 重放检查 | 要回答的问题 | 不满足时动作 |
|---|---|---|
| 幂等 | 重复副作用是否可吸收 | 先补唯一键或人工核验 |
| 版本 | 旧消息会否覆盖新状态 | 转换或丢弃并留痕 |
| 时效 | 业务动作是否已过期 | 标记过期,不执行 |
| 容量 | 下游能否承受 | 限速、分批、暂停 |
| 审计 | 能否追踪操作者和结果 | 建立重放批次 |
数据演绎 12:死信重放风险。 2,400 条库存释放消息因字段兼容失败进入死信,停留 6h。其中 1,900 个预占仍是待释放,400 个已被新版本确认,100 个已人工释放。直接重放会错误释放 400 个已确认库存。正确流程先按预占号查询状态,只重放 1,900 条,速率限制为 100 msg/s,19s 完成;其余 500 条记为跳过并生成审计清单。
热门面试题
问题(基础题):死信队列有什么作用? 考点:隔离、证据和恢复入口。 回答思路:说明死信不是删除或成功。 详细答案:死信队列把持续失败消息从实时消费路径隔离,避免毒消息无限阻塞或形成重试风暴,同时保存原始负载、错误类型、尝试次数和时间,供修复后重放。死信增长必须告警并有责任人;业务上仍是未完成状态,需要补偿或明确过期,不能因为消息进入死信就提交业务成功。 进阶追问:死信可以自动无限重放吗? 进阶回答:不可以。必须先确认根因已消除,并限制批次与速率;否则会在主队列和死信队列之间循环,扩大事故。
问题(原理题):人工重放前为什么要检查版本和时效? 考点:历史意图可能已经失效。 回答思路:用库存释放或订单取消反例。 详细答案:死信停留期间业务可能继续演进。六小时前的释放事件在订单已确认后再执行,会破坏库存;过期通知再发送会误导客户。重放器应查询当前状态,验证期望版本和允许迁移,把消息分类为可重放、已完成、已过期和需人工。所有跳过也要保留原因,而不是静默删除。 进阶追问:重放是否沿用原事件标识? 进阶回答:原业务意图未变时沿用原标识,并增加独立的重放批次号用于审计;若是修正后的新业务动作,应生成新事件且显式关联原事件。
问题(项目题):支付死信如何处理? 考点:资金安全、查询优先和人工闸门。 回答思路:先对账事实,再决定补记或重调渠道。 详细答案:支付死信先按支付单和渠道流水查询渠道、本地支付单、账务流水与订单状态。渠道已成功但本地账务缺失时,重放的是内部记账事件;不能再次发起扣款。渠道结果未知时先主动查询,仍未知则人工处理。重放前校验金额、币种、商户和版本,采用小批量双人复核,并在完成后核对总金额和笔数。 进阶追问:如何防止运维误点全量重放? 进阶回答:权限分级、审批、影响预估、最大批次和速率硬限制;资金类要求预演查询结果与双人确认,所有操作写不可篡改审计日志。
11. 事务消息、Outbox(发件箱)与对账补偿
RocketMQ(分布式消息队列)事务消息通过 Half Message(半消息)、本地事务结果和事务回查协调“本地事务与消息是否可见”;Outbox(发件箱)通过同库事务和扫描发布完成相同方向的最终一致。前者减少自建发布扫描,依赖代理节点回查协议;后者产品中立、可查询性强,但增加表和发布器。二者都不保证下游数据库或第三方渠道与上游一起原子提交。
flowchart TD
A["本地业务与事件一致"] --> B{"选择协调方式"}
B --> C["事务消息"]
C --> C1["Half Message(半消息)"]
C1 --> C2["本地事务"]
C2 --> C3["提交/回滚/回查"]
B --> D["Outbox(发件箱)"]
D --> D1["业务 + 事件同事务"]
D1 --> D2["扫描/日志捕获发布"]
C3 --> E["下游幂等"]
D2 --> E
E --> F["对账补偿"]| 方案 | 优点 | 代价 | 适用条件 |
|---|---|---|---|
| 事务消息 | 协议内协调、低扫描延迟 | 产品绑定、需处理回查 | 客户端和代理节点支持 |
| Outbox(发件箱) | 产品中立、审计直观 | 扫描与清理成本 | 业务库可同事务写事件 |
| 定时对账补偿 | 能覆盖长期和外部差异 | 延迟高、逻辑复杂 | 所有关键链路兜底 |
数据演绎 13:支付事件最终收敛。 支付单 P-600、事件 E-600、金额 USD 120。本地事务成功,但事务消息二次提交请求丢失;Broker(代理节点)回查支付单发现成功,提交消息。账务消费者写入流水后确认丢失,第二次消费命中 UNIQUE(event_id)。订单消费者临时失败三次后成功。日终对账比较渠道 120、支付单 120、账务贷记 120 和订单已支付,四方一致。
热门面试题
问题(基础题):事务消息与 Outbox(发件箱)有什么区别? 考点:协调协议、产品依赖和运维成本。 回答思路:先说共同目标,再比较实现。 详细答案:二者都解决本地业务事务与事件发布的最终一致。事务消息先写不可消费的半消息,再执行本地事务并提交或回滚,状态未知时由代理节点回查;Outbox(发件箱)把事件与业务同事务落库,由发布器扫描或捕获日志发送。事务消息协议集成更紧,Outbox(发件箱)更产品中立、审计直观。二者下游都必须幂等。 进阶追问:有事务消息还需要对账吗? 进阶回答:需要。回查程序可能故障,下游可能长期失败,外部资金事实也不在消息事务域内;对账负责发现协议之外的差异。
问题(原理题):事务回查方法应该怎么写? 考点:以本地权威状态裁决,避免再次执行业务。 回答思路:查询稳定业务键并返回确定或未知。 详细答案:回查根据事务标识查询本地业务表或事务日志:已成功则提交消息,明确失败则回滚,事务仍进行中或证据不足则返回未知等待后续回查。回查不能再次扣库存、创建订单或调用支付,因为它可能执行多次。业务事务开始前要持久化可查询标识,避免回查时找不到上下文。 进阶追问:长期返回未知怎么办? 进阶回答:超过回查预算后告警并进入人工或对账补偿;不能永久占用半消息,也不能无证据地猜测提交。
问题(项目题):支付成功后如何同步订单、账务和通知? 考点:事实源、订阅者幂等和差异补偿。 回答思路:支付单成功与事件同事务,下游独立消费。 详细答案:渠道回调验签后,以渠道流水唯一键更新支付单成功,并通过事务消息或 Outbox(发件箱)产生
PaymentSucceeded事件。账务按事件标识生成不可重复流水,订单按支付单号和状态版本转已支付,通知按通知任务号去重。任一订阅失败不回滚资金事实,而是重试、死信和补偿;日终按渠道、支付、账务和订单四方对账。 进阶追问:订单取消与支付成功并发怎么办? 进阶回答:状态机裁决:已支付不能直接关闭;若取消先成功而支付后到,则创建退款补偿流程,不能简单覆盖状态或丢弃资金事件。
12. 可观测性、对账闭环与项目统一模板
可靠性设计必须能回答某个事件现在在哪里、尝试了几次、业务副作用是否完成、差异由谁处理。统一模板包括:权威数据源、稳定事件标识、状态机、发送记录、消费记录、重试与死信、补偿任务、业务不变量和告警阈值。链路标识便于排障,但不能替代业务幂等键。
flowchart LR
A["事件台账"] --> B["发送指标"]
A --> C["消费指标"]
A --> D["业务状态"]
B --> E["差异检测"]
C --> E
D --> E
E --> F["自动补偿"]
E --> G["人工工单"]
F --> H["业务不变量复核"]
G --> HsequenceDiagram
participant J as 对账任务
participant P as 支付权威表
participant L as 账务流水
participant O as 订单状态
J->>P: 拉取批次 20260714
J->>L: 按支付单匹配金额
J->>O: 匹配履约状态
J->>J: 分类缺账、缺状态、金额冲突
J-->>P: 生成补偿或人工差异单| 场景 | 权威数据 | 幂等键 | 不变量 | 补偿 |
|---|---|---|---|---|
| 库存 | 库存流水与库存表 | 预占动作号 | 可用量不为负 | 重发、释放核验 |
| 支付 | 渠道流水与支付单 | 渠道交易号 | 金额、币种、方向一致 | 补账、退款人工化 |
| 物流 | 承运商源事件 | 运单号与源序号 | 聚合版本不倒退 | 缺口拉取 |
| 异步任务 | 任务表 | 任务号与分片号 | 一个版本一个最终产物 | 租约恢复 |
数据演绎 14:统一告警阈值。 实时生产 5,000 msg/s,正常重复命中率 0.03%、死信每分钟小于 5。发布故障后未知结果升到 300/s,重试使入口增至 7,500 msg/s,消费失败率 8%。系统先关闭非关键通知重试,把补偿流限制为 500 msg/s,保护支付与库存主链路;故障修复后按事件台账补发 18,000 条,最终去重 17,940 条、首次处理 60 条,对账差异为零。
数据演绎 15:跨境轨迹差异闭环。 对账批次包含 100,000 个运单,发现 620 个来源版本大于本地版本,其中 580 个可自动拉取补齐,30 个因承运商限流进入次日重试,10 个源状态冲突转人工。补齐后聚合版本全部单调,人工单保留原始报文与裁决结论;成功率不能只报 99.99%,还要报告未闭环的 40 个差异。
热门面试题
问题(基础题):消息可靠性应该监控哪些指标? 考点:技术指标和业务指标联动。 回答思路:按生产、存储、消费、重试、业务差异分层。 详细答案:生产侧看成功、失败、超时未知和重试放大;代理节点看不可用、复制、磁盘与堆积;消费侧看处理延迟、失败、重复命中、确认失败和在途数量;恢复侧看重试、死信、重放批次;业务侧看库存负数、支付四方差异、轨迹版本缺口和任务悬挂。所有指标都应能按事件标识下钻。 进阶追问:为什么只看消费积压不够? 进阶回答:没有积压也可能因提前确认造成静默丢失,或消费成功但业务写入错误;必须用业务不变量和对账发现传输指标看不到的问题。
问题(原理题):链路标识能当幂等键吗? 考点:诊断关联与业务身份的区别。 回答思路:重试可能换链路,业务意图仍相同。 详细答案:链路标识用于串联一次调用链,网关重试、定时补偿和人工重放可能产生新的链路标识;同一链路也可能包含多个业务动作。幂等键必须稳定标识支付、预占、轨迹等业务意图。应同时记录事件标识、业务键和链路标识,分别服务正确性、路由与诊断。 进阶追问:日志里最少记录什么? 进阶回答:事件标识、业务键、消息位置、消费者职责、尝试次数、状态版本、结果分类、耗时和错误码;资金类还记录金额与币种但应脱敏。
问题(项目题):如何用三分钟讲清一次消息可靠性事故? 考点:影响、证据、止血、根因和长期修复。 回答思路:用可量化事实和业务不变量收尾。 详细答案:先说影响范围和权威数据是否受损,再给时间线:何时超时、积压或重复上升;随后说明通过事件标识对齐发送、代理节点、消费和业务流水的证据。止血包括限重试、隔离毒消息和保护关键消费;根因要落到确认顺序、幂等缺失或容量错误;长期修复加入唯一约束、退避、死信重放审批和对账。最后用补发量、去重数和差异归零验证恢复。 进阶追问:怎样证明修复没有制造二次事故? 进阶回答:先小批量重放,限制速率,实时观察下游延迟与业务不变量;全量后核对事件总数、唯一流水、金额或库存差异,并保留回滚开关。
题库前非知识型过渡
下面的题目用于口述训练,不重复承担上文知识小节的六字段问答职责。回答时先说结论,再说明确认边界、失败窗口、业务幂等和最终对账;不要把任何单一产品确认误说成资金、库存或履约已完成。
13. 模块综合题库
13.1 三至五分钟口述练习
- 问题(综合题):请完整解释什么是端到端消息可靠性
口述答案:我的结论是,端到端可靠性不是“MQ(消息队列)不丢消息”这一句,而是一条可核验的证据链。第一段是业务事实与事件产生:订单、支付或库存事务提交时,必须通过 Outbox(发件箱)或事务消息避免“数据库成功、事件没有留下”的双写缺口。第二段是生产:保存稳定事件标识,把返回分成明确成功、明确失败和超时未知;未知只能用同一标识查证或重试。第三段是 Broker(代理节点):确认只代表消息达到当前产品和配置约定的落盘、复制或路由边界,不代表消费者业务完成。第四段是消费:关键业务通常先在本地事务中插入去重记录、校验状态机并更新业务,提交后再发送 ACK(确认)或推进 Offset(位移),于是崩溃更可能产生可观察的重复,而不是静默丢失。第五段是业务闭环:外部支付、仓库接口和通知不在消息事务域内,必须有幂等请求号、主动查询、死信、人工介入和定时对账。项目中我会用事件标识贯通发送日志、消息位置、消费记录与业务流水,并监控未知发送、重复命中、死信、差异单和补偿成功率。最终验收看金额、库存、轨迹版本等业务不变量是否一致,而不是只看队列没有积压。
- 关联机制:可靠性端到端证据链
- 追问 1:生产确认成功能否删除业务事件记录?回答:不能立即删除,应至少保留到消费或对账窗口结束,并按审计期限归档。
- 追问 2:为什么重复比丢失更可控?回答:重复有稳定标识时可由唯一约束吸收,静默丢失若没有事件台账通常无法发现。
- 追问 3:最重要的业务指标是什么?回答:按场景选择支付四方差异、库存负数与流水差异、轨迹版本缺口等不变量指标。
- 问题(综合题):如何选择三种消息投递语义
口述答案:我不会先看产品宣传,而是先问“丢一条和重复一次分别造成什么损失”。At-most-once(至多一次)通常先推进消费位置再处理,进程在两者之间崩溃会丢消息,因此只适合可采样、可重建、允许缺口的普通遥测。At-least-once(至少一次)先完成业务再确认,确认丢失会重投,适合订单、库存、支付通知等不能静默遗漏的场景;前提是接收方有数据库唯一约束、业务状态机、版本条件和对账。Exactly-once(恰好一次)需要把输入位置、状态变化和输出纳入同一受控事务域,能在流处理日志内部成立,但跨独立数据库、第三方支付、短信或仓库设备后就存在“执行成功但响应丢失”的不确定窗口。此时工程目标应表述为“至少一次传递,加幂等实现效果一次,加对账实现最终收敛”。例如支付回调可能重复五次,我用渠道交易号唯一约束只更新一次支付单,再通过 Outbox(发件箱)传播;账务和订单各按自己的消费职责去重。这样既不依赖脆弱的全局事务,也不把可能的重复隐藏起来。面试中我还会说明语义是分层的:代理节点投递一次,不等于业务副作用一次,更不等于资金绝对正确。
- 关联机制:三种语义与业务边界
- 追问 1:日志处理中的 Exactly-once(恰好一次)有价值吗?回答:有,在受控日志与状态存储内可减少重复结果,但不能外推到外部副作用。
- 追问 2:通知短信可以用 At-most-once(至多一次)吗?回答:普通营销短信可以按业务容忍度选择,支付结果通知通常仍需可查、可补且接收方幂等。
- 追问 3:语义选择后还要压测什么?回答:要注入超时、确认丢失、重启和重复投递,验证不变量、重放能力与恢复时间。
- 问题(综合题):请给出端到端 Exactly-once(恰好一次)不成立的反例
口述答案:最清楚的反例是退款。消费者收到退款事件,在本地事务中把退款单改为处理中,并向第三方渠道发起退款。渠道已经把 100 元退回,但响应包在网络中丢失。消费者此时只能看到超时,无法区分“渠道未执行”和“渠道已执行”。如果直接重试,而渠道不支持幂等请求号,就可能再退 100 元;如果不重试,又可能留下真实未退款。即使 MQ(消息队列)能保证消息只投递一次,也没有消除这次远程调用的结果未知;反过来,即使本地消费位置和数据库状态能原子提交,外部渠道仍不在同一事务域。正确设计是给退款单稳定业务号,渠道支持时将其作为幂等请求号;超时后先按该号主动查询,明确未执行才重试,仍无法判断则转人工。内部账务还要以退款单号唯一记账,日终比较渠道退款流水、本地退款单、账务借记和订单退款金额。这个案例说明 Exactly-once(恰好一次)必须附带作用域:可以说某个日志事务内只提交一次,不能说跨任意系统的现实副作用天然只发生一次。工程上追求的是重复可识别、未知可查询、差异可补偿和结果可审计。
- 关联机制:语义边界与支付反例
- 追问 1:两阶段提交能否彻底解决?回答:只有参与者都支持同一协议且接受可用性和阻塞成本时才可能,第三方渠道通常不参加。
- 追问 2:幂等请求号由谁生成?回答:由发起业务意图的一方生成并持久化,所有重试复用同一值。
- 追问 3:渠道查询也超时怎么办?回答:保持未知状态,限制自动动作并进入后续查询或人工核验,不能猜测成功或失败。
- 问题(综合题):发送超时但消息已落盘时如何处理
口述答案:发送超时必须建模为“结果未知”,不能直接当失败。消息从生产者到 Broker(代理节点)至少有三种真实状态:请求没到、到达但未达到确认边界、已经落盘或复制但确认响应丢失。生产者仅凭超时无法判别。我的实现会在业务事务中先保存稳定 event_id,发布器每次尝试都记录节点、时间、错误和发送状态;超时后把记录设为未知,按指数退避与 Jitter(随机抖动)再次发送同一语义事件。不能生成新事件标识,否则消费端会把同一订单变化当作两次合法事实。消费者必须在共享持久化存储中按“消费职责加事件标识”建立唯一约束,即使收到两份也只执行一次。若产品提供可用的生产幂等或事务能力,可以减少传输层重复,但业务幂等仍保留,因为应用重启、跨主题补发和人工重放仍可能制造重复。以支付成功事件为例,本地支付单已经成功,发送超时不应回滚资金事实或给前端返回“支付失败”;接口返回已受理并提供支付单查询,后台继续查证和发布。最后通过发件箱滞留、发送未知数、消费回执和支付四方对账确认事件已收敛。
- 关联机制:生产确认与结果未知
- 追问 1:可以无限重试吗?回答:不能,要有次数、总时长、并发和租户预算,超限进入补偿队列。
- 追问 2:明确参数错误怎么处理?回答:归为不可重试,直接修复数据或配置,避免用重试掩盖问题。
- 追问 3:前端为什么不返回失败?回答:业务事实已提交,消息发送是内部传播状态;返回失败会诱导用户再次创建业务意图。
- 问题(综合题):消费业务提交但 ACK(确认)丢失时如何保证正确
口述答案:这是 At-least-once(至少一次)最典型的重复窗口。消费者收到事件后已经提交了业务数据库,但 ACK(确认)或 Offset(位移)提交请求丢失,Broker(代理节点)无法读取业务库,只能再次投递。正确顺序不是试图消灭重投,而是让重投安全:在同一个本地事务中先插入消费去重记录,再校验状态机、版本和业务约束并写入结果,事务成功后才确认。去重表以 consumer_role + event_id 建唯一键,因为账务、履约和通知都应各处理一次,不能互相抢占同一个全局去重记录。第二次消费若唯一键冲突,要读取既有处理结果并再次确认,不应抛出异常形成永久重试。若首次业务事务回滚,去重记录也必须回滚,否则后续会被错误挡住。对于外部接口,不能在数据库事务里简单调用后再假设可回滚;应先落本地任务状态,调用时传稳定幂等号,结果未知则查询。项目验收会主动在“业务提交后、确认前”杀掉进程,观察同一事件重投、重复命中指标增加,而库存、账务或任务产物仍只变化一次。这个故障注入比只看正常消费成功率更能证明设计成立。
- 关联机制:消费提交与重复投递
- 追问 1:先确认再写库有什么后果?回答:确认后宕机会让代理节点推进位置,业务永远没有执行,形成静默丢失。
- 追问 2:唯一冲突应该返回失败吗?回答:若查询确认既有结果正确,应视为幂等成功并确认;业务冲突则另行分类。
- 追问 3:去重表故障怎么办?回答:暂停关键消费或降级到安全的串行路径,不能跳过去重直接执行资金副作用。
- 问题(综合题):怎样为不同业务设计正确的幂等键
口述答案:幂等键标识的是一次稳定的业务意图,而不是一次网络请求或消息投递。设计时我先问“哪些动作允许合法发生多次”。支付成功可用渠道交易号或商户支付单号;一次订单可有多笔部分退款,因此退款必须用退款单号,不能只用订单号;库存预占可用订单行号加预占动作号,确认和释放还要加入动作类型;跨境轨迹使用承运商、运单号和来源序号;异步导出使用任务号和分片号。键在客户端重试、服务重启、消息重投和人工重放时都必须不变,不能使用当前时间或每次新生成的随机数。数据库层以消费职责和幂等键建立唯一约束,在与业务更新相同的本地事务中插入,避免“先查再写”的并发竞态。去重记录不仅保存已见标记,还可保存处理状态、结果摘要、业务版本和错误,重复到达时可以返回一致结果。对于有时效的普通防抖,可用 Redis(远程字典服务)提高速度,但资金和库存正确性不能只依赖会过期的缓存键。最后要定义保留期,至少覆盖消息保留、自动重试和人工重放窗口;资金审计记录往往需要更久。
- 关联机制:幂等键与唯一约束
- 追问 1:为什么幂等键要加消费职责?回答:同一事件的账务、履约、通知应分别成功一次,不能让一个订阅方阻断另一个。
- 追问 2:幂等记录可以只存布尔值吗?回答:简单场景可以,复杂场景应保存状态和结果,便于处理中恢复、重复响应与审计。
- 追问 3:上游不给稳定标识怎么办?回答:在接入层按业务字段生成并持久化映射,无法可靠映射时拒绝高风险自动执行。
- 问题(综合题):为什么数据库唯一约束比先查再写可靠
口述答案:先查再写把“是否处理过”和“执行业务”拆成两个非原子步骤。两个消费者可以同时查询不存在,然后都扣库存、记账或创建任务,应用层加一次普通判断无法阻止并发。数据库唯一约束把裁决放在真正的写入点:两个事务都尝试插入 consumer_role + event_id,只有一个成功,另一个得到唯一冲突并读取已有结果。这个去重插入要与业务更新处于同一本地事务;若先提交去重再写业务,业务失败后重试会被误判为完成;若业务先提交、去重后写,崩溃会再次执行。对于状态对象,还应叠加条件更新,例如库存必须满足 available >= qty,订单必须处于预期旧状态,轨迹聚合版本必须小于来件版本。唯一键解决同一事件并发,状态条件解决不同事件之间的业务冲突,两者不能相互替代。发生唯一冲突时也不能一律吞掉,要查询历史结果:若金额、对象和动作一致就是重复成功;若同一键对应不同金额,则是数据污染,应报警并隔离。压测时我会让几十个线程并发消费同一事件,验证只有一条去重记录、一条业务流水且最终不变量正确。
- 关联机制:去重表并发裁决
- 追问 1:悲观锁是否也能实现?回答:能,但锁范围和等待成本更高;唯一约束通常更直接,复杂聚合再结合锁或版本。
- 追问 2:唯一冲突日志很多会不会影响性能?回答:高重复场景可先缓存过滤,但数据库约束仍保底,并应监控重复来源而非关闭约束。
- 追问 3:批量消费怎么处理唯一键?回答:按事件逐项记录结果或使用可返回冲突明细的批量写入,不能整批失败后盲目重复副作用。
- 问题(综合题):去重、状态机和版本号怎样协同
口述答案:这三种机制解决不同层面的问题。去重回答“同一个事件是否已经处理过”,状态机回答“当前状态能否执行这个动作”,版本号回答“这条事件是否比当前聚合更新”。例如订单先收到支付成功事件 E1,随后迟到的关闭事件 E2 标识不同,仅靠事件去重会允许两者都执行;状态机规定已支付不能直接关闭,从而拒绝冲突。跨境轨迹中版本 43 先到、版本 42 后到,两条都是合法且不同的事件,明细表可以都保存,但聚合表用 current_version < incoming_version 条件只接受 43,避免状态倒退。版本跳跃还要记录缺口并主动拉取,不能把未收到的中间事件当成重复。库存预占则以状态机管理待处理、已预占、已确认和已释放,以条件扣减守住可用量不为负。实现上,去重记录、状态判断和业务更新应在同一本地事务中;条件更新受影响行为零后,读取当前值分类为重复、迟到或真正冲突。对撤销、退款等补偿动作,不应让版本倒退,而是创建更高版本的显式反向事件。这样即使消息重复、乱序和并发到达,系统仍按业务规则单调收敛。
- 关联机制:状态机与版本控制
- 追问 1:状态枚举越多越好吗?回答:不是,应围绕可观察业务阶段和补偿边界设计,避免无法解释的组合状态。
- 追问 2:版本缺口要阻塞后续事件吗?回答:视业务而定;可先接受高版本并记录缺口,强依赖中间状态时才按键暂停。
- 追问 3:谁负责生成版本号?回答:应由该聚合的权威写入方生成,多个上游不能各自创造不可比较的版本。
- 问题(综合题):请完整说明 Inbox(收件箱)与 Outbox(发件箱)模式
口述答案:Outbox(发件箱)用于发送侧,核心是把业务事实和待发送事件写入同一个本地数据库事务。订单提交成功时事件必然可恢复,发布器通过扫描、租约抢占或日志捕获发送;发送超时保持未知并以原事件标识重试。它解决数据库与消息发布的双写缺口,但会重复发布,也带来索引、扫描、归档和积压监控成本。Inbox(收件箱)用于接收侧,消费者在本地事务中先按消费职责和事件标识插入收件记录,再执行状态机与业务更新;重复命中时读取已有结果并确认。这样去重凭证不会与业务结果分离。两个模式组合后,每个服务只依赖自己的本地事务:订单服务发件箱发布出库事件,仓储服务收件箱创建任务,仓储状态再通过自己的发件箱回传。系统没有获得瞬时强一致,而是获得了每一步可追踪、可重试和可对账的最终一致。需要特别处理发送中租约过期、长期未知、收件处理中宕机和历史表增长。清理周期至少覆盖消息保留、最大重试和人工重放窗口,资金类关键字段还要归档。最终仍以业务差异对账兜底,而不是把发件箱已发送当作下游完成。
- 关联机制:Inbox(收件箱)与 Outbox(发件箱)
- 追问 1:发布器并发扫描如何防重复领取?回答:用条件更新、跳过锁定行或带版本租约抢占;即便重复领取也复用事件标识。
- 追问 2:发件箱能否放在独立数据库?回答:若不能与业务事实同事务,就重新出现双写缺口;应同库提交后再复制或归档。
- 追问 3:收件箱长期处理中如何恢复?回答:记录租约、处理版本和错误,超时后由补偿器重新领取,旧持有者失去令牌后不得提交。
- 问题(综合题):如何设计既有吞吐又有业务顺序的消费链路
口述答案:我会先缩小顺序范围,因为全局顺序要求所有消息通过一个串行点,会牺牲吞吐、扩容和故障隔离。订单履约通常只要求同一订单有序,物流只要求同一运单有序,设备告警只要求同一设备和告警实例有序。生产端用稳定业务键路由到同一 Partition(分区)或队列,不同键可以并行;消费端在键维度串行提交,并用业务版本号作为最后防线。单一消费者仍不能自动保证端到端顺序,因为它可能并发调用外部服务,后发请求反而先完成。失败处理是关键:若版本 v2 失败后进入普通延迟队列,而主队列继续提交 v3,就会破序;若原地无限重试,又会让整个分区头阻塞。可把失败键隔离到有序重试通道,暂停该键后续事件,其他键继续;超过预算转人工补偿。分区扩容也会改变取模结果,新分区的后续事件可能越过旧分区积压。安全做法是引入路由版本,排空旧路由后切换,迁移窗口用版本条件拒绝倒退。验收时要构造生产重试、并发完成、失败重投和扩容四类乱序,而不只是观察正常发送顺序。
- 关联机制:局部顺序与扩容破序
- 追问 1:热键怎么办?回答:先聚合和批量处理;能拆分时使用保持业务语义的子键,不能拆的键只能接受其串行上限。
- 追问 2:全局顺序什么时候值得?回答:极少数总账序列或配置变更可能需要,但应明确单点成本并有独立高可用设计。
- 追问 3:旧版本消息能直接丢弃吗?回答:聚合更新可跳过,原始事件和跳过原因仍应保留,必要时补历史明细。
- 问题(综合题):怎样建立可配置且不会放大故障的重试体系
口述答案:重试的第一步不是设置次数,而是错误分类。格式错误、验签失败、权限不足和状态机非法迁移属于永久错误,重复一百次也不会自愈,应直接进入修复或死信;网络抖动、临时限流、锁冲突可有限重试;支付、退款、面单购买等响应未知必须先查询权威状态,不能把查询替换成再次执行。暂时性错误采用指数退避,例如 1s、2s、4s、8s、16s,并加入 Jitter(随机抖动),防止同一时刻失败的实例同步醒来。除此之外还要限制单消息次数、总等待时长、全局并发、租户配额和目标依赖配额。实时消息与历史补偿使用不同预算,确保恢复后旧积压不挤占新业务。每次尝试保存错误码、下次时间和结果分类,达到预算后进入 DLQ(死信队列)或人工状态,而不是永久循环。恢复阶段先用小流量探针验证下游,再逐级放量;如果错误率或响应时间再次越线就回退。以承运商面单为例,每个渠道独立限速,超时先按请求号查单,明确不存在才重试,一个渠道熔断不阻塞其他渠道。验收不仅看最终成功率,还看重试放大倍数、恢复时间和是否影响正常流量。
- 关联机制:重试分类与退避
- 追问 1:固定间隔重试有什么问题?回答:会持续施压且大量实例同频,形成周期性流量尖峰。
- 追问 2:数据库死锁属于哪类?回答:通常是暂时性错误,可短退避有限重试,同时必须修复锁顺序或热点根因。
- 追问 3:最大次数如何确定?回答:由业务时效、下游恢复特征和容量预算共同确定,不应全系统固定一个值。
- 问题(综合题):指数退避和 Jitter(随机抖动)怎样计算与验证
口述答案:指数退避的目标是让连续失败的请求迅速降低施压频率,常见基础窗口为 base * 2^attempt,再设置最大等待上限。仅有指数仍不够:如果一万条消息在同一秒失败,它们会在第一、第二、第四秒同时醒来。全抖动可在 0 到当前指数上限之间随机选择,下游看到的是分散流量;等抖动则保留一半固定等待,再在剩余一半随机,延迟更可控。实际参数要结合业务时限,例如支付通知允许数分钟,紧急告警只能数秒,历史轨迹可更久。假设下游稳定能力 2,000 msg/s,当前有 10,000 条失败消息,恢复后重放预算只能给 1,500 msg/s,还要为实时流保留 500 msg/s。因此除了单消息退避,还要用令牌配额控制总体出口。验证时模拟下游完全不可用、间歇成功和缓慢恢复,观察每秒请求是否平滑、最大等待是否符合业务目标、停止重试后是否进入可查的死信。随机不等于不可审计,每条消息仍记录计划时间、实际时间和尝试次数。若服务再次恶化,熔断器应暂停新增重试,而不是继续耗尽线程和连接。
- 关联机制:退避与随机抖动决策树
- 追问 1:抖动会破坏消息顺序吗?回答:会,因此有序业务应按键安排重试窗口并暂停该键后续提交。
- 追问 2:最大退避设置很长有什么副作用?回答:恢复后业务收敛慢,应由主动探测和可控提前唤醒缩短等待。
- 追问 3:为什么还需要熔断?回答:退避控制单消息频率,熔断可在依赖整体故障时快速停止大批无效调用。
- 问题(综合题):死信队列应如何设计而不是沦为垃圾桶
口述答案:DLQ(死信队列)的职责有三个:隔离持续失败的毒消息,保护实时消费;保存失败证据,支持定位;提供修复后的受控恢复入口。进入死信时至少保存原始事件标识、业务键、来源位置、负载版本、错误分类、尝试次数、首末失败时间和关联链路,业务表同步进入“待补偿”而不是假装成功。死信数量、最老停留时间和业务金额必须告警并分配责任人。消息不能永远堆放,应按审计要求归档,但清理前必须确认业务已经完成、过期或人工关闭。重放不是简单把消息复制回主队列:先修复代码或数据,再查询当前状态,检查幂等记录、期望版本、业务时效和外部副作用。库存释放事件停留六小时后,订单可能已经确认,直接重放会释放合法占用;支付扣款事件更不能盲重放。重放使用原业务事件标识保持幂等,另加重放批次号记录操作者、审批、速率和结果。先选小样本观察错误率与业务不变量,再分批扩大,给实时流保留容量。仍失败的消息回到隔离区并更新原因,不能在主队列与死信之间无限循环。
- 关联机制:死信与人工重放
- 追问 1:死信是否算消费成功?回答:传输层可停止重投,但业务仍未完成,必须有待补偿状态和闭环责任。
- 追问 2:死信消息能修改后重放吗?回答:可以生成修正版新事件,但必须关联原事件并保留原文,避免审计断链。
- 追问 3:怎样判断死信积压严重?回答:同时看数量、最老年龄、涉及金额或订单数以及自动补偿成功率。
- 问题(综合题):人工重放怎样避免制造第二次事故
口述答案:人工重放本质是一次高风险批量变更,必须先做影响分析。第一步固定待处理集合,按业务状态分为可重放、已经完成、已过期、版本冲突和结果未知,不能用“死信总数”直接作为执行数。第二步验证幂等边界:数据库唯一键是否存在、保留期是否覆盖、外部渠道是否支持幂等请求号;若不满足,先补防护或逐笔人工确认。第三步核对版本和时效,例如旧轨迹可补历史但不能回退聚合,过期库存释放要看预占是否已经确认。第四步建立重放批次,记录审批人、事件范围、原始校验和、计划速率、停止阈值与回滚策略。先用几十条预演,观察消费错误、下游延迟、重复命中和业务差异;再以受控速率扩大,实时流拥有更高优先级。第五步结束后按输入、成功、幂等跳过、过期跳过、失败和人工六类核对总数,支付还要核金额与币种,库存要核流水和可用量。任何不能归类的消息都不能宣称恢复完成。系统界面应禁止无审批的全量按钮,并对资金操作使用双人复核和不可篡改审计记录。
- 关联机制:死信重放检查表
- 追问 1:重放使用原标识还是新标识?回答:原业务意图不变就沿用原标识;修正成新动作时生成新标识并关联原事件。
- 追问 2:如何停止正在进行的重放?回答:批次有独立开关和令牌配额,停止后保留每条进度,恢复时不从头盲跑。
- 追问 3:回滚能否删除已执行结果?回答:通常不能简单删除,应通过业务补偿动作逆转,并保留正反向流水。
- 问题(综合题):事务消息的完整流程、价值与失败边界是什么
口述答案:以 RocketMQ(分布式消息队列)事务消息为例,生产者先发送 Half Message(半消息),该消息对普通消费者不可见;Broker(代理节点)接收后,生产者执行本地订单或支付事务。事务明确成功则提交消息,明确失败则回滚;若二次结果没有送达,代理节点稍后回查。回查方法只能根据事务标识查询本地权威表:成功返回提交、失败返回回滚、证据不足返回未知,绝不能再次扣库存或调用支付,因为回查可能执行多次。它的价值是协调“本地事务提交”和“消息最终可见”,减少应用自己扫描发件箱的工作,但不是全链路分布式强一致。消息提交后,下游账务、履约和通知仍可能失败或重复,必须各自幂等;外部渠道资金也不在事务消息协议内,仍需主动查询和对账。生产者长期不可回查、事务标识无法查询、代理节点超过回查预算等情况都要告警并转补偿。项目中我会预先持久化事务标识,确保回查有证据;监控半消息年龄、回查次数和最终提交比例。最后用支付单、事件台账、下游流水和订单状态做四方对账,而不是只看事务消息已经提交。
- 关联机制:事务消息与对账
- 追问 1:本地事务执行很久怎么办?回答:缩短事务并让回查返回未知,避免在回查中重复执行;超时后进入异常流程。
- 追问 2:事务消息能代替下游幂等吗?回答:不能,它只协调上游本地事务与消息可见性,投递和下游副作用仍可能重复。
- 追问 3:回查查询不到记录应回滚吗?回答:先区分事务未开始与数据暂不可见;证据不足应返回未知并告警,不能武断裁决。
- 问题(综合题):事务消息和 Outbox(发件箱)应该怎样选
口述答案:两者解决的是同一方向的问题:本地业务事实已经提交时,事件最终必须可发布。事务消息把协调协议放在消息产品中,通过半消息、本地事务结果和回查决定可见性,延迟较低、集成路径明确,但客户端与运维依赖特定产品,回查逻辑和半消息异常必须治理。Outbox(发件箱)把业务和事件写入同一数据库事务,再由发布器扫描或捕获数据库日志发送,产品中立、数据可直接查询和补发,代价是表增长、索引、清理、扫描延迟、发布租约与未知状态管理。选择时看团队基础设施、数据库负载、事件量、审计要求和跨产品迁移需求。支付与订单若已经有成熟发件箱平台,我倾向继续使用,便于按支付单查询;高吞吐且团队对 RocketMQ(分布式消息队列)事务消息运维成熟时,也可采用事务消息。但不能同时叠加两套而没有唯一权威,否则可能产生两个事件源。无论选哪种,下游都需要幂等、状态机和对账,因为它们只保护上游本地事务与事件之间的缺口。定时对账是第三道独立保险,用于发现发布器、回查器、下游和外部渠道的长期差异。
- 关联机制:事务消息与 Outbox(发件箱)对比
- 追问 1:发件箱扫描会拖慢业务库吗?回答:按状态和时间建立合适索引、分片归档并限制批次,必要时使用日志捕获降低扫描压力。
- 追问 2:两种方案能迁移吗?回答:可以双读校验后切换唯一事件源,但迁移期必须共享事件标识并防止双发成为两次语义事件。
- 追问 3:对账频率怎么定?回答:由资金风险、业务时效和系统容量决定,实时异常检测与日终全量对账可并行。
- 问题(综合题):支付成功后订单、账务与通知怎样最终一致
口述答案:权威事实首先是经过验签和防重放校验的渠道交易结果。本地事务以渠道交易号唯一约束,把支付单从处理中变为成功,并写入稳定 PaymentSucceeded 事件;事件可由事务消息或 Outbox(发件箱)发布。账务消费者按消费职责和事件标识唯一生成资金流水,金额、币种、方向必须与支付单一致;订单消费者用支付单号和状态机将订单转已支付;通知消费者创建独立通知任务,失败不影响资金和订单。每个消费者业务提交后再确认,重复消息只命中去重或状态条件。订单若已取消而支付成功迟到,不能丢弃资金事件,应进入退款补偿状态;账务失败进入重试和死信,但支付单仍保持渠道事实。发送或消费响应未知时先查询本地结果,外部渠道动作复用幂等请求号。日终按渠道流水、支付单、账务流水和订单支付状态四方对账,将缺账、缺状态、金额冲突和未知分别处理;金额冲突必须人工,不能自动覆盖。面试中我会强调“消息成功不等于资金正确”,最终依据是笔数、金额、币种和方向全部一致,且每个差异都有补偿单和审计记录。
- 关联机制:支付事件最终收敛
- 追问 1:通知先成功、订单后失败有问题吗?回答:通知内容应读取可查询状态或等待订单事件,错误通知需更正,但不能反向修改资金事实。
- 追问 2:渠道回调重复怎么处理?回答:渠道交易号唯一,状态机只允许合法迁移,重复返回既有处理结果。
- 追问 3:支付成功事件丢失如何发现?回答:发件箱滞留与四方对账都会暴露支付成功但下游无流水或无状态的差异。
- 问题(综合题):MQ(消息队列)在库存防超卖中到底承担什么职责
口述答案:MQ(消息队列)可以削峰、异步通知履约和驱动补偿,但不能作为库存正确性的唯一权威。防超卖底线仍在数据库:扣减或预占使用 available >= qty 的条件更新,受影响行为一才成功;同时写库存流水和预占单,状态从待处理到已预占,再到已确认或已释放。事件与这些事实通过 Outbox(发件箱)同事务产生,消费者按预占动作号去重,重复确认或释放由状态机拒绝。若先在缓存或队列中减数而数据库没有约束,消息丢失、重复和并发切换都可能造成负库存。超时释放也不能仅靠一条延迟消息:释放事件可能早于订单确认事件,必须检查当前预占状态和版本;已确认预占不得被普通超时释放。发送超时使用同一事件标识重发,消费确认丢失由唯一流水吸收。库存对账定期验证期初数量加收入减出库等于期末数量,并比较可用、占用和实物。故障时可暂停非关键库存广播,保留权威写入;恢复后限速追赶并拒绝旧版本。这个设计把队列的吞吐价值和数据库的正确性职责分开,面试时更可信。
- 关联机制:库存状态机与幂等
- 追问 1:库存消息积压是否还能下单?回答:权威库存写入健康时可继续,但下游展示可能降级并明确延迟,不能绕开条件扣减。
- 追问 2:重复释放如何处理?回答:释放动作号唯一且状态只能从已预占到已释放,第二次更新受影响行为零。
- 追问 3:缓存库存有什么作用?回答:可用于预检和限流,最终成功仍以数据库约束或一致的权威库存服务裁决。
- 问题(综合题):跨境物流轨迹的重复、乱序与缺口怎样治理
口述答案:轨迹链路首先保存承运商原始报文,不能只保存最终状态。幂等键优先使用承运商事件标识;没有时组合承运商、运单号、来源序号或事件码与发生时间。生产侧按运单号路由,让同一运单进入同一 Partition(分区)或队列,不同运单并行。明细表对来源序号唯一,重复消息只增加重复指标;聚合表使用版本条件,只接受更高版本或状态机允许的补偿动作。若版本 43 先到、42 后到,42 可以补历史,但不能把已出库回退到已拣货。发现 41 直接跳到 43 时记录版本缺口并主动调用承运商查询,不必阻塞其他运单。外部接口限流按承运商隔离,超时使用稳定请求号查证,失败进入退避和死信。分区扩容通过路由版本迁移,避免同一运单的新旧事件分散后越过。对账按运单比较来源最大版本、本地明细最大版本和聚合版本,把可自动拉取、来源冲突和人工核验分开。这样顺序不是依赖“消息永远按时到达”,而是由键路由、版本裁决、原始证据与缺口补偿共同实现。
- 关联机制:局部顺序与版本控制
- 追问 1:承运商时间不准怎么办?回答:时间只作辅助,优先稳定事件号和来源序号;无法比较时建立偏序并保留冲突。
- 追问 2:撤销事件会让状态倒退吗?回答:它应是更高版本的显式补偿动作,由状态机允许,而不是伪造较小版本。
- 追问 3:热点运单如何处理?回答:单运单通常仍需串行,可批量聚合;不要为了吞吐把一个运单随机拆到多个分区。
- 问题(综合题):异步导出任务怎样避免重复执行与重复产物
口述答案:消息只负责唤醒执行,任务表才是权威状态。用户提交导出后先创建唯一任务号,状态为待处理,并记录查询条件快照、数据版本和输出格式;任务事件与任务表同事务产生。消费者通过条件更新把待处理改为运行中,同时写执行者、租约截止时间和版本令牌,受影响行为零说明任务已被其他实例领取。长任务按分片号拆分,每个分片结果有任务号加分片号唯一约束;消费者确认丢失或实例重启后,重复领取不会生成两份有效分片。执行器周期续租,失去租约的旧实例必须停止提交。所有分片完成后,汇总器在版本化临时路径生成文件,校验行数和摘要,再通过条件更新原子发布唯一最终版本;通知是独立幂等任务。实例宕机时租约到期,补偿器重发原任务事件,不创建新任务号。失败按可重试、参数错误和资源不足分类,超过预算进入人工或降级为更小分片。验收会模拟重复消息、续租失败、部分分片成功和汇总宕机,最后核对一个任务只有一个已发布版本,分片总行数与数据库快照一致。
- 关联机制:消费幂等与任务状态机
- 追问 1:为什么不直接在消费者内一次导完?回答:长事务难恢复、占用租约且容易 OOM(内存溢出),分片状态机更可控。
- 追问 2:通知重复有什么影响?回答:通知任务也需唯一键;即使重复,下载地址指向同一已发布版本。
- 追问 3:任务参数改变怎么办?回答:创建新任务版本和新业务意图,不能复用旧幂等键覆盖已执行任务。
- 问题(综合题):如何把未知结果纳入统一状态机?
口述答案:未知不是失败的别名,而是必须由权威证据收敛的状态。支付渠道超时、生产端确认包丢失、外部面单创建响应丢失,都可能已经在对方完成。设计上先持久化业务键、事件标识、尝试号、目标系统、请求摘要和最后时间;状态机显式区分明确成功、明确失败与未知。未知状态禁止直接新建请求或执行反向动作,应先以同一幂等键查询本地 Outbox(发件箱)、Broker(代理节点)审计和外部权威记录;查到成功则补写本地结果并推进下游,查到失败才允许重试,仍未知则退避重查并由对账覆盖。消费侧同样把“业务已提交而确认丢失”视为可重放未知,依靠唯一约束返回既有结果。这样把网络不确定性转为可审计任务,支付、库存和履约都不会因为一次超时擅自重复扣款、释放库存或重复建单。最终用差集和状态年龄告警验证未知没有长期悬挂。
- 问题(综合题):为什么本地消息表需要租约与状态机?
口述答案:本地消息表不是插入一行待发就自动可靠。多个发布器可能并发扫描,同一实例也可能在发送确认前宕机;没有状态机和租约会双发、永远卡在发送中,或把未知误标成功。记录至少有新建、发送中、已发、未知、待人工等状态,扫描时以条件更新抢占短租约,过期后其他实例接管。发送使用稳定事件标识,确认成功才更新已发;确认丢失则保留未知并允许同键重发,依赖消费者 Inbox(收件箱)消化重复。表要按状态和时间索引、分区归档,监控最老待发年龄、未知数量和抢占失败。支付状态与待发支付成功事件同事务写入,库存预占与库存事件同事务写入,才避免业务成功却没有补发证据。发布器只负责传输,最终是否推进仍由消费审计和对账判断。
- 问题(综合题):如何处理第三方限流与消息可靠性的冲突?
口述答案:限流时不能用无限并发重试证明可靠,可靠的目标是让消息在业务时效内可恢复地等待。消费端按承运商、支付渠道或租户隔离队列和并发,读取限流响应的等待建议或使用指数退避加抖动;每次延迟保留原业务键、截止时间和尝试原因。实时主链路与历史补偿分开配额,防止积压回放挤掉新订单。超过业务时效、权限错误或格式错误的消息进入死信并告警,而不是继续占用配额;响应未知先按请求号查询,明确不存在才重试。指标同时看配额拒绝、重试放大、延迟队列年龄、下游成功率和业务差集。对支付,渠道查询和对账优先于再扣;对履约,查面单优先于再创建。这样系统承认依赖能力边界,用排队、退避和补偿保护正确性,而不是把下游压垮。
- 问题(综合题):如何验证幂等实现真的正确?
口述答案:验证幂等不能只做一次正常消费测试,而要对每个副作用注入重复、并发、确认丢失、重启、乱序和人工重放。支付测试同一渠道交易号并发十次,断言只有一条账务流水且金额币种一致;库存测试同一预占号重复确认和释放,断言状态不能从已确认回到已释放、可用量不为负;履约测试确认丢失后的重投,断言只有一个外部面单和一个有效状态。实现层看唯一约束是否是最终裁判、条件更新是否检查影响行数、Inbox(收件箱)与业务写是否同事务;观测层看重复命中是否可关联到事件标识而非静默吞掉。最后构造对账差集,确认消息重复后权威数据仍收敛。只有这些故障试验和业务不变量都通过,才能说至少一次投递被正确消化。
- 问题(综合题):消息优先级会怎样影响顺序和可靠性?
口述答案:优先级是业务调度策略,不是免费的可靠性增强。高优先级消息越过低优先级消息时,会破坏同一业务键的先后关系;如果支付、取消和履约共享优先级队列,后来的高优先级取消可能先执行,因此必须按业务键和状态机判断,而不是按队列出队顺序直接改状态。正确做法是把严重告警、资金风险等独立业务域隔离,或在同一键内保持顺序,再用版本控制处理跨域事件。优先级还可能让低优先级长期饥饿,所以要设置容量配额、最大等待时间和降级规则。重试、死信和人工重放同样要带原始优先级但不得绕过幂等校验。验证时观察高低优先级延迟、饥饿率、状态冲突和对账差集,确保“先处理重要事件”没有演变为“破坏业务事实”。
- 问题(综合题):如何做可靠消息的灰度发布?
口述答案:灰度发布首先保证新旧消费者对同一事件语义一致。事件带版本,生产端在迁移窗口可双写或保持向后兼容,消费者先具备读取新旧版本和幂等处理能力,再逐步切流;不要先发布生产端而让旧消费者收到无法解析的毒消息。新旧路径共享稳定业务键,避免双写变成两次扣款或两次履约;Inbox(收件箱)和唯一约束要跨版本生效。灰度期间分别监控解析失败、路由退回、重复命中、状态冲突、重试和死信,按业务键抽样核对支付、库存、履约的权威结果。回滚时停止新版本生产而不删除已发事件,兼容消费者继续处理或把不可处理消息隔离,随后按原事件标识补偿。完成条件不是发布成功,而是旧版本流量归零、对账差集不扩大、死信分类清零后再移除兼容逻辑。
- 问题(综合题):如何避免人工补偿绕过可靠性设计?
口述答案:人工补偿必须被当作一种受控业务命令,而不是直接修改最终状态。操作人员先选择差集来源、业务键和原因,系统查询当前权威状态、幂等记录、外部副作用和时效;满足条件后生成新的补偿单或重放批次,用唯一键、审批、限速和状态机执行。补偿消息关联原事件而不是伪造原事件,方便审计谁在何时为何处理;执行成功后再次对账,失败进入可跟踪队列。资金补偿如退款或冲正必须以账务和渠道事实为前提,库存补偿不得释放已确认预占,履约补偿不得删除已签收面单。监控人工补偿量、成功率、二次冲突和差集年龄;异常上升说明自动链路存在系统性缺陷。这样人工介入仍沿用幂等、确认和审计规则,不会成为绕开所有安全边界的新故障源。
- 问题(综合题):如何衡量消息可靠性投入是否有效?
口述答案:不能只看发送成功率或队列积压。可靠性效果应同时看技术证据和业务不变量:技术层包括未知发送率、确认失败率、副本或队列可用性、重复命中率、重试放大倍数、死信增长和最老消息年龄;业务层包括支付渠道—账务差异、库存流水差异和负库存、履约漏建、轨迹版本缺口、任务悬挂年龄。还要测恢复能力:注入 Broker(代理节点)重启、确认丢失、消费者崩溃、依赖限流和版本错误后,差集多久归零,恢复是否压垮下游,人工介入多少。投入有效意味着重复可被安全吸收、未知可被快速收敛、毒消息不会阻塞主链路、业务差集持续在目标窗口内,而不是简单地把失败隐藏在更长的重试队列里。
- 问题(综合题):如何向业务方解释最终一致而不让人担心?
口述答案:我会把最终一致说成有边界、有时限、有证据的业务承诺,而不是“以后会好”。先明确同步完成的权威动作,例如库存预占、支付验签和账务入账;随后说明异步动作如通知、履约推进、轨迹同步会在目标时间内通过可靠消息处理。若出现超时,系统不会丢弃,而是有稳定事件标识、重试、死信、对账和人工升级;业务可在订单页查询当前状态,不能因为通知延迟而认为资金未到账。对高风险差异设更短告警和人工通道,对低优先级通知允许更长补偿窗口。给业务方的可见指标是差集数量、最老差异年龄和补偿成功率。这样最终一致不是模糊承诺,而是把可用性与正确性取舍、恢复时间和责任边界讲清楚。
- 问题(综合题):如何总结本章的可靠性设计原则?
口述答案:第一,任何确认都只证明自己的边界:发布确认不等于路由,路由不等于消费,消费确认不等于资金、库存或履约成功。第二,关键链路选择至少一次投递,把重复视为正常故障恢复,用稳定幂等键、唯一约束、状态机和版本保证业务效果一次。第三,发送和外部调用超时属于未知,先查证再同键重试,禁止另造业务事实。第四,顺序缩小到业务键,不承诺昂贵的全局顺序,并用状态机消化扩容、重试和回补带来的乱序。第五,重试只用于可恢复错误,使用退避、抖动、预算、隔离和死信保护下游;死信重放先预检幂等、版本、时效和副作用。第六,Outbox(发件箱)或事务消息解决本地双写窗口,Inbox(收件箱)解决重复消费,Reconciliation(对账)和补偿解决所有跨系统未知差异。最后以支付、库存和履约的不变量验证收敛。
flowchart LR
A["权威业务事实"] --> B["稳定事件标识"] --> C["可靠发布"] --> D["幂等消费"]
D --> E["重试或死信隔离"] --> F["对账补偿"] --> A- 问题(综合题):Runner(执行器)调度中消息重复和执行器失联如何处理
口述答案:Runner(执行器)调度不能把“消息已投递”当作“任务只执行一次”。调度中心先创建任务实例和尝试号,记录目标、参数摘要、计划时间和状态,再发送包含稳定实例号的事件。执行器领取时使用条件更新取得租约和 Fencing Token(隔离令牌);重复消息或两个实例并发领取时,只有版本更新成功者获得当前令牌。执行期间定期续租并上报进度,完成结果提交必须携带令牌,数据库只接受当前令牌,防止已经超时的旧执行器在新执行器完成后覆盖结果。失联时调度中心不能立即假定任务未执行:若动作支持幂等,以同一业务请求号重试;若是不可重复的外部操作,先查询目标状态,无法判定则进入人工。失败重试创建新的尝试号但沿用任务实例业务意图,记录每次开始、结束和错误。消息确认在任务实例成功落状态后完成,确认丢失只会再次查询状态。对长任务设置最大运行、心跳和取消协议,取消也是状态机动作,不是简单删消息。验收重点是网络分区:旧执行器继续运行、新执行器接管,最终只有最高隔离令牌能发布结果;同时核对任务实例、尝试记录和外部副作用,而不是只看调度中心显示成功。
- 关联机制:状态机、版本与消费确认
- 追问 1:租约到期是否证明旧执行器停止?回答:不能,隔离令牌用于拒绝旧执行器后续提交,外部系统也应校验令牌或幂等号。
- 追问 2:重试时为什么要新尝试号?回答:便于区分执行历史和度量,但任务业务幂等键仍保持稳定。
- 追问 3:执行器回报成功但中心超时怎么办?回答:中心按任务实例号查询结果,未知期间不创建新的业务动作。
- 问题(综合题):IoT(物联网)报警风暴如何兼顾可靠性和容量
口述答案:报警风暴不能把所有事件都按同一可靠级别处理。接入层先按设备、告警码和时间窗口建立事件身份,紧急停机、火灾等高等级告警走独立高优先级通道,使用 At-least-once(至少一次)、持久化、幂等和超时升级;普通抖动告警可在边缘或流处理中窗口聚合、采样,允许可解释的 At-most-once(至多一次)损失。消费者以 device_id + alarm_code + alarm_instance 管理状态机,从正常到触发创建实例,持续触发只增加次数和最后时间,从触发到恢复关闭实例,再次触发产生新实例,不能永远按设备和告警码去重。突发 50,000 msg/s 而后端只能处理 10,000 msg/s 时,先保证紧急流配额,普通流按窗口摘要;重试流有独立上限,避免失败消息把入口放大。通知任务按告警实例和渠道去重,高等级未确认时按升级策略通知值班人。死信保留原始测点和聚合关系,重放前检查告警是否已恢复,过期普通通知不再发送。最终指标包括紧急告警端到端延迟、聚合压缩比、重复命中、未确认告警和状态差异,不能只报消息吞吐。
- 关联机制:语义分层与重试预算
- 追问 1:聚合会不会丢重要细节?回答:保留首值、末值、峰值、次数和原始存储索引,高等级事件不进入普通聚合。
- 追问 2:恢复消息先于触发消息怎么办?回答:按设备版本或发生时间处理,无法判定时保留冲突并查询设备当前状态。
- 追问 3:通知失败是否反复重试?回答:按等级设预算,高等级升级到其他渠道和人工确认,普通通知过期后停止。
- 问题(综合题):失败重试导致业务顺序颠倒时有哪些处理策略
口述答案:假设同一订单版本 v2 更新失败,被放入延迟重试,而主队列继续处理 v3,那么 v3 可能先提交,随后 v2 把状态倒退。处理策略取决于顺序强度。最严格的是原地阻塞该键直到 v2 成功,但不能阻塞整个 Partition(分区);可以把该业务键及后续消息转入有序重试通道,其他键继续。第二种是允许并发到达,但聚合更新必须携带版本条件,v2 在当前版本已为 v3 时只补历史,不更新当前状态。第三种适用于可交换操作,例如计数增量,可把事件设计成独立流水后聚合,不要求到达顺序。永久失败时不能让该键无限阻塞,应保存缺口、转人工或从权威上游重新拉取快照,再从更高版本继续。生产者也要使用同一业务键,生产重试不能随机换分区。分区扩容期间引入路由版本,避免新旧分区同时承载同一键。验收要故意让中间版本失败、后续版本成功,再恢复旧消息,检查状态不倒退、历史完整、缺口有记录。面试答案不能只说“单消费者保证顺序”,因为延迟队列、外部异步完成和扩容都可能突破这层假设。
- 关联机制:局部顺序和重试破序
- 追问 1:所有消息都需要版本号吗?回答:不一定;有聚合状态和乱序风险的消息需要,可交换流水可凭唯一键累积。
- 追问 2:按键暂停如何实现?回答:维护键级阻塞状态,将同键消息暂存到有序重试流,成功或人工裁决后再释放。
- 追问 3:快照覆盖是否会丢历史?回答:快照只修复聚合状态,原始事件与缺口记录仍保留用于审计。
- 问题(综合题):支付消息进入死信后为什么不能直接重放
口述答案:资金事件在死信期间可能已经通过渠道回调、主动查询或人工处理完成,直接重放会重复扣款、退款或记账。正确流程先冻结死信集合并按支付单查询四类证据:渠道流水、本地支付或退款单、账务流水、订单状态。若渠道成功而内部账务缺失,重放的是内部记账事件,不是再次调用渠道;若本地和渠道都失败,确认业务仍有效后才允许重试;若渠道结果未知,继续主动查询或转人工。每条消息校验商户、金额、币种、方向、事件标识和当前状态版本,任何金额冲突都禁止自动化。内部账务用事件标识和流水类型唯一约束,渠道调用复用原支付单或退款单作为幂等请求号。重放建立审批批次,先小额、小数量预演,限制每秒笔数和总金额,观察渠道错误率与账务差异后逐步扩大。输入总笔数要与成功、幂等跳过、过期、冲突和失败分类求和一致;完成后再做金额、币种和方向对账。死信只是技术隔离,业务上支付单必须处于待补偿或人工状态,不能因为消息已转移就对用户显示最终成功。
- 关联机制:支付死信与人工重放
- 追问 1:为什么金额也要成为校验项?回答:同一标识若负载金额不同是严重数据污染,唯一键不能把冲突误当正常重复。
- 追问 2:可以回滚重放吗?回答:已发生的资金副作用只能通过退款、冲正等新业务流水补偿,不能删除历史。
- 追问 3:人工处理后如何防止后续自动重放?回答:写入带版本的裁决结果和关闭原因,重放预检必须读取该权威状态。
- 问题(综合题):怎样建设消息可靠性的可观测性和对账闭环
口述答案:可观测性要让一个事件从产生到业务结果都可定位,而不是只有一条链路标识。生产侧记录业务键、稳定事件标识、负载版本、发送尝试、明确结果或未知状态;代理节点侧能定位 Topic(主题)、Partition(分区)或队列、消息位置和确认边界;消费侧记录消费职责、尝试次数、去重命中、状态前后值、业务流水与确认结果。重试、死信和人工重放都有独立批次与操作者。指标分为技术和业务两组:技术上看发送未知、发布失败、消费延迟、确认失败、重复率、重试放大、死信数量与最老年龄;业务上看支付四方差异、库存负数和流水差异、轨迹版本缺口、任务悬挂。对账不是只查“有没有记录”,而是比较笔数、金额、币种、方向、版本和状态,并把差异分为自动补偿、等待外部查询和人工冲突。告警应按影响量而非单一条数,例如十条高金额支付差异比一万条普通通知死信更紧急。恢复后用事件输入数等于首次成功、幂等跳过、过期和失败之和来闭环,并验证业务不变量归零。链路标识服务诊断,业务幂等键服务正确性,二者同时记录但不能混用。
- 关联机制:可观测性与业务对账
- 追问 1:没有积压是否代表健康?回答:不代表,提前确认可能静默丢失,业务差异只能靠事件台账和对账发现。
- 追问 2:重复率升高一定是故障吗?回答:不一定,发布结果未知或再均衡会升高;要结合确认失败和业务差异判断。
- 追问 3:日志保留多久?回答:覆盖排障、重试和审计周期,资金关键字段按合规期限归档并脱敏。
- 问题(综合题):如何复盘一次“业务成功但消息确认丢失”的生产事故
口述答案:复盘先讲影响和权威事实。例如库存消费者写库成功后 ACK(确认)持续丢失,五分钟内同一批事件平均投递六次,数据库连接被重复查询占满,但库存数量没有多扣。时间线要对齐网络抖动、确认错误率、重复命中率、连接池等待和消费积压,而不是从应用异常栈猜原因。止血时先限制重投并为重复命中走快速路径,保护关键库存事务;不能提前确认未处理消息,也不能删除去重数据。证据上按事件标识抽取代理节点投递记录、消费者事务提交、唯一键冲突与确认请求,证明业务一次、投递多次。根因可能是确认链路网络故障,同时重复路径仍查询大量关联表,导致放大。短期修复确认连接并对已完成事件快速返回;长期将去重索引前置、设置重试预算、监控确认失败与重复成本,并在压测中注入“提交后断网”。恢复验证要核对输入事件数、唯一库存流水、重复命中数、最终积压和库存不变量。复盘还要说明如果没有数据库唯一约束会造成什么资金或库存后果,让措施与风险直接对应,而不是只写“加强监控”。
- 关联机制:消费确认丢失与幂等
- 追问 1:重复快速路径还要查业务表吗?回答:应读取去重记录中的结果摘要,必要时最小查询,避免重新执行完整业务链。
- 追问 2:可以临时改成自动确认吗?回答:关键业务不可以,这会把重复风险换成静默丢失。
- 追问 3:事故结束标准是什么?回答:确认错误恢复、积压清零、重复成本正常且库存流水对账无差异。
- 问题(综合题):去重记录保留期与清理机制应该如何设计
口述答案:去重记录不能凭数据库容量随意删除,保留期至少覆盖消息的最大保留时间、自动重试、死信停留、备份恢复和人工重放窗口。假设消息保留七天、死信允许三十天处理,那么七天后删除去重记录会让旧死信重放再次产生副作用。支付、账务和退款还涉及审计,关键业务键和结果摘要可能长期归档,而不是彻底删除。容量治理可按处理日期分区,把活跃窗口放在主表,历史数据转只读归档;重放前同时查询活跃与归档索引。清理任务本身要有水位和审计,只有确认业务已终结、相关消息不可再合法重放且归档成功,才能删除明细。不能只依赖 Redis(远程字典服务)过期键,因为重启、淘汰和过期都会失去正确性凭证;缓存只能加速近期重复过滤,数据库唯一约束或业务流水仍保底。还要考虑业务键复用,例如外部系统多年后重复使用请求号,应把商户、业务类型和时间域纳入唯一键。清理后若收到超出保留窗口的旧消息,应进入隔离与人工核验,而不是当新消息执行。通过历史重放演练验证归档查询和旧消息拒绝路径,才能证明保留策略完整。
- 关联机制:幂等记录与 Inbox(收件箱)
- 追问 1:去重表会成为热点吗?回答:可按消费职责和时间分片、批量归档,并让业务主键均匀分布,唯一约束仍保留。
- 追问 2:事件永不过期怎么办?回答:关键标识和结果摘要长期归档,原始大负载可按合规规则压缩或外置。
- 追问 3:删除前需要消费方确认吗?回答:应以全链路最大重放窗口和业务终态为准,不能只看单个消费方近期无重复。
- 问题(综合题):调用外部接口响应未知时怎样避免重复副作用
口述答案:响应未知是分布式系统无法仅靠重试消除的核心问题。消费者调用支付、面单、仓库或短信接口超时,远端可能没执行,也可能已执行但响应丢失。第一选择是协议级幂等:调用前在本地任务表保存稳定业务请求号,每次重试复用同一值,远端对该值唯一并返回原结果。第二选择是主动查询:超时后先按请求号或业务单号查询,明确不存在才重试,已存在则把本地状态收敛到成功。第三是状态机将结果分成处理中、成功、明确失败和未知,未知不会被普通重试器当失败。若对方既不支持幂等也不能查询,高风险动作必须串行、缩小自动重试并设置人工闸门,尤其不能盲重试扣款或退款。外部调用不应长时间占用本地数据库事务;先落任务状态,调用后再用版本条件提交结果,旧执行器或迟到响应不能覆盖新裁决。对账用对方清单、回调或账单发现长期差异。面试中我会明确 MQ(消息队列)只能再次触发调用,无法知道现实副作用是否发生,正确性依赖业务协议和证据。
- 关联机制:结果未知与幂等请求
- 追问 1:查询结果也未知怎么办?回答:保持未知并继续受控查询,超过时限转人工,不猜测结果。
- 追问 2:迟到成功响应如何处理?回答:携带请求号和版本校验;若已经人工裁决,进入冲突核验而非覆盖。
- 追问 3:短信为何也要幂等?回答:重复通知会误导用户;以通知任务和渠道唯一键去重,过期后停止发送。
- 问题(综合题):分区或队列扩容为什么可能破坏顺序,如何安全迁移
口述答案:按业务键取模路由时,分区数从 8 增到 12 会改变很多键的映射。某订单旧版本仍积压在原分区,新版本却进入新分区并先被消费,于是局部顺序被打破。仅靠“同一键发同一分区”的口号没有覆盖路由变更。安全迁移有三类办法:最稳妥的是冻结相关生产、排空旧分区、更新路由版本后恢复,适合短窗口;第二种是双路由迁移,事件携带路由版本,消费者为每个业务键维护切换水位,确认旧版本已处理后才放行新路由;第三种允许新旧并发,但聚合写入必须带业务版本条件,旧事件只能补历史,不能覆盖当前状态。无论哪种都要避免生产者在重试时使用不同分区数计算路由,因此路由规则应由集中版本控制,而不是实例自行读取变化值。迁移前评估热点键,扩容并不能拆开必须串行的单键瓶颈。迁移验收选取固定业务键发送连续版本,在切换、消费者重启和失败重投下检查聚合不倒退、历史不缺失,并对比新旧路由事件数量。发现版本缺口时暂停该键或从权威源补拉,不能用全局停机掩盖。
- 关联机制:局部顺序与分区扩容
- 追问 1:一致性哈希能完全避免吗?回答:只能减少迁移键数量,发生迁移的键仍要处理新旧队列并行窗口。
- 追问 2:扩消费者会破坏顺序吗?回答:产品若保持单分区单所有者,存储顺序仍在;业务内部并发完成仍需控制。
- 追问 3:热点键能拆分吗?回答:只有业务操作可交换或可按子实体独立时才拆,资金总序列不能随意拆。
- 问题(综合题):请设计一个统一适用于库存、支付、轨迹和异步任务的消息可靠性模板
口述答案:统一模板先确定权威数据源和业务不变量:库存以库存表与流水为准且可用量不为负,支付以渠道流水和支付单为准且金额币种方向一致,轨迹以来源事件和聚合版本为准且状态不倒退,异步任务以任务表和唯一产物版本为准。每次业务意图生成稳定事件标识,业务事实与 Outbox(发件箱)或事务消息状态同本地事务提交。发布结果分成功、失败、未知,未知按原标识查证和退避重试。消费者按“职责加事件标识”建立唯一约束,在同一事务内写 Inbox(收件箱)、校验状态机和版本、提交业务,再发送 ACK(确认)或推进 Offset(位移)。外部副作用使用稳定请求号,超时先查询。顺序只在订单、运单或设备键内保证,失败键隔离重试,聚合版本拒绝倒退。永久错误和超预算消息进入 DLQ(死信队列),重放前核幂等、版本、时效和容量,并用批次审计。全链路记录事件、业务键、尝试、位置和结果;技术指标与业务对账并行。事故时先保护权威写入和关键消费,限制重试,再按事件证据定位。最终用差异归零和不变量成立验收,而不是用某个产品确认参数代表全部正确性。
- 关联机制:本篇完整可靠性模板
- 追问 1:这个模板最容易遗漏什么?回答:外部副作用结果未知、死信重放前的时效校验以及业务对账责任。
- 追问 2:什么时候可以简化?回答:数据可丢、可重建且无资金库存风险时,可降低持久化与补偿成本,但要明确损失边界。
- 追问 3:上线前最关键的演练是什么?回答:发送确认丢失、业务提交后宕机、重复并发、乱序、下游限流和死信重放六类故障注入。
