场景五 · 任务调度
高并发场景 · Scenario 05

场景五:分布式定时任务调度

凌晨 1 点,千万个"闹钟"同时响起:还款提醒、订单超时关闭、对账批处理、报表生成……调度系统要保证每个闹钟不重复响、不漏响,还要把活儿均匀摊给一群打工机器。

整点脉冲 不重不漏 时间轮 幂等兜底
千万级待调度任务总量
10万+/秒触发峰值(整点脉冲)
不重不漏调度语义底线
秒级触发精度要求
📑 本页目录
01

场景描述与业务特征

定时任务分两大类:固定周期任务(每天凌晨跑对账、每 5 分钟同步库存)和海量单次延迟任务(每笔订单下单后 30 分钟未支付自动关闭——1000 万笔订单就是 1000 万个独立闹钟)。前者数量少但任务重,后者数量恐怖但单个轻。

类比:公司行政订了一个"每天 9 点全员打卡提醒"(周期任务),同时每个员工还各自设了"会前 10 分钟提醒"(延迟任务)。行政的难点是大扫除那天活儿太重要分工;个人闹钟的难点是数量太多,不能给每个闹钟雇一个人盯着

🕐 整点脉冲

Cron 表达式天然喜欢整点(0 0 * * *),导致凌晨0点/1点出现人为制造的调度洪峰——自己给自己造了个"秒杀"。

👥 集群去重

任务服务部署 10 个实例,"每天发一次账单"绝不能发 10 次——分布式环境下"谁来执行"必须有共识。

📮 不能漏

执行到一半机器宕机、调度中心重启期间到期的任务——漏跑一次还款扣款,就是资损事故

⚖️ 分片均衡

"给 5000 万用户发推送"这种大任务,要切成分片摊给整个集群;切得不匀,就会一台累死、九台围观。

🔀 依赖编排

报表任务要等对账任务先完成——任务间有 DAG 依赖,上游失败下游怎么办,都是调度语义的一部分。

📈 触发精度分层

订单关单差 5 秒无所谓,但优惠券整点生效差 5 秒会被投诉——不同任务对精度的要求差异巨大,可分级对待。

02

整体运作流程

主流架构是"调度中心 + 执行器集群"分离:调度中心只负责"看表掐点"(谁到期了、派给谁),执行器只负责"干活"。海量延迟任务则常用时间轮延迟消息专门处理——不为每个闹钟雇人,而是雇一个"敲钟人"每秒看一格。

🧠 调度中心(主备) 扫描到期任务(DB索引) 选主:只有Leader派活 路由策略:轮询/分片/故障转移 失败重试 · 超时告警 ⚙️ 执行器 A 分片0:处理 uid%3==0 ⚙️ 执行器 B 分片1:处理 uid%3==1 ⚙️ 执行器 C 分片2:处理 uid%3==2 📋 注册中心/DB 执行器心跳注册 · 任务表 · 执行日志 🎡 海量延迟任务专用通道 时间轮 / RocketMQ延迟消息 / Redis ZSet(score=到期时间) 订单关单等千万级"小闹钟" 🏭 业务逻辑 对账 · 推送 · 关单 幂等键防重复执行 执行结果回报调度中心 时间轮 指针每秒走一格, 只处理当前格子挂的任务; 千万任务 O(1) 触发, 不用给每个闹钟开线程 核心思想:调度与执行分离;周期任务走"调度中心",千万级延迟小闹钟走"时间轮/延迟消息"专用通道
🧠
时间轮为什么快?朴素做法:每秒扫描全表"WHERE 到期时间 <= now"——千万任务扫到天荒地老。时间轮把任务按到期时间挂到环形格子上,指针走到哪格才看哪格,触发复杂度 O(1)。Netty、Kafka、Dubbo 的内部延迟任务全是这么玩的。
03

核心瓶颈与数据一致性挑战

👯 重复执行:最常见的翻车

调度中心派活后没收到 ACK(网络抖动/执行器GC),触发重试——但其实第一次已经执行了。还款扣两次、短信发两条。解法:调度侧"分布式锁+状态机"防重派,业务侧幂等键兜底(双保险缺一不可)。

🕳️ 漏触发:静悄悄的事故

调度中心主节点宕机、主备切换的 30 秒里到期的任务无人问津。解法:启动后补扫(捞出"应触发而未触发"的任务补跑)+ 任务表带状态字段可对账——漏跑要能被发现,更要能被补救。

⚖️ 分片倾斜:一台累死九台围观

按 uid%10 分片发推送,但大客户的数据集中在某几个分片——数据分布不均导致执行时长差 10 倍。解法:更细粒度的动态分片(任务队列模式:干完领下一批),代替静态取模。

🌊 调度洪峰自伤:DB 被自己人打挂

凌晨 0 点 5000 个 Cron 同时开跑,全都猛查业务库——调度系统自己制造了秒杀现场。解法:错峰(加随机抖动 offset)、分级(非关键任务错开整点)、对下游限流。

💥
事故现场还原:某金融平台调度中心升级重启 3 分钟,期间 1.2 万笔"还款日扣款"任务未触发,且无补扫机制——第二天用户集体逾期、征信受影响,赔偿+公关损失惨重。此后行规:资金类任务必须"可对账、可补跑、有监控",三者缺一不上线
04

优化手段、获益与代价

① 调度中心选主 + 执行器分离 架构

