用 flock 防止 crontab 定时任务重叠执行:包装脚本与边界说明

举报
yd_246123679 发表于 2026/10/24 00:36:40 2026/10/24
【摘要】 crontab 任务执行时间超过调度间隔时可能并发运行。本文介绍如何用 util-linux 的 flock 对固定锁文件加建议锁,让同一任务在任意时刻只有一个实例;给出包装脚本、crontab 配置、本地并发验证方法,并说明 -n/-w、锁继承、NFS、容器、锁文件残留等边界。

用 flock 防止 crontab 定时任务重叠执行

问题场景

crontab 按固定时间点触发命令。如果某次任务执行时间超过调度间隔,下一次仍会启动,导致同一任务并发运行。比如每 5 分钟执行一次数据同步,某次同步因数据量增大跑了 12 分钟,就会出现两三个进程同时写同一张表、同一份文件或同一个队列。并发可能带来数据竞争、重复处理、资源耗尽,甚至把磁盘或连接池打满。

crontab 本身不提供“上一次未结束则跳过”的语义。要避免重叠,需要在任务入口加互斥。文件锁是常用做法:所有实例先抢同一把锁,抢到才执行,抢不到直接退出或等待。

为什么用 flock

Linux 上 util-linux 提供的 flock 命令可以对文件或文件描述符加建议锁。它使用 flock(2) 系统调用,锁与打开的文件描述符关联,进程退出后锁自动释放。相比自己创建 pid 文件,flock 不需要处理进程崩溃后的残留锁文件,也不依赖进程名判断。只要锁文件路径固定,所有任务实例都遵守同一约定,就能实现互斥。

需要注意,flock 是建议锁,不是强制锁。所有参与方都必须主动调用 flock 才能互斥;如果某个脚本绕过锁直接运行,仍可能并发。

最小包装脚本

假设有一个耗时任务,原始命令是 /opt/app/bin/sync_data.sh。我们不把原始命令直接写进 crontab,而是写一个包装脚本 /opt/app/bin/run_with_lock.sh:

#!/usr/bin/env bash
set -euo pipefail

LOCK_FILE=/var/lock/app-sync.lock
LOG_PREFIX='[app-sync]'

# 以读写方式打开锁文件,文件描述符 9
exec 9>"$LOCK_FILE"

# -n 表示非阻塞:拿不到锁立即退出,不等待
if ! flock -n 9; then
    echo "$(date '+%F %T') $LOG_PREFIX 已有实例在运行,本次跳过" >&2
    exit 0
fi

echo "$(date '+%F %T') $LOG_PREFIX 开始执行" >&2

# 在这里调用真正的任务
/opt/app/bin/sync_data.sh

echo "$(date '+%F %T') $LOG_PREFIX 执行结束" >&2

注意:exec 9>"$LOCK_FILE" 会创建或打开锁文件。flock -n 9 尝试对 fd 9 加排他锁。如果失败,说明已有进程持有锁。抢到锁后,后续命令在同一 shell 进程中运行,fd 9 一直保持打开,直到脚本退出,锁自动释放。子进程也会继承 fd 9,因此即使 sync_data.sh 启动后台子进程,只要包装脚本没有退出,锁仍然有效。但若 sync_data.sh 自己 daemonize 并让包装脚本退出,锁会释放,互斥范围就只覆盖到包装脚本退出为止。这是边界之一。

crontab 配置

将 crontab 中原来的任务替换为包装脚本。例如每 5 分钟执行一次:

*/5 * * * * /opt/app/bin/run_with_lock.sh >> /var/log/app-sync.log 2>&1

如果不想单独写包装脚本,也可以直接用 flock 命令:

*/5 * * * * /usr/bin/flock -n /var/lock/app-sync.lock -c '/opt/app/bin/sync_data.sh >> /var/log/app-sync.log 2>&1'

