Cron 表达式怎么读:日与星期是「或」,以及下一次到底什么时候跑

· 6 分钟阅读 · cron crontab 定时任务 · English

一行 crontab 只有五个字段,但它每年制造的意外排得进运维工单前列:该跑的没跑、不该跑的跑了一天、“每周五 13 号"写不出来。原因基本都在三件事上——段的顺序、日和星期的合并规则、以及"下一次"是对着哪台时钟算的。

五段的顺序就是读的顺序

分钟  小时  日  月份  星期
0-59  0-23  1-31  1-12  0-7   (0 和 7 都是周日)

第一段是分钟,不是小时——绝大多数"我以为每天凌晨一点跑,结果它每天跑 60 次"都错在这。每段可以写单值、列表 1,15,30、范围 1-5、步长 */10 或 0-59/5;月份和星期还认三字母名称 JAN…DEC、SUN…SAT。剩下的合法内容只有别名(@daily、@weekly、@hourly、@reboot)和笔误。

6 段是前面多一列秒,7 段是末尾多一列年。这两种在 Vixie cron 和 Linux /etc/crontab 里都不存在,是 Quartz / Spring @Scheduled / EventBridge 的方言;同一天又出现 L(当月最后一天)、W(最邻近工作日)、#(第三个周五写 5#3),各家语义并不一致。先确认你面对的是哪种方言,再去解释这一行。

一、日和星期同时限制时是「或」

这是 cron 最反直觉的一条:当两段的日期列都做了限制,只要任一段匹配就执行,不是交集。

0 0 13 * 5      # 每月 13 号,外加每个周五

按直觉它像"恰好落在周五的 13 号”,实测排出来的下一次是 2026-10-09(周五)、10-13(周二)、10-16(周五)、10-23、10-30、11-06、11-13(这天既是 13 号也是周五)——两类的并集。往后数 366 天:52 个周五 + 12 个 13 号 − 2 天重叠 = 62 次,而不是你以为的"大概一年两次"。

反过来,只要其中一段是 *(或 ?),就完全由另一段决定。于是有个纯逻辑的推论:0 0 13 * 0-6 里星期段被写成 0–6(等于整周,仍算"有限制"),两段并起来覆盖每一天,它就变成每天跑。而"周五 13 号"这种需求写不进一行 crontab,只能 0 0 * * 5 触发后在命令体里自己 if [ "$(date +\%d)" = 13 ] 判一句。

二、*/5 是从零点对齐,不是"距上次五小时"

小时段写 */5 得到的是 00:00、05:00、10:00、15:00、20:00——步长是拿当前时钟和字段各段比对算出来的。cron 不记得上一次是什么时候跑的,所以任务跑慢了、跳过了一次,它也不会补,更不会有"上次成功"的概念。要"每 5 小时一次"这种相对节奏,得靠 systemd timer 的 OnUnitActiveSec= 或外部调度器。

同一个原因让 0 0 30 * * 表现出"每月最后一天不是 30 号就不跑"的怪样子:它匹配"每个月的第 30 天",2 月根本没有这天,所以 2 月直接缺席。想要"每月最后一天",5 段 cron 没有通用写法(L 是各家扩展),常见妥协是 0 0 28-31 * * 加一句"明天是不是 1 号"的判断。

三、0 0 29 2 * 下一次要等四年

按这台机器的日历从 2026-10-04 往后排:0 0 29 2 * 的下一次是 2028-02-29,再下一次是 2032-02-29。中间的 2029、2030、2031 都不存在 2 月 29 日——而如果表达式再刁钻一点,要撞上 2100 年,那也不是闰年(能被 100 整除但不能被 400 整除),2100 年 2 月只有 28 天,这一天要等到 2104。所以任何"下一次"的表都要写明它按哪个日历、哪个时区排的。想核对"那个日子离今天还剩几天",用日期间隔计算器算一遍,别手算跨闰年的那段。

四、夏令时那一小时:跳过是日历决定的,跑几次是实现决定的