调度中心多实例部署但只有 Leader 派活(DB乐观锁/ZooKeeper选主),执行器无状态水平扩容。

✅ 获益:调度不重复、执行可扩容;调度中心与业务解耦,升级互不影响。
⚠️ 代价:主备切换窗口存在触发延迟;多一套系统要部署运维。

② 分片广播(静态/动态) 扩展

大任务广播给所有执行器,每台按分片参数处理自己那份(index/total);进阶版用"任务队列"动态领取。

✅ 获益:千万级数据的批处理时长随机器数线性下降;动态领取天然抗倾斜。
⚠️ 代价:业务代码要感知分片逻辑;某分片失败的"部分重跑"逻辑要自己设计。

③ 时间轮 / 延迟消息处理海量小闹钟 核心

订单关单类"一单一闹钟"任务不进调度中心,改用 RocketMQ 延迟消息 / Redis ZSet 轮询 / 内存时间轮。

✅ 获益:千万级延迟任务触发成本趋近 O(1);调度中心只管几千个周期任务,压力骤降。
⚠️ 代价:RocketMQ 开源版只有固定延迟级别(18档);Redis ZSet 方案要自己处理轮询与竞争;内存时间轮重启丢任务需持久化配合。

④ 幂等键 + 状态机 一致性

每次执行携带唯一执行ID,业务表加"执行状态"字段:INIT→RUNNING→DONE,重复请求直接短路返回。

✅ 获益:无论调度层怎么重试、网络怎么抖,业务效果恰好一次;对账有据可查。
⚠️ 代价:每个任务都要设计幂等逻辑(无法框架代劳);状态表数据量随执行次数增长需归档。

⑤ 错峰 + 随机抖动 防自伤

给 Cron 加随机偏移(0点0分 → 0点0~15分随机),非关键任务主动避开整点;对下游 DB 设并发上限。

✅ 获益:削掉人为制造的整点洪峰,下游数据库夜里不再"心律不齐"。
⚠️ 代价:任务完成时间不确定性增大;有严格时点要求的任务(优惠券生效)不能参与错峰。
🛡️
降级策略:① 调度中心故障 → 执行器本地兜底 Cron 接管核心任务(提前配置);② 下游过载 → 非关键任务自动延后(任务分级:资金类 > 通知类 > 报表类);③ 积压严重 → 丢弃可丢任务(如营销推送),保住资金类任务的执行窗口。三角站位:一致性(不重不漏)优先,触发精度可适度松动。
05

多种解决方案对比与选型建议

方案任务规模不重不漏保障运维成本适用场景
单机 Cron / Spring @Scheduled
最朴素方案
几十个无(多实例会重复)单体应用、非关键任务
@Scheduled + 分布式锁
Redis/DB锁抢执行权
百级防重但不防漏小集群过渡方案
XXL-Job
调度中心+执行器 · 国内主流
万级失败重试+路由策略中 · 需部署调度中心中大型业务的标准答案
PowerJob / DolphinScheduler
支持DAG工作流编排
十万级强 · 支持依赖重跑中高任务间有复杂依赖/大数据ETL
延迟消息/时间轮
RocketMQ延迟/Redis ZSet
千万级单次任务MQ可靠投递+幂等订单关单等海量"一次性闹钟"
🎯
选型口诀:单体应用 @Scheduled 就好别折腾;服务多实例了先加分布式锁止血;任务上百、要监控要重试,直接上 XXL-Job(部署半天就能跑);有 DAG 依赖选 PowerJob/DolphinScheduler;海量订单关单这种"一单一闹钟",别塞进调度中心,走延迟消息才是正道。两类任务分流处理,是这个场景最重要的架构直觉。
06

推演检验:换个条件还成立吗?

背下架构图不算学会,能判断"条件变了方案还成不成立"才算。先自己推,再展开参考。

推演 1:如果调度中心整体宕机 10 分钟后恢复,这 10 分钟里错过的定时任务怎么办?全部补跑一遍对吗?

💡 参考推演

不存在统一答案,这就是 misfire 策略要按任务语义选:对路同步这种"只要最新一次"的任务,补一次即可(中间错过的丢弃);对每小时对账这种"每次都有意义"的任务,要逐次补齐;对报表推送这种过时就没用的,直接跳过。前提是任务幂等——补跑两遍不能出两份错账。恢复策略不是框架配置项,是业务决策。

推演 2:如果凌晨跑批任务从 10 分钟涨到 3 小时(数据量涨了 20 倍),架构要跟着变吗?

💡 参考推演

两个问题会先后爆发:① 单机 3 小时可能撞上早高峰——上分片广播,把数据按 ID 取模切给 N 台机器并行跑,3 小时压回十几分钟;② 如果任务周期比执行时长还短(每小时调一次但跑 3 小时),还要选阻塞策略:串行等待、丢弃后续还是覆盖前一次?任务变重从来不只是"跑慢点",它会把调度重叠、资源争抢这些隐藏问题一并引爆。

推演 3:如果我的小网站每天只有 100 单,订单超时关单还需要延迟消息吗?扫表轮询不行吗?

💡 参考推演

完全行,而且更好:每分钟扫一次订单表,100 单的表扫描毫无压力,方案简单到不会出错、不依赖任何中间件。延迟消息是为"千万级订单 + 秒级精度"准备的,小体量上它反而多了一个故障源。方案没有高级与低级,只有与体量匹配与否——这和选型口诀里"单体别折腾"是同一个直觉。