cron表达式真是个神奇的东西看起来就几个星号加斜杠真要写对却没几个同事能一次搞定。尤其是“每N秒”“每N分钟”“每N小时”这种高频需求网上答案五花八门抄错了也不知道问题出在哪。我翻了翻手头的项目发现大部分定时任务的坑都出在表达式写法和框架差异上今天索性把这块彻底捋清楚。1. cron表达式的基础认知五段、六段还是七段在动手写任何表达式之前得先把cron表达式的“规格”搞明白。不同平台采用的cron标准并不完全一样最直观的差别就是字段数量。1.1 标准cron格式速览平时用得最多的格式是六段式秒 分 时 日 月 周Linux系统自带的crontab用的是五段式没有秒分 时 日 月 周Quartz框架在六段基础上还支持可选的年就是七段式秒 分 时 日 月 周 年不少云平台和分布式任务调度系统也是七段式可以指定到年份和最后一个周的偏移量。各字段的取值范围字段取值范围说明秒0-59有些实现支持0-59有些实现不支持秒分0-59时0-23日1-31注意月份天数差异月1-12 或 JAN-DEC周0-7 或 SUN-SAT0和7都代表周日部分实现有差异年留空或1970-2099不常用1.2 特殊字符的含义与使用边界cron表达式的灵活性主要靠几个特殊字符撑起来*匹配字段的任意值比如“每分钟”就是每一分钟都匹配?仅在“日”和“周”两个字段中使用表示“不指定具体值”避免日和周同时约束造成的冲突-范围例如10-12表示从10到12,枚举例如1,15,30表示第1分、第15分、第30分/步长例如*/5表示每5个时间单位L最后的含义例如L在日字段表示当月最后一天在周字段表示周六W最近的工作日仅日字段使用#第几个周几例如3#2表示第二周的周三C结合日历值的含义实际很少用?和*的区别经常被追问。*表示“每”这个概念而?表示“我不关心这个字段”。举个例子0 0 12 ? * *和0 0 12 * * ?在大多数实现里行为等价但如果在日和周两个字段同时写具体值比如0 0 12 1 * 1每月1号且又是周一有些实现会报错或行为不一致所以规范做法是日和周只使用一个来指定。1.3 举一个完整的例子0 */5 9-18 * * MON-FRI意思是工作日周一到周五的上午9点到下午18点之间每5分钟执行一次。拆开看秒0整分触发分*/5每5分钟时9-18在9点到18点这个时间范围内月*任何月份周MON-FRI周一至周五这个表达式在工作日看报表任务里很常见。注意“时9-18”的含义是9:00到18:59之间每5分钟都会跑实际业务需求常常只想跑到18:00就得写成0 0 18单列一个表达式或者把时字段拆开处理。2. 三种高频需求每N秒、每N分钟、每N小时的写法拆解标题里最核心的其实就是“每N秒分小时执行一次”这个写法。这部分单独展开讲因为里面的细节特别多少写一个0或者多写一个/效果天差地别。2.1 每N秒怎么处理最标准的写法*/5 * * * * *这表示每5秒执行一次。类似的*/10 * * * * *每10秒*/30 * * * * *每30秒。这里有个容易忽略的问题*/N在秒字段上的含义是从0秒开始每隔N秒取一次。如果N不能被60整除比如每7秒或每25秒那么每个周期内的时间点是不均匀的实际执行时刻会“错位”。*/7在0到59秒内实际匹配的是0、7、14、21、28、35、42、49、56然后跳回0。也就是说每轮执行间隔基本上是7秒但在56到下一次0秒之间只隔了4秒59到下一轮0秒。在长时间运行下这个误差很难被感知但如果你在日志里精确看时间戳就会发现分布不均匀。如果你的需求不是“自然秒序列均匀分布”而是“从第N秒开始每隔M秒执行一次”可以用范围加步长的组合比如从第10秒开始每5秒一次10-59/5 * * * * *表示10到59秒范围内每5秒触发一次。对于秒级任务还要考虑执行时间本身。如果任务执行耗时超过设置间隔就会产生堆积或重叠。一般要配合任务框架的“不重叠执行”机制比如在任务入口加锁或使用单线程调度器。2.2 每N分钟怎么写标准写法0 */N * * * *例如每5分钟0 */5 * * * *每15分钟0 */15 * * * *前面的0是秒字段表示在分钟的0秒触发避免在分钟中间某个随机秒上执行。如果漏掉这个0写成* */5 * * * *就会在每5分钟的窗口内每一秒都执行一次效果变成每分钟执行60次这显然不是本意。从“整分触发”的控制粒度想更细一点比如要在每分钟的第30秒跑一次30 * * * * *这就是每分钟的第30秒执行不是每30秒执行一次两者完全不同。2.3 每N小时怎么写标准写法0 0 */N * * *例如每2小时0 0 */2 * * *每6小时0 0 */6 * * *在小时字段使用*/N时同样存在“从0点开始每隔N小时”的语义。0 0 */6 * * *实际执行时间是0点、6点、12点、18点。如果你希望任务从凌晨2点开始每6小时跑一次可以这样写0 0 2-20/6 * * *含义是2点到20点之间每6小时实际是2点、8点、14点、20点。小时字段还有一种常见写法是多个枚举值比如0 0 1,3,5 * * *每天1点、3点、5点执行这个表示法在排班和定时拉取中特别常用。2.4 范围类“每N”的特殊处理有时候业务上希望“每天凌晨1点到5点之间每半个小时执行一次”。如果写成0 0/30 1-5 * * *注意小时字段范围是1到5分钟字段的0/30是“从0分开始每30分”那么这个表达式的实际执行时刻是1:00、1:30、2:00、2:30……5:00。看起来没问题但如果期望的是1:00、1:30、2:00这种标准写法确实就是这样。Quartz的0/30与*/30在大多数实现中等价但Spring的Scheduled在这两种写法上解析规则基本一致。为了减少歧义我建议统一用*/N而不是0/N来表达“从0开始每隔N”把0/N留给真正需要显式指定起始位置的场景。3. 日常工作里高频使用的cron表达式及解读这里列一个自己项目里高频在用的速查表基本涵盖了95%以上的场景。每个表达式都附上了适用场景说明直接抄也行。场景表达式说明每分钟执行0 * * * * *秒字段为0整分触发每5分钟执行0 */5 * * * *缓存刷新、轻量数据同步每30分钟执行0 */30 * * * *状态检查每小时执行0 0 * * * *整点任务每2小时执行0 0 */2 * * *数据聚合每天凌晨执行0 0 0 * * *日志清理、备份每天固定时刻执行0 30 2 * * *每天凌晨2:30执行每周一凌晨执行0 0 0 * * MON周报生成每月1号凌晨执行0 30 0 1 * *月报、账单工作日每15分钟0 */15 9-18 * * MON-FRI工作时段内轮询每月最后一天执行0 0 0 L * *月末统计月末倒数第三天0 0 0 L-3 * *注意只有部分实现支持L-n格式3.1 速查表之外的几个特殊写法“每个工作日的中午12点和下午6点”可以合并0 0 12,18 * * MON-FRI“每月的第5个周五”在Quartz里可以写0 0 0 ? * 5#5意思是周字段为周五且是该月第5个周五如果这个月没有第5个周五就不执行。注意这种写法日字段必须用?。“每季度第一个月的第一天执行”写起来比较啰嗦我是直接用三个表达式拼的0 0 0 1 1,4,7,10 *这三种写法在面试和协作中很容易遇到别慌对照字段拆开看就明白了。3.2 为什么秒字段在多数场景下不用六段式cron的秒字段功能很强大但大多数业务任务根本不需要秒级触发。秒级任务放大到整个系统里如果多个任务同时踩秒跑会产生一种“整点冲击”打满CPU或数据库连接池。我见过某系统因为凌晨0点整堆积了几十个定时任务一起跑导致数据库瞬时负载飙高。如果你的任务不是对时效敏感的类型建议错峰设置比如0点5分、0点10分、1点30分。排定时任务有个小技巧固定秒位置为0分钟位置错峰比如10分钟以内多个任务分别设0 1/10、0 2/10、0 3/10避免在同一分钟扎堆。4. 同样是cron不同调度框架的“方言”差异很多人不知道把同一个表达式从Linux crontab搬到Quartz或Spring里结果可能完全不同这不是表达式写错了而是框架对语法的实现有差异。4.1 Linux crontab五段式不支持秒Linux系统自带的crontab是五个字段且最小精度是分钟没有秒字段分 时 日 月 周例如每5分钟*/5 * * * * commandLinux crontab里的/步长工作和Quartz基本一致但要注意两个方面一是周和日同时设置时的逻辑与其他实现不同Linux的crontab在“周”和“日”任一匹配即可OR关系而Quartz默认是AND关系除非日字段用?二是不支持表达式动态预判需要自己用crontab命令验证。想实现Linux下每30秒执行一次的任务原生的crontab无法直接做到。可以写一个小循环脚本* * * * * for i in $(seq 1 30); do command; sleep 1; done这种方式等于每分钟内循环执行30次简单粗暴但脚本自身耗时会叠加实际间隔比预定值偏大。更可控的做法是改用systemd timer它能精确到秒级且支持日历事件的复杂表达。4.2 Quartz六段或七段式支持年Quartz是Java生态使用最广的任务调度库它的cron格式支持秒和年0 0/30 9-18 ? * MON-FRI注意上面表达式中日字段用了?这是为了避免日和周同时约束。Quartz里还有一个重要的配置叫Misfire错过触发策略。当调度器长时间停机或任务被阻塞导致错过触发时间Quartz不会补上所有错过的调度而是根据任务设置的misfire策略决定行为比如MISFIRE_INSTRUCTION_FIRE_ONCE_NOW表示立即补偿执行一次DO_NOTHING则直接跳过。我在一个订单超时检查任务里踩过这个坑调度器因发布重启停止了5分钟重建后发现所有错过的触发全部立即补偿导致几千条超时消息瞬间发出。解决方式是使用DO_NOTHING策略每次调度时只处理当前窗口内的数据。4.3 Spring Scheduled六段式秒级可用Spring从4.3开始支持cron表达式中的秒位Scheduled的cron默认是六段式Scheduled(cron 0 */5 * * * *) public void refreshCache() { // 每5分钟刷新缓存 }使用中有几个容易踩的地方时区问题Scheduled默认使用服务器默认时区。如果你的服务器是UTC时区而业务在北京时间每天0点执行的任务会在早上8点才跑务必显式配置时区。线程池问题Scheduled默认使用单线程调度器如果前一个任务运行时间超过间隔后面所有任务都会排队。业务量上来后记得使用TaskScheduler并配置自定义线程池。不支持动态修改Scheduled的cron在应用启动时解析一次运行期间改注解属性不生效。需要动态修改定时策略的要么用SchedulingConfigurer注册动态cron的Trigger要么把任务抽到独立组件中管理。4.4 分布式任务调度系统的cron基本都是七段式像xxl-job、ElasticJob这类分布式调度系统前端录入的cron用的是Quartz标准七段式年可省略。这类系统自己的几个特点秒字段支持完整常用于实现每5秒或每10秒的极短周期任务分片广播和路由策略不在cron表达式的范畴内需要额外配置时区以调度中心的服务器时区为准调度中心与执行机之间的时间差会影响触发准点率如果你服务器的系统时间误差较大记得用NTP同步一下否则表达式再准也没用。5. 调试cron表达式的方法论在线工具、计算脚本、日志验证一个都不能少表达式写完了怎么确认下一次性就能跑在预期的时刻我的习惯是“三层验证法”先用在线工具算一遍再写一个小脚本或命令验证最后看日志确认。5.1 在线工具的边界常用工具crontab.guru最直观支持5字段格式输入表达式会翻译成一段英文描述Quartz的在线生成器搜索“cron maker”就能找到支持6字段和7字段Spring官方文档中的cron示例在线工具适合快速检查语法但有两个局限只验证格式和常规语义不验证框架的特殊扩展比如L-3或#5这类不加载你的业务时区结果展示按浏览器时区来会有偏差5.2 自己算下一次执行时间写代码时可以用主流语言的cron库来计算表达式下一次执行时间。Java生态里可以用CronExpressionQuartz自带CronExpression cronExpression new CronExpression(0 */5 * * * *); Date next cronExpression.getNextValidTimeAfter(new Date()); System.out.println(下一次执行时间: next);Python里用croniterfrom croniter import croniter from datetime import datetime cron croniter(0 */5 * * * *, datetime.now()) next_time cron.get_next(datetime) print(f下一次执行时间: {next_time})在调度框架中调试时直接在单元测试里断言“下一次执行时间”往往比手工看日志快得多。我习惯把核心任务的cron表达式写成配置项然后加一个单元测试跑未来24小时所有触发点检查是否有落在预期窗口之外的情况。5.3 日志验证的三个要点线上任务第一次上线我基本都会在任务入口打日志重点记录三个东西当前时间戳精确到毫秒配置的cron表达式实际触发时间与理论触发时间的时间差只记录“任务执行了”这一条日志是不够的因为你要判断的是“调度准不准”而不是“任务跑没跑”。如果发现实际触发时间与预期相差固定间隔多半是时区问题如果相差不固定且整体延迟优先检查任务执行时间是否超过了执行周期导致下一次触发被阻塞或堆积。5.4 几个必踩的坑日字段与周字段的互斥。写表达式时如果日字段和周字段都指定了具体值在不同框架里要么报错要么行为诡异。遇到这种需求先用?放弃一个字段比如“每月1号且是周一”这种本身就比较绕干脆拆成两个表达式。秒级任务的高延迟陷阱。每5秒执行一次的表达式配合3秒的接口调用如果任务本身是同步执行且没有加锁你看到的实际触发间隔会逐渐偏离设定值。处理方法是任务方法内部用异步线程池执行调度线程只负责投递任务。cron表达式与节假日无关。cron是纯日历调度它不会“知道”今天是法定节假日。需要跳过节假日的任务只能在任务逻辑里判断或者依赖调度平台提供的日历功能。测试时不要用真实的短周期。每5秒执行一次的任务直接上线观察日志量会很大。合适的方式是把表达式下载到本地用假时钟模拟或者临时把*/5改成*/15验证链路确认无误后再改回正式周期。最后分享一个使用习惯我始终会在项目的配置中心放一个“表达式自查表”每个定时任务都配上上次修改时间、修改人、期望触发逻辑说明。cron表达式的语义密度很高看起来就几十个字符如果没有说明三个月之后再回来看大概率要重新推演一遍。表达式本身不需要注释但它周围一定得有上下文说明。