4. 调度频率设计:Cron表达式陷阱与动态调度策略
调度频率,说白了就是「任务多久跑一次」。
很多人觉得这有什么好设计的?写个 Cron 表达式不就完了?
嗯,我以前也这么想。直到有一次线上事故——凌晨三点,几百个任务同时涌上来,数据库连接池直接被打爆。那晚我盯着报警短信,才意识到:Cron 表达式看着简单,坑是真不少。
4.1 Cron 表达式的常见陷阱
Cron 表达式是调度系统的「通用语言」。但越是通用,越容易踩坑。
4.1.1 整点并发风暴
我见过最典型的场景:
- 任务A:
0 0 * * * ?(每小时整点执行) - 任务B:
0 0 * * * ?(也是每小时整点) - 任务C:
0 0 * * * ?(还是每小时整点)
几十个任务全挤在 00:00 这一秒触发。服务器瞬间压力爆表。
解决方案: 给每个任务加一个随机偏移量。
// 不推荐:整点触发
0 0 * * * ?
// 推荐:随机秒级偏移
// 任务A:第15秒触发
15 0 * * * ?
// 任务B:第37秒触发
37 0 * * * ?
// 任务C:第52秒触发
52 0 * * * ?
你想想看,每个任务错开几十秒,压力就分散了。这个习惯我后来一直保留着。
4.1.2 跨天与跨月边界
Cron 表达式在处理跨天、跨月时,容易出逻辑问题。
| 场景 | 错误写法 | 正确写法 | 说明 |
|---|---|---|---|
| 每月最后一天 | 0 0 31 * ? |
0 0 L * ? |
2月没有31号,任务会跳过 |
| 每周最后一个工作日 | 0 0 0 * 5 |
0 0 0 ? * 5L |
普通写法只匹配周五,不是最后一个 |
| 每季度第一天 | 0 0 0 1 1,4,7,10 ? |
建议用代码计算 | Cron 无法表达「季度」概念 |
4.1.3 时区与夏令时
这个坑我印象特别深。有一次部署到海外服务器,任务本该每天凌晨 2 点执行。结果夏令时切换那天,任务要么没跑,要么跑了两次。
原因很简单:Cron 表达式默认使用服务器本地时区。夏令时切换时,凌晨 2 点可能变成 3 点,或者出现两个 2 点。
避坑指南:
- 统一使用 UTC 时区配置 Cron
- 在应用层做时区转换
- 避免在夏令时切换窗口(凌晨 1-3 点)安排关键任务
4.2 动态调度策略
固定 Cron 表达式只能解决「定时执行」的问题。但真实业务场景中,调度频率往往是动态的。
4.2.1 基于负载的自动调节
我记得有个数据同步任务,平时每小时跑一次就够了。但大促期间数据量暴增,需要缩短到 5 分钟一次。
手动改 Cron 表达式?太慢了。而且大促结束后还得改回来。
更好的做法: 让调度系统根据负载自动调整频率。
// 伪代码:动态调整调度间隔
public class DynamicScheduler {
private int currentInterval = 60; // 默认60分钟
public void adjustInterval() {
long queueSize = getQueueSize();
if (queueSize > 10000) {
// 队列积压,缩短间隔
currentInterval = 5;
} else if (queueSize < 100) {
// 队列空闲,延长间隔
currentInterval = 60;
}
// 更新调度器
scheduler.reschedule(currentInterval);
}
}
4.2.2 基于时间的多级频率
有些任务在不同时间段的执行频率不同。比如:
- 白天(8:00-22:00):每 5 分钟执行一次
- 夜间(22:00-8:00):每 30 分钟执行一次
- 周末:每 1 小时执行一次
用单个 Cron 表达式很难表达这种逻辑。我一般会拆成多个调度规则:
// 多级频率配置示例
[
{
"timeRange": "08:00-22:00",
"weekdays": "1-5",
"cron": "0 */5 * * * ?"
},
{
"timeRange": "22:00-08:00",
"weekdays": "1-5",
"cron": "0 */30 * * * ?"
},
{
"weekdays": "6,7",
"cron": "0 0 * * * ?"
}
]
调度系统在运行时,根据当前时间匹配对应的规则。这样既灵活,又容易维护。
4.2.3 事件驱动的触发
定时调度有个天然缺陷:它不知道数据什么时候准备好。
举个例子:上游系统每天凌晨 3 点生成报表文件。但有时候生成得早(2:30),有时候生成得晚(4:00)。
如果 Cron 写死 3:00 执行,要么等不到文件,要么文件还没写完。
我建议的做法: 用事件驱动替代定时轮询。
- 上游系统生成完文件后,发送一个 MQ 消息
- 调度系统收到消息后,立即触发任务
- 同时保留一个兜底的定时任务(比如每 30 分钟检查一次)
4.3 调度频率设计全景图
下面这张图,是我总结的调度频率设计核心逻辑。你可以对照着检查自己的系统。
4.4 避坑清单
最后,我整理了一份调度频率设计的避坑清单。你可以直接拿来当检查表用。
- 整点并发: 所有任务不要用相同的整点 Cron,至少错开秒级或分钟级
- 边界日期: 每月最后一天、每周最后一天,用
L或代码计算,别硬写数字 - 时区问题: 统一用 UTC 配置 Cron,应用层做时区转换
- 夏令时: 避免在凌晨 1-3 点安排关键任务
- 固定频率: 考虑用动态调度替代固定 Cron,适应业务波动
- 事件驱动: 能用事件触发就别用定时轮询,实时性更好
- 兜底机制: 事件驱动一定要配合定时兜底,防止消息丢失
调度频率设计,说白了就是「在正确的时间,用正确的频率,做正确的事」。Cron 表达式是工具,不是目的。别让工具限制了你的设计思路。