题目与场景
事件生产者向外部 HTTP 端点发送业务事件。平台要持久化事件、至少一次投递、让重复安全,并为每个租户提供清晰的重试与重放控制。
面试官考察什么
- 选择明确的投递保证,不在 HTTP 上承诺恰好一次。
- 分离接收、调度、投递尝试与订阅方副作用。
- 结合事件 ID、签名时间戳、重试策略、死信处理与公平性。
作答前的澄清问题
- 事件量、载荷大小、端点数和最大投递延迟是多少?
- 需要按租户、端点保证顺序,还是不要求顺序?
- 消费者能否幂等处理事件,去重状态要保留多久?
- 载荷有什么重放、删除、隐私和审计要求?
30 秒回答框架
我会先持久化带稳定 ID 的不可变事件,再调度投递尝试并快速返回。工作进程用时间戳签名原始载荷,采用带抖动的指数退避,并区分可重试与终止失败。消费者在副作用前按事件 ID 去重。按租户调度、熔断和死信队列防止离线端点拖垮其他租户;重放创建新的投递尝试,但不改变原事件身份。
分步深挖
1. 先保证事件持久化
通过事务性 outbox 或可靠写入,把业务事件与投递记录一起落盘。记录租户、端点、事件 ID、尝试次数、下次尝试时间和状态。发送后记录成功前崩溃是正常情况,会产生再次尝试,因此消费者必须容忍重复。
2. 安全验证与签名
使用时间戳和原始字节签名,并把事件 ID 纳入签名消息。Stripe 文档使用带时间戳的签名限制重放攻击。消费者在解析前验证签名,拒绝超出容忍窗口的时间戳,并设计密钥轮换。
3. 分类响应并重试
网络超时、连接失败和部分 5xx 属于可重试错误。认证错误、事件版本不支持和多数 4xx 属于终止或人工处理错误。使用带抖动的指数退避、最大尝试窗口和死信状态;不要对永久失败端点无限重试。
4. 显式处理重复与重放
消费者用持久唯一约束保存已处理事件 ID,并尽量与业务副作用同事务提交。人工重放复用不可变载荷,创建新的投递尝试并记录审计;仪表盘区分首次投递、自动重试和人工重放。网络只能提供至少一次,副作用的恰好一次需要消费者事务。
5. 扩展而不让租户互相饥饿
按租户或端点分区队列,限制每租户并发与速率,并为健康租户预留容量。端点连续失败时触发熔断。监控最老待处理事件年龄、成功延迟、尝试分布、重复率、签名失败和死信量。
高质量示范回答
“我会在调度前持久化带稳定 ID 的事件,并明确承诺至少一次投递。每次尝试都用事件 ID 和时间戳签名原始载荷;超时和 5xx 采用带抖动重试,终止性 4xx 进入死信。消费者在副作用事务内按事件 ID 去重。按租户分队列、熔断、年龄告警和可审计重放,让离线端点不会拖垮其他租户。”
常见误区
- 承诺 HTTP 恰好一次 → 崩溃会产生不确定结果 → 声明至少一次并要求消费者幂等。
- 签名解析后的 JSON → 等价格式可能验证失败 → 签名和校验原始字节。
- 所有 4xx 无限重试 → 永久错误消耗容量 → 分类错误并将终止情况送入死信。
- 使用全局单队列 → 单个租户可能拖垮全部任务 → 分区、限速并预留租户容量。
追问与回答
去重状态要保留多久?
至少覆盖自动重试和支持的重放窗口,并增加安全余量。若允许数月后重放,应保留紧凑事件账本,或明确要求消费者选择重放身份和保留策略。
重放应生成新的事件 ID 吗?
通常不应。业务事件身份保持稳定,投递尝试使用独立 ID 和审计记录。这样消费者能识别重放是同一事件,避免重复业务副作用。
消费者在副作用提交前返回 200 怎么办?
这是消费者契约问题,平台无法从响应推断真实成功。消费者应在持久接收后确认,使用 inbox 或事务性去重记录,并提供不确定结果的对账流程。