定时任务基础:cron表达式详解、时间调度核心概念

说实话,做运维这些年,我见过太多因为定时任务写错导致的事故。凌晨三点把全量备份跑到了白天业务库上,或者周日的维护窗口因为cron表达式少了一个空格,硬生生提前了1小时执行——嗯,这些都是真实案例。今天咱们就把cron表达式这块硬骨头彻底啃透。

cron表达式长什么样

cron表达式本质上就是一个时间模式串。在Linux系统中,它包含5个字段:分钟、小时、日、月、星期。我习惯用一个口诀记:“分时日,月周”。

# 标准格式
* * * * * command
│ │ │ │ │
│ │ │ │ └─ 星期 (0-7, 0和7都表示周日)
│ │ │ └─── 月份 (1-12)
│ │ └───── 日期 (1-31)
│ └─────── 小时 (0-23)
└───────── 分钟 (0-59)
字段 允许值 通配符
分钟0-59* / , -
小时0-23* / , -
日期1-31* / , - ? L W
月份1-12 或 JAN-DEC* / , -
星期0-7 或 SUN-SAT* / , - ? L #

小提示:日期和星期字段不能同时用数字指定,否则容易产生歧义。建议只用一个,另一个用 ? 表示无限制。我一般只在星期字段用 *,日期字段留 ?。

常用表达式速查

  • 0 2 * * * — 每天凌晨2点整执行
  • */15 * * * * — 每15分钟执行一次
  • 0 0 1 * * — 每月1号0点执行
  • 0 0 * * 1 — 每周一0点执行
  • 0 1-5 9 * * — 每天9点到9点5分之间每分钟执行
  • 0 23,0 * * * — 每天23点和0点各执行一次

你想想看,如果我们要在每周五晚上做全量备份,表达式该怎么写?当然是 0 22 * * 5。但我记得有次同事写成了 0 22 * * 0,结果周日凌晨跑了备份——因为0在星期里代表周日。

避坑指南

我曾经踩过一个坑:在cron表达式里写 0 */2 * * *,以为会每2小时执行一次。实际上这个表达式是 每2小时的第0分钟执行,也就是0:00, 2:00, 4:00……但如果服务器在00:01才启动,那第一次执行会延迟到2:00。更安全的做法是用 0 0 */2 * *,但注意这表示每个偶数日期的0点执行,不是每2小时。真正每2小时应该写成多个时间点,比如 0 0,2,4,6,8,10,12,14,16,18,20,22 * * *,或者用系统级的timedatectl控制。

关键点:*/N 在分钟字段上表示每N分钟,但在小时字段上表示每N小时,但跨天会重置。如果想精确控制周期,我建议用 0,30 这种列举方式,可读性更强。

时间调度核心概念

搞清楚了cron表达式,咱们还得理解背后的时间调度逻辑。说白了,调度器的核心只有四个概念:周期、固定时间点、窗口、延迟与偏差。

  1. 周期 (Period):两次执行之间的固定间隔,比如每5分钟、每小时。cron里的 */5 就是周期调度。
  2. 固定时间点 (Fixed Time):在每天的特定时刻执行,不考虑上次执行时间。比如每天的2:00。
  3. 窗口 (Time Window):一个允许执行的时间范围,比如“只允许在 23:00-06:00 执行”。巡检策略里经常用到。
  4. 延迟与偏差 (Delay & Skew):为了避免多个任务同时爆发,在基准时间上加一个随机偏移。我负责过一个1000台集群的日志收集,如果每台都在整点执行,网络就炸了。加上 RANDOM_MINUTE 偏移后,成功率从50%提到了98%。
概念 cron表达式示例 实际含义
周期*/10 * * * *每10分钟一次,从整点开始
固定时间点0 3 * * *每天凌晨3:00整
窗口0 0 * * 6 + 脚本内检查时间周六全天执行,但脚本只允许晚上23点后
延迟与偏差0 */12 * * * + 程序内sleep(random(0,600))每12小时执行一次,但实际时刻随机偏移0-10分钟

实战建议:对于巡检类任务,我推荐固定时间点+随机偏差的组合。比如每天凌晨2:00巡检,但每个节点延时0-300秒,这样能避免所有节点同时打爆监控接口。代码里加个 sleep(random.uniform(0, 300)) 就行。

cron表达式解析流程

来看看一个cron表达式从输入到生成任务的全过程。我画了个简图,你跟着箭头走一趟就明白了。

cron表达式解析与任务调度流程 cron 表达式字符串 字段拆解与验证 分钟 小时 日 月 星期 → 通配符展开 当前时间与所有通配符匹配? 执行任务 跳过,等待下一分钟 调度器每分钟检查一次,匹配则执行,否则等待下一分钟继续检查

这张图揭示了核心逻辑:cron引擎每分钟唤醒一次,拿当前时间与表达式里的所有通配符集合做比较。如果全部命中,就拉起任务。你可能会问:“那如果服务器在这一分钟刚好卡住怎么办?” 嗯,这就是为什么大型巡检系统很少直接依赖cron,而是用分布式调度框架(比如xxl-job、Quartz)做补偿。

我自己的双保险设计

最后分享一个我自己在用的cron写法。针对重要的巡检定时任务,我会同时配置两条cron:

  • 主任务:标准时间点,比如 0 2 * * *
  • 兜底任务:主任务执行后5分钟,检测是否成功,失败则重试。兜底的cron写 5 2 * * *,脚本里检查标志文件。

这样就算主任务因为系统负载高延迟了,兜底也能接住。记住,定时任务不是写完就完了,得靠运维经验给它上保险。