原理解说发布日期 阅读时间约 10 分钟

cron 的日期与星期是 OR 关系:第 2 个周一、月末、最后一个周五、隔周的正确写法

想表示“每月第 1 个周一 9 点”而写成“0 9 1-7 * 1”,结果除了每周一之外,每月 1 到 7 日每天都运行了。cron 中这个非常常见的失误,源于 cron 特有的规则:同时指定日期和星期时,只要符合“其中之一”就会运行。本文结合实际执行日期的示例,讲解日期与星期构成 OR 的机制及其例外,在 crontab 中正确运行第 n 个星期几、月末、最后一个周五、隔周的写法,以及 AWS EventBridge 和 Spring 中可用的 L、#、W 的写法。

同时写日期和星期,会在“其中之一”符合的日子运行

在 cron 表达式的 5 个字段中,分、时、月按“满足全部条件的时间运行”这种直观方式解读。但日期(第 3 个)和星期(第 5 个)比较特殊:两者都写了 * 以外的值时,会在“满足日期条件或星期条件之一的日子”运行。这是 Linux 的 cron(Vixie cron 及沿袭它的 cronie 等)一直以来的规范,GitHub Actions 和 Kubernetes 的 CronJob 也采用同样的解读方式。

例如本想表示“每月第 1 个周一 9 点”而写的“0 9 1-7 * 1”,会在“每月 1 到 7 日”或“每周一”的 9:00 运行。从 2026 年 10 月 12 日(周一)算起,实际执行日期如下。

执行日期符合的条件
10 月 12 日(周一)星期为周一
10 月 19 日(周一)星期为周一
10 月 26 日(周一)星期为周一
11 月 1 日(周日)日期为 1-7 日
11 月 2 日(周一)日期为 1-7 日,星期也是周一
11 月 3 日(周二)日期为 1-7 日

本来只想在第 1 个周一运行,结果每周一和每月最初 7 天都会运行,一个月 10 次左右。如果用这种写法设置例会提醒或月报发送,就会变成几乎每周都发通知的事故。

例外:任一方以 * 开头就变成“两者都满足”

更麻烦的是,Linux 的 cron 还有一个例外:日期或星期的值以 * 开头时,就不是 OR,而是 AND(两者都满足的日子)。像“*/2”这种以 * 开头的间隔指定就属于这种情况。

表达式解读方式运行的日子
0 10 1-31/2 * 2OR(其中之一)每个奇数日 + 每周二
0 10 */2 * 2AND(两者都满足)只有落在奇数日的周二

“1-31/2”和“*/2”都表示 1、3、5……31 日,但写法不同,执行日期就完全不同。列出 2026 年 10 月以后“0 10 */2 * 2”的执行日期,是 10 月 13 日、10 月 27 日、11 月 3 日、11 月 17 日、12 月 1 日……只有奇数日的周二。这个例外属于实现上的细节,换到其他调度器时解读方式也可能改变。与其刻意利用,不如记住“避免这种写法”更安全。

在 crontab 中运行第 n 个星期几的写法

crontab 没有直接表示“第 2 个周二”的写法。因此要利用这样一个性质:第 n 个星期几一定落在“(n−1)×7+1 日到 n×7 日”这 7 天之中。第 1 个是 1-7 日,第 2 个是 8-14 日,第 3 个是 15-21 日,第 4 个是 22-28 日。

  1. 1

    用日期字段限定为 7 天

    第 2 个周二 10:00 的话,把日期字段设为 8-14、星期字段设为 *,写成“0 10 8-14 * *”。如果星期写成 2 就会变成 OR,所以这里不写。

  2. 2

    在命令开头判断星期

    date +%u 返回以周一为 1、周日为 7 的星期数字。在 crontab 中转义 % 后写成“[ "$(date +\%u)" = 2 ] && /path/to/command”,只有周二才会执行后面的命令。

  3. 3

    用执行日期确认

    对 cron 来说它在 8 到 14 日每天都运行,所以要通过脚本日志或手动测试,确认命令确实只在第 2 个周二执行。