这种写法适合简单命令。注意 -c 后面的整条命令要作为一个参数,内部有引号时要正确转义。更复杂的逻辑建议用包装脚本,便于维护和测试。

运行方法与验证

  1. 给包装脚本执行权限:
chmod +x /opt/app/bin/run_with_lock.sh
  1. 先手动运行一次,确认能正常执行:
/opt/app/bin/run_with_lock.sh
  1. 模拟并发:打开两个终端,先运行一个长时间任务,再运行另一个。例如让任务 sleep 30:
/opt/app/bin/run_with_lock.sh
# 在另一个终端
/opt/app/bin/run_with_lock.sh

第二个实例应输出“已有实例在运行,本次跳过”并退出。也可以查看锁文件是否被占用:

lsof /var/lock/app-sync.lock

或

fuser -v /var/lock/app-sync.lock
  1. 安装到 crontab:
crontab -e

加入上面的定时行,保存后可用 crontab -l 查看。

边界与限制

锁文件位置:不要放在 /tmp 等可能被系统清理的目录,也不要用会被任务自己删除的路径。建议使用 /var/lock 或 /run/lock 下按应用命名的文件。运行用户需要对锁文件有读写权限,否则打开失败会导致任务无法启动。

-n 与等待:-n 表示拿不到锁立即退出,适合“跳过重叠”的语义。如果希望排队等待,可以用 -w 秒数,例如 flock -w 10 9,等待最多 10 秒;超时后仍拿不到锁再退出。等待会占用一个进程,若任务间隔短、执行久,可能堆积等待进程,需要评估。

退出码:flock -n 拿不到锁时返回非 0。包装脚本里用 if ! flock -n 9; then ... exit 0 可以避免 cron 发送失败邮件,或按需改成 exit 75 等自定义码。若任务本身失败,应保留其退出码并向上传递,例如:

if ! /opt/app/bin/sync_data.sh; then
    echo '任务失败' >&2
    exit 1
fi

这样 cron 能通过邮件或日志感知失败。

锁的继承与子进程:flock 锁与打开的文件描述符关联。如果包装脚本执行任务时启动后台进程,并且包装脚本很快退出,锁会随 fd 关闭而释放,后台进程不再受保护。若任务内部会 fork 长时间运行的后台进程,应确保互斥范围覆盖整个业务生命周期,或让包装脚本等待后台进程结束。

网络文件系统:flock 在本地文件系统上行为明确,但在 NFS 等网络文件系统上的支持取决于实现和挂载选项。如果多台机器需要互斥,不能简单假设 flock 跨主机可靠,应使用分布式锁或数据库锁。

容器与 cron:容器内可能没有 flock 命令,需要安装 util-linux 或改用其他语言实现文件锁。容器重启后 /var/lock 可能丢失,但锁文件丢失本身不是问题,重新创建即可;如果目录不存在,脚本会失败。可以在脚本开头 mkdir -p 锁文件所在目录。

锁文件残留:flock 的锁在进程退出后自动释放,锁文件可以长期保留,这是正常设计。不要在任务开始时删除锁文件,否则会破坏互斥:一个进程可能持有旧 inode 的锁,另一个进程创建新文件并加锁,两者会同时运行。

crontab 环境:cron 的环境变量很少,PATH 可能不包含命令路径。脚本中最好使用绝对路径,或显式设置 PATH。重定向日志时也要确保目录存在且可写。

最后,flock 只解决“同一任务不重叠”的问题。如果任务本身不是幂等的,或者多个不同任务会修改同一资源,还需要在业务层做幂等、事务或更细粒度的锁。把锁、日志、退出码和告警一起考虑,定时任务才更可靠。

小结

用 flock 包装 crontab 任务,可以用较小改动避免同一任务并发执行。核心是固定锁文件路径、所有实例先抢锁、抢不到按策略退出或等待,并注意锁的继承、NFS、容器和日志等边界。包装脚本比直接在 crontab 里写长命令更易维护,也方便本地模拟并发测试。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。