如何防止 cron 任务重复(并发)执行?
任务执行时间超过调度间隔会导致并发重叠。标准解法:flock -n 锁文件——上一个没跑完,新的直接跳过。本文给出用法、脚本内置锁与应用内幂等设计。
标准解法:用 flock——* * * * * /usr/bin/flock -n /tmp/job.lock /path/to/job.sh。它给锁文件加排他锁:上次任务没跑完锁还在,新一轮直接跳过(-n 非阻塞),跑完自动释放。执行时长超过调度间隔的任务都必须加这层保护。
flock 用法
* * * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
-n:拿不到锁立即退出(不加则等待,可能越堆越多)- 锁文件应位于该账户控制且路径稳定的目录;不要在运行期间删掉/重建锁文件,避免不同进程锁住不同 inode。/run 重启后的目录需重新创建
- 想记录"被跳过":默认锁冲突退出码为 1,但任务自身也可能返回 1;可用 -E 指定专用冲突码,并与任务退出码约定区分,可包一层记日志;同时记录执行标识、开始和结束时间。持续跳过时,对比任务耗时与调度间隔,检查是否需要优化任务。
脚本内置锁(自带防重入)
不想依赖 crontab 写法,把锁写进脚本:
#!/bin/bash
exec 200>/tmp/myjob.lock # 打开文件描述符 200
flock -n 200 || { echo "$(date) 上一个实例还在跑,跳过"; exit 1; }
# 正常任务逻辑
好处:怎么调用(cron、手动、其他调度器)都受保护。
其他可选方案
| 方案 | 说明 |
|---|---|
| systemd timer | OnCalendar 触发的 service 默认不并发(上一个没结束不启新实例) |
| 应用内分布式锁 | 共享数据库/协调系统锁需正确处理租约、续期、持有者验证及 fencing;不能仅 SETNX 就保证分布式互斥 |
| PID 文件自检 | 脚本开头查 PID 文件有效性,老派写法 |
多机场景的特别提醒
使用本机锁文件的 flock 只防本机并发;共享文件系统的锁语义另需验证。同一个任务在多台的 crontab 里都配了(高可用部署常见),要用 Redis/数据库级的分布式锁,或让任务只在 leader 节点跑;也可集中调度;K8s CronJob 的 concurrencyPolicy: Forbid 只约束同一个 CronJob 的 Job,不是跨所有调度器的全局锁。
常见问题(FAQ)
Q:flock 的锁文件要定期清理吗?
A:不用。锁是加在打开的文件句柄上的,文件本身留着无妨;放 /tmp 或 /run 重启自清。
Q:任务卡死了锁永远不释放?
A:当持有该锁的所有打开文件描述符都关闭时才释放;子进程可能继承描述符,父进程退出不一定立即解锁。但任务本身卡死要另外加超时(timeout 300 job.sh)。
Q:想"排队执行"而不是"跳过"?
A:去掉 -n 会等待;-w 限制的是等待锁时间,不是任务运行时长;注意大量堆积的风险,一般跳过+告警更合理。