ThinkPHP 6.0 任务调度需用 Redis 分布式锁防重,锁键全局唯一并含业务标识,结合客户端 token 和 Lua 脚本安全加解锁;Redis 不可用时降级为文件锁;crontab 需用 flock/lockf 避免系统级并发。

ThinkPHP 6.0 任务调度(Crontab)分布式锁实现【防重】需要解决多台服务器同时拉起同一计划任务导致的重复执行问题,例如定时同步库存、批量推送消息或清理缓存时出现数据错乱或资源争抢。
用 Redis 分布式锁拦截并发任务启动
该方法适用于部署在多台服务器上的 ThinkPHP 6.0 应用,所有节点共用同一套 Redis 实例,通过原子加锁确保同一时刻仅有一个节点真正执行任务。
第一步:在任务指令类的 execute() 方法开头获取 Redis 实例并构造锁键
【锁键必须全局唯一且含业务标识】例如:'crontab:sync_stock_daily',避免不同任务间误删锁。
立即学习“PHP免费学习笔记(深入)”;
第二步:生成客户端唯一 token,防止误删他人锁
第三步:调用 $redis->set($lockKey, $token, ['nx', 'ex' => 30]) 原子加锁,过期时间设为任务预估最大执行时长的 1.5 倍(如任务通常耗时 12 秒,则设 30 秒)。
第四步:判断返回值——true 表示成功持锁,继续执行业务逻辑;false 直接输出日志并退出,不抛异常也不重试。
用 Lua 脚本安全释放锁
这一步不可跳过。若直接用 $redis->del($lockKey),当任务 A 持锁超时自动释放后,任务 B 拿到锁正在运行,此时任务 A 执行完 del 操作会误删任务 B 的锁,引发后续冲突。
方法一:内联 Lua 脚本解锁
定义脚本:"if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"
执行:$redis->eval($script, [$lockKey], [$token]);
检查返回值是否为 1,是则解锁成功;为 0 说明锁已不属于当前客户端,无需处理。
结合文件锁做本地单实例兜底
当 Redis 不可用时,该方案可防止同一台服务器上多个 PHP 进程并发执行同一指令,属于降级保护。
在指令类 execute() 开头打开一个固定路径的锁文件,例如 /runtime/lock/sync_stock.lock
调用 flock($fp, LOCK_EX | LOCK_NB) 尝试非阻塞加锁
加锁失败立即 exit(0),不记录错误日志(避免刷屏),也不尝试连接 Redis
加锁成功后,在 finally 块中显式调用 flock($fp, LOCK_UN) 并 fclose($fp)
配置 crontab 时绕过系统级并发陷阱
操作系统级 crontab 不感知 PHP 层锁状态,若任务执行慢于设定间隔(如每分钟跑一次但实际耗时 90 秒),就会堆积进程。必须从调度层压制。
方法1:用 flock 包裹命令(推荐)
* * * * * /usr/bin/flock -xn /tmp/tp6_sync_stock.lock -c "cd /var/www/project && php think sync:stock > /dev/null 2>&1"
方法2:用 lockf 替代(部分 Alpine 镜像无 flock)
* * * * * /usr/bin/lockf -s -t0 /tmp/tp6_sync_stock.lock /usr/bin/php /var/www/project/think sync:stock > /dev/null 2>&1
【-t0 表示超时为 0,即不等待,立刻失败退出】这是关键参数,避免阻塞后续调度周期。



















