延迟任务系统设计总结文档
本文总结了一套高可用、高并发、支持秒级~天级延迟任务的系统设计方案。内容来源为一次系统性设计讨论,包含用户的完整设想、关键提问、追问细节,以及对应的深入分析和解答。适用于大厂级系统设计面试、或延迟任务平台的落地实践参考。
🧩 1. 系统目标与背景
目标:
- 支持亿级任务调度能力,每天约2亿延迟任务;
- 支持秒级~7天的延迟任务精度;
- 精准触发,误差不超过1秒;
- 支持任务修改/取消、失败重试;
- 保证消息不丢、幂等执行。
架构分层(用户初步设想):
- MySQL 作为任务持久化层,按创建时间分库分表;
- Redis 为调度中间层,短期任务提前1小时预热入队;
- Worker 层负责定时触发任务并发消息;
- ExecuteWorker(业务执行方)实时消费消息;
- 支持热数据7天维护,冷数据归档。
🏗️ 2. 任务链路设计细节
2.1 MySQL 持久化层
- 创建任务时即写入数据库,支持事务;
- 建立
execute_time
索引(或执行窗口字段); - 每张表约1000w记录,支持水平扩展;
- 定期归档历史任务(>7天)数据;
✅ 反问:
多个 worker 并发扫描是否会重复扫同一任务?
回答: 可通过扫描分片(库+表+id区间)策略避免重复,任务表结构上需标记状态,辅助状态过滤。
2.2 Redis 中间层(调度层)
- 每小时扫描一次未来一小时任务入 Redis;
- Redis 按时间段或分片进行
ZSET
划分; - 使用
ZPOPMIN
或ZRANGEBYSCORE + ZREM
实现原子弹出;
✅ 反问:
多个 worker 并发消费同一个 Redis ZSET,会有线程安全问题吗?是否需要先查再删?
回答: 使用 ZPOPMIN
(Redis >=6.2)或 MULTI + ZRANGEBYSCORE + ZREM
保证原子性,无需担心线程安全问题。
2.3 Worker 本地触发队列
- 多 worker 拉取任务后入本地缓冲队列;
- 使用**时间轮(TimeWheel)**实现毫秒级精度调度;
- 也可使用小顶堆(min heap)+ 阻塞机制,但控制精度较麻烦;
✅ 用户感悟:
原本以为从 MySQL 到 Redis 再到本地队列可以天然有序,但分片 + 多 worker + Redis 多 key + 并发拉取会打乱顺序。每层都需要局部排序。Redis 靠 ZSET;worker 靠 min heap 或时间轮。
🔁 3. 幂等性与重复执行问题
3.1 ExecuteWorker 执行重复场景:
- 消息重复发出(同一任务被多 worker 扫描)
- 消费者重复拉到同一消息(多副本订阅)
- SETNX 被提前 DEL 掉,导致幂等失效
✅ 推荐方案:
- 执行任务前:使用
SET key NX EX 60
建立幂等保护窗; - 执行后:不立即 DEL,让 Redis 自动过期防止瞬时重复执行;
- 附带任务 ID + 执行 ID,便于日志追踪;
📤 4. 消息投递与状态一致性问题(系统最难点)
情景:
worker 发出 MQ 消息后,更新任务状态失败; 下一次扫描未见状态更新,又发一次相同消息 → 重复执行风险
✅ 反问:
发完 MQ 更新失败,这时如果靠补偿机制去扫事务表重发,是不是太重了?或者补偿不够及时又会造成任务重复执行。
📌 解法对比与建议:
方案A:标记调度中状态
- 先更新任务状态为
DISPATCHING
- 然后发 MQ
- 发成功 → 标记为
DISPATCHED
- 发失败 → 标记
DISPATCH_FAILED
优点:无需本地事务表;worker 层避免重复发
方案B:本地事务表 + 补偿扫描
- 写消息事务表记录;
- MQ 发成功后状态更新失败 → 补偿任务定期拉取重发
缺点:系统复杂度高,仅用于金融/支付等强一致场景
方案C:MQ 成功 + 更新失败立即重试一次 + 失败交给补偿器
- 补偿器间隔 10s 内跑一次,配合限流
- 幂等执行保障覆盖少量重复投递风险
推荐作为默认策略,工程性价比最优
⚖️ 5. 总结建议
- 多层解耦架构(调度 vs 执行)是合理且推荐的高可用设计;
- 每层需处理自己的局部顺序性,不能指望天然全链路有序;
- Redis 中建议使用
ZPOPMIN
或多命令原子组合消费; - Worker 层推荐使用时间轮调度器组织本地执行;
- 幂等性必须放在 ExecuteWorker 最后一层做兜底,SETNX+EX 是工程化通解;
- 消息发出与状态更新间的弱事务需通过“调度中标记+幂等执行+补偿器”协同保障最终一致性;
- 本地事务表仅在确实强一致需求场景中使用。