可靠性

那次惨痛的教训

2024 年 4 月 1 日(愚人节,但对我们来说不是玩笑),我们的延时队列服务发生了一起严重事故。

事故经过

时间线:

14:00 - 用户 A 下单,创建 30 分钟后取消订单的任务
14:05 - 用户 A 支付成功,取消超时任务
14:30 - 延时队列服务重启(部署新版本)
14:31 - 用户 A 收到订单被取消的通知 🤯

根因分析

经过排查,问题出在这里:

问题根源:

  1. ❌ 任务只存储在内存,无持久化
  2. ❌ 服务重启后,任务全部丢失
  3. ❌ 取消任务只是从内存删除,没有记录
  4. ❌ 重启后,任务不存在,但用户的期望还在

可靠性的三个维度

1. 任务不丢失(No Loss)

任务创建后,无论发生什么情况(服务器崩溃、网络故障、停电),都不能丢失。

2. 任务不重复(No Duplicate)

同一个任务不能被多次执行。如果执行失败需要重试,要有明确的控制。

3. 任务不遗漏(No Miss)

到期的任务必须被执行,不能因为任何原因被遗漏。

可靠性三角
    不丢失
      / \
     /   \
    /_____\
   不遗漏 不重复

任务不丢失的保障

方案 1:双层存储(数据库 + 内存)

双层存储的优势:

层级存储优点缺点
L1: 数据库磁盘✅ 强持久化
✅ 事务保证
✅ 可靠性高
⚠️ 性能较低
L2: Redis内存✅ 高性能
✅ 快速查询
⚠️ 需要持久化配置

方案 2:WAL(Write-Ahead Log)

使用写前日志确保数据不丢失:

方案 3:消息队列持久化

使用 Kafka 等持久化消息队列:


任务不重复的保障

幂等性设计

确保同一个任务即使被多次调用,结果也是一样的。

三层幂等检查


任务不遗漏的保障

补偿机制

定期检查是否有遗漏的任务:

心跳机制

消费者定期发送心跳,证明自己还活着:


想一想

思考 1

如果消费者在执行任务时挂了,如何保证任务能被重新执行?

参考答案

问题分析:

消费者在执行任务时挂了,会导致:

  1. 任务状态停留在 processing
  2. 任务没有完成
  3. 其他消费者不知道该任务需要重试

解决方案:心跳 + 超时检测

关键要点:

  1. ✅ 记录心跳时间:heartbeat_at 字段
  2. ✅ 定期检测死任务:心跳超时 > 30 秒
  3. ✅ 重置任务状态:从 processing 改回 pending
  4. ✅ 增加重试次数:记录失败次数