本文将详细介绍如何通过 Linux 原生工具 flock 实现进程锁,解决该 crontab 定时任务并发冲突的问题。

问题背景 @

在使用 crontab 配置高频定时任务(如每分钟执行一次)时,若任务本身执行耗时超过设定周期(如脚本运行时间超过 1 分钟),会导致前一个任务未结束,后一个任务已启动的并发问题,可能引发数据错乱、资源争抢等风险。

当 crontab 任务的执行周期 ≤ 任务实际耗时时,必然触发并发:

  • 例如:设置 */1 * * * * /home/test.sh(每分钟执行一次),但 test.sh 因处理大量数据需 2 分钟才能完成。
  • 后果:第 1 分钟启动的脚本在第 2 分钟仍在运行,而 crontab 会按规则在第 2 分钟再次启动新的脚本进程,两个进程同时操作相同资源(如文件、数据库),可能导致数据覆盖、死锁等问题。

解决方案 @

flock 使用介绍 @

flock 是 Linux 系统中用于管理文件锁的工具,通过对一个锁文件加独占锁,确保同一时间只有一个进程能持有该锁,从而阻止并发执行。

flock 基于文件描述符实现锁机制:

  • 当进程 A 通过 flock 对锁文件加独占锁后,其他进程(如进程 B)再尝试对该锁文件加独占锁时,会被阻塞或直接失败。
  • 进程 A 执行完毕后,会自动释放锁(无需手动解锁),后续进程才能获取锁并执行。

flock 支持通过 “文件描述符” 或 “锁文件路径” 两种方式加锁,日常用于 crontab 时,以 “锁文件路径 + 命令” 的方式最便捷,核心选项如下:

选项 长选项 功能说明 适用场景
-x --exclusive 获取独占锁(默认选项),同一时间仅一个进程可持有 绝大多数场景(如脚本执行需独占资源)
-s --shared 获取共享锁,多个进程可同时持有(仅读场景) 多进程需同时读取同一文件,不允许写入时
-n --nonblock 非阻塞模式:若无法立即获取锁,直接失败(不等待) 避免进程阻塞(如 crontab 每分钟执行,前一个未结束则直接跳过当前)
-w --timeout 阻塞模式:若无法立即获取锁,等待数秒后再失败 允许短暂等待前一个任务结束(如设置等待 30 秒)
-o --close 执行命令前关闭锁文件的文件描述符 防止命令产生的子进程继承锁,导致锁无法释放
-c --command 指定要执行的命令 / 脚本(与锁文件配合使用) 直接在 flock 命令中嵌入待执行脚本,无需单独写壳

crontab + flock 防并发 @

  1. 基础配置(推荐)

需求:每分钟执行 test.sh,若前一个脚本未结束,当前任务直接跳过(不阻塞)。

# crontab 任务配置(通过 crontab -e 编辑)
*/1 * * * * /usr/bin/flock -xn /var/run/test.lock -c '/home/test.sh'
  1. 进阶配置(带超时等待)

需求:若前一个脚本未结束,当前任务等待 30 秒,30 秒后仍无法获取锁则跳过。

# 等待 30 秒后失败,适合脚本执行时间波动较大的场景
*/1 * * * * /usr/bin/flock -xw 30 /var/run/test.lock -c '/home/test.sh'
  1. 特殊场景:脚本需生成子进程

test.sh 内部会启动子进程(如后台执行 nohup 命令),需加 -o 选项关闭锁文件描述符,避免子进程继承锁导致锁无法释放:

# -o 选项确保子进程不继承锁,防止锁残留
*/1 * * * * /usr/bin/flock -xno /var/run/test.lock -c '/home/test.sh'

关键注意事项 @

  1. 锁文件路径选择

    • 推荐放在 /var/run (运行时目录,系统重启后自动清空,避免残留无效锁文件)。
    • 若脚本为普通用户执行,需确保用户对锁文件路径有写权限(如 /home/xxx/run)。
  2. 避免锁文件残留

    • flock 会在进程结束后自动释放锁, 即使脚本异常退出, 内核也会回收锁, 因此无需担心死锁。
    • 若手动删除锁文件,已持有锁的进程仍能正常执行,新进程会重新创建锁文件并尝试获取锁。
  3. 脚本路径必须绝对

    • crontab 执行时的环境变量(如 PATH)与终端不同,flock、脚本、命令的路径均需写绝对路径(可通过 which 命令 查看路径,如 which flock、which bash)。
  4. 测试锁机制有效性

    • 手动执行 flock -xn /var/run/test.lock -c '/home/test.sh',同时在另一个终端再次执行相同命令,观察第二个命令是否直接退出(验证非阻塞效果)。