AWS EventBridge 和 Spring 的 @Scheduled 可以用 # 直接写。EventBridge 的星期数字从 1(周日)开始,所以第 2 个周二 10:00 是“cron(0 10 ? * 3#2 *)”,Spring 则是“0 0 10 ? * TUE#2”。按 EventBridge 的写法列出 2026 年 10 月以后的执行日期,是 10 月 13 日、11 月 10 日、12 月 8 日、1 月 12 日,只有每月的第 2 个周二。

每月最后一天、最后一个周五的写法(指定 31 日的陷阱)

把“每月最后一天做结算处理”写成“0 23 31 * *”是典型错误。没有 31 日的 2、4、6、9、11 月一次都不会运行。同样,指定 30 日在 2 月不会运行,而像“0 9 30 2 *”(2 月 30 日)这样指定不存在的日期,就会成为永远不运行的表达式。

想做的事crontabEventBridgeSpring
每月最后一天 23:000 23 28-31 * * + 月末判断cron(0 23 L * ? *)0 0 23 L * *
月末前 3 天0 9 25-28 * * + 日期判断cron(0 9 L-3 * ? *)0 0 9 L-3 * *
最后一个周五 18:000 18 22-31 * * + 星期和周的判断cron(0 18 ? * 6L *)0 0 18 ? * 5L
最后一个工作日0 18 26-31 * * + 工作日判断cron(0 18 LW * ? *)0 0 18 LW * *
离 15 日最近的工作日无法写出(在命令中判断)cron(0 9 15W * ? *)0 0 9 15W * *

在 crontab 中判断月末,要用“明天是 1 日,今天就是最后一天”的思路。使用 GNU 的 date(一般的 Linux)时,写成“0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/command”,在 28 到 31 日中只有最后一天会执行命令。最后一个周五则是把日期限定为 22-31,再确认“今天是周五”且“7 天后是下个月”(例:[ "$(date +\%u)" = 5 ] && [ "$(date -d '+7 days' +\%m)" != "$(date +\%m)" ] && 命令)。

EventBridge 的 L 表示“该月最后一天”,6L 表示“最后一个周五(6 是周五)”,15W 表示“离 15 日最近的工作日”。例如按 2026 年 11 月计算 15W,15 日是周日,所以会在次日 16 日(周一)运行。W 不会跨月,所以如果 1 日是周六,1W 会在 3 日(周一)运行。

隔周、两周一次只靠 cron 写不出来

“隔周周一”无法用 cron 的 5 个字段表示。日期字段的“*/14”只表示“1、15、29 日”,换月后又从 1 日重新计数,所以不是每两周一次。也没有周数字段,因此常规做法是在命令一侧判断。

  • 每周一运行,只在 ISO 周数为偶数的周执行:0 9 * * 1 [ $(( $(date +\%-V) \% 2 )) -eq 0 ] && 命令(%-V 用来去掉 08 这样开头的 0。带 0 的 08、09 在 shell 运算中会被当作八进制而报错)
  • 周数跨年时会从 52 或 53 回到 1,所以在有 53 周的年份,年末年初可能连续两周执行或空出三周
  • 想严格每 14 天执行一次,就计算距基准日的天数,只在能被 14 整除的日子执行(例:把与基准日 UNIX 时间的差除以 86400)
  • 由任务把上次执行日期记录到文件中,过了 13 天以上再执行,这种方法也很可靠

各种写法的区别汇总与确认方法

日期与星期的处理方式因使用 cron 的系统而异。同一个表达式移到别处后含义可能改变,所以先确认要用哪个系统运行。

系统同时写日期和星期时L、#、W
crontab(Linux)其中之一(OR);以 * 开头则两者都满足(AND)不可用
GitHub Actions与 crontab 相同不可用
Kubernetes CronJob与 crontab 相同不可用
AWS EventBridge不能两者都写,须把一方设为 ?可用
Spring @Scheduled两者都满足(AND)可用
systemd timer两者都满足(AND)有用 ~ 从月末倒数的写法

在 Spring 中写“0 0 9 1-7 * MON”,就直接表示“每月 1 到 7 日中的周一”,也就是第 1 个周一。按 crontab 的习惯来写意思正好相反,移植时要特别注意。EventBridge 的写法在EventBridge cron 表达式的写法中有详细讲解。