用 ICU 日历实测(不是查文档,是数同一分钟能对上几个瞬间):

  • America/New_York,2026-03-08(周日)本地 02:30 对应 0 个瞬间——时钟从 01:59 直接跳到 03:00。所以 30 2 * * * 在那一天没有任何匹配的时刻,往下一天。
  • 同地 2026-11-01(周日)本地 01:30 对应 2 个瞬间(05:30Z 与 06:30Z)——秋天回调让这一小时出现两次。
  • Asia/Shanghai 两个探测点都只有 1 个瞬间:中国自 1991 年后不再实行夏令时,北京常年 GMT+8,所以挂在北京时间服务器上的 30 2 * * * 一年 365 次(闰年 366),不存在"跳过"或"重复"这一类问题。

日历只告诉你"这一小时不存在"或"存在两次";你的 cron 那天到底跑 0 次、1 次还是 2 次,是实现自己的策略,Vixie / cronie 的 crontab(5) 各写各的,权威在那台机器的手册页,不在任何网页或在线解析器。跨时区团队约好的"每天凌晨两点报表",如果那台机器在夏令时区,就必然有几天不准——这类需求应该写成 UTC 时刻,或者用带时区字段的调度器(cronie 这类实现支持在 crontab 里写 CRON_TZ=,Debian 自带的 cron 手册里没有它,别假设)。

五、“这一行没错但没执行"的排查清单

按命中概率排:

  1. 时区。 服务器时钟和你不是同一个(“说明明还有 3 小时,它已经跑过了"最常见就是这个)。先 date 看服务器,再对照表达式。
  2. CRLF。 用 Windows 编辑器改过的 crontab 每行末尾带 \r,轻则命令里多个看不见的字符,重则整行被拒绝。sed -i 's/\r$//' 或 dos2unix 过一遍。
  3. % 没转义。 在 Vixie/cronie 这一系里,命令里的 % 会被换成换行、第一个之后的内容变成命令的标准输入——写日期格式 date +%F 必须写成 date +\%F。这一条几乎人人踩一次。
  4. PATH 与相对路径。 cron 给你的环境是一个极简的 shell,~ 展开、PATH 里的用户目录都不一定在。命令写绝对路径,脚本里先 cd。
  5. 没有装载。 crontab -e 或 crontab file 才生效;直接编辑 /var/spool/cron/... 里的文件在多数实现上不会自动装载,/etc/cron.d/ 则要求文件里没有 : 和 .(含扩展名)且字段多一列用户名。
  6. 命令本身失败了。 任何解析器都看不出这个:把输出重定向到日志(>> /var/log/job.log 2>&1)再判断,邮件通知在多数精简系统上根本没配。

六、什么时候别用 cron

需要"错过就补跑"的用 systemd timer(Persistent=true)或外部调度器;需要"第 N 个工作日"“距上次 X 秒"的也一样——cron 的模型只有"当前时钟与各段比对”。跨机器的任务依赖(A 完再跑 B)不该靠时间对齐来实现。

顺手用在线 cron 解析对一遍:它把每一段展开成真正匹配的取值,明说这行按 5 段还是 6 段读,并用你这台机器的日历排出接下来 8 次实际时刻(连同用的时区、以及"这个表达式在短月份不匹配"的提示)。厂商扩展(L/W/#、步长 0)它只报那个读不懂的 token,不发明含义——这也是你的 crontab 该有的态度。

相关问题

0 0 * * 0 和 0 0 * * 7 一样吗? 一样,0 和 7 都是周日。@weekly 到底是哪一天? 各家实现是周日,但别名不是标准,别赌。

表达式会被传到别处吗? 不会。解析就是页面脚本里的字符串切分,时刻来自浏览器自己的日历,没有哪一步经过服务器,你输入的表达式不会离开这台设备。

为什么"每天 00:00"和"每小时第 0 分"看起来一样但次数不同? 0 0 * * * 一天一次,0 * * * * 一天 24 次——第一段是分钟,第二段是小时,把第二段写成 * 就从"0 点"变成"每小时”。

微信
微信
支付宝
支付宝