系统设计之延时任务系统(schedulerX)

Posted by BX on Wed, May 14, 2025

延迟任务系统设计总结文档

本文总结了一套高可用、高并发、支持秒级~天级延迟任务的系统设计方案。内容来源为一次系统性设计讨论,包含用户的完整设想、关键提问、追问细节,以及对应的深入分析和解答。适用于大厂级系统设计面试、或延迟任务平台的落地实践参考。

🧩 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 划分;
  • 使用 ZPOPMINZRANGEBYSCORE + 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:标记调度中状态

  1. 先更新任务状态为 DISPATCHING
  2. 然后发 MQ
  3. 发成功 → 标记为 DISPATCHED
  4. 发失败 → 标记 DISPATCH_FAILED

优点:无需本地事务表;worker 层避免重复发

方案B:本地事务表 + 补偿扫描

  • 写消息事务表记录;
  • MQ 发成功后状态更新失败 → 补偿任务定期拉取重发

缺点:系统复杂度高,仅用于金融/支付等强一致场景

方案C:MQ 成功 + 更新失败立即重试一次 + 失败交给补偿器

  • 补偿器间隔 10s 内跑一次,配合限流
  • 幂等执行保障覆盖少量重复投递风险

推荐作为默认策略,工程性价比最优

⚖️ 5. 总结建议

  • 多层解耦架构(调度 vs 执行)是合理且推荐的高可用设计;
  • 每层需处理自己的局部顺序性,不能指望天然全链路有序;
  • Redis 中建议使用 ZPOPMIN 或多命令原子组合消费;
  • Worker 层推荐使用时间轮调度器组织本地执行;
  • 幂等性必须放在 ExecuteWorker 最后一层做兜底,SETNX+EX 是工程化通解;
  • 消息发出与状态更新间的弱事务需通过“调度中标记+幂等执行+补偿器”协同保障最终一致性;
  • 本地事务表仅在确实强一致需求场景中使用。