在 cron 表达式检查器中,同时指定日期和星期时会警告“会在符合其中之一的日子运行”,以 * 开头变成 AND 时也会提示。只指定 31 日导致有的月份不运行、指定不存在的日期导致一次都不运行之类的错误也能检测出来,并列出下次执行日期供你确认。crontab 的基本写法请看crontab 的写法与速查表,想让 GitHub Actions 按日本时间运行可以一并参考让 GitHub Actions 的 cron 按日本时间运行的方法。

常见问题

cron 怎样在每月第 2 个周一执行?
在 crontab 中把日期字段设为 8-14、星期字段设为 *,写成“0 9 8-14 * *”,并在命令开头判断是否为周一([ "$(date +\%u)" = 1 ] && 命令)。如果在星期字段写 1 就会变成 OR,每周一也会运行。EventBridge 可以直接写 cron(0 9 ? * 2#2 *),Spring 可以写 0 0 9 ? * MON#2。
cron 怎样在每月最后一天执行?
crontab 没有表示月末的写法,所以把日期字段设为 28-31 每天运行,并在命令中加上“明天是 1 日就执行”的判断([ "$(date -d tomorrow +\%d)" = "01" ] && 命令)。EventBridge 或 Spring 只要在日期字段写 L 就是月末。写 31 的话,没有 31 日的月份不会运行。
cron 的日期与星期为什么是 OR 而不是 AND?
这是早期 Unix cron 沿袭下来的规范,为了能用一行写出“每月 1 日和 15 日,再加上每周五也运行”这样的指定,规定同时写日期和星期时为 OR。出于兼容性,Linux 的 cron、GitHub Actions、Kubernetes 也沿用相同的解读方式。Spring 和 systemd 则是 AND。
cron 能隔周(两周一次)执行吗?
只靠 cron 表达式写不出来。可以每周运行,再在命令中判断周数的奇偶(看 date +\%V 除以 2 的余数),或者记录上次执行日期并确认是否已过 14 天。日期字段的 */14 只表示 1、15、29 日,并不是每两周一次。

本文介绍的工具

相关文章

使用指南约 10 分钟

让 GitHub Actions 的 cron 按日本时间运行:UTC 换算速查表与星期偏移、延迟的对策

在 GitHub Actions 的 schedule 里写了“0 9 * * 1-5”,工作流却在日本时间傍晚 6 点才运行。这并不是写错了,而是因为 GitHub Actions 的 cron 一律按 UTC(协调世界时)解读。本文整理了把日本时间(JST)换算成 UTC cron 表达式的方法和速查表、指定早上 0 点到 8 点时出现的“星期和日期退回前一天”问题、一行写不下时如何拆成两行,以及 schedule 延迟或不运行的原因与对策。

阅读文章 →
使用指南约 10 分钟

crontab 的写法与速查表:5 个字段与符号的含义、常用示例和注册命令

“每天 3 点备份”“每 5 分钟运行一次监控脚本”这类定时任务,在 Linux 上只要在 crontab 里写一行就能设置。不过“分 时 日 月 星期”这 5 个字段与 * , - / 这些符号的组合,刚开始很容易看错,常会出现 1 小时运行 60 次、本想每 7 分钟一次却出现 4 分钟间隔之类的事故。本文以示例为主,讲解 crontab 写法的基础和符号含义、可直接套用的速查表、crontab -e、-l、-r 的用法,以及编写命令部分时的注意事项。

阅读文章 →
使用指南约 10 分钟

EventBridge cron 表达式的写法:? 的含义、星期数字、日本时间的指定与从 crontab 改写

在 AWS 上定时运行 Lambda 或 ECS 任务时使用的 EventBridge cron 表达式,看起来和 Linux 的 crontab 很像,但字段数量、? 的用法、星期数字和时区都略有不同。按 crontab 的习惯写成“0 9 * * 1-5”会报错,直接照搬星期数字会差一天,时间按 UTC 解读又会差 9 小时,绊脚的地方接二连三。本文整理了 EventBridge cron 表达式的 6 个字段与 ? 的规则、星期数字、L、W、# 的用法、与 rate 表达式的区别、规则和 Scheduler 中日本时间的处理,以及与 crontab、Kubernetes、Spring、systemd 的改写对照表。

阅读文章 →