最稳妥方案是系统crontab触发+命令行指令封装+Redis分布式锁+业务幂等设计四层协同:用Linux内核级调度替代think-crontab,通过原子set加锁、Lua脚本解锁、唯一token、含时间维度key,并在数据层用精确where、行锁、唯一索引等实现幂等。

ThinkPHP6 定时任务防止重复执行,核心不是“加个判断就行”,而是要应对真实生产环境中的多节点、多进程、进程中断、锁过期等复杂场景。最稳妥的做法是:系统 crontab 触发 + 命令行指令封装 + Redis 分布式锁 + 业务幂等设计,四层协同。
用系统 crontab 替代 think-crontab
think-crontab 这类扩展本质是 CLI 进程内轮询或 HTTP 模拟触发,没有进程守护、无法防崩溃、不支持并发控制——它本身就会导致重复执行。生产环境唯一可靠方案是交由 Linux 内核级调度器管理:
- 在 app/command/ 下定义标准命令类(如
cron:sync-order),configure()中显式设名,execute()写纯业务逻辑 - crontab 条目必须带三要素:
cd /project/root、/usr/bin/php绝对路径、APP_ENV=production - 示例:
0 3 * * * cd /var/www/myapp && APP_ENV=production /usr/bin/php think cron:sync-order >> /var/log/sync-order.log 2>&1
加 Redis 分布式锁(原子 set + Lua 解锁)
即使只配了一条 crontab,当项目部署在多台服务器(或单机多实例 Docker)时,每台都会执行该命令——必须靠分布式锁确保同一时刻仅一个节点真正干活:
- 加锁必须一步到位:
$redis->set('lock:sync_order', $token, ['nx', 'ex' => 300]),['nx', 'ex' => N]是 ThinkPHP 6.x Redis 驱动唯一支持的原子写法 -
$token必须全局唯一(推荐uniqid('', true)),不能用固定字符串或时间戳 - 解锁必须用 Lua 脚本校验 value 一致性:
if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end - 锁 key 建议含时间维度,如
sync_order_20261029,避免跨天任务被旧锁阻塞
业务逻辑自身做幂等处理
锁只能防并发,防不了用户刷新重提、网络超时重试、脚本手动多跑等串行重复。必须在数据层兜底:
立即学习“PHP免费学习笔记(深入)”;
- 清缓存类任务可直接调
Cache::clear(),天然幂等 - 状态更新类任务加精确 where 条件,例如:
Order::where('status', 'pending')->where('created_at', '>=', strtotime('-2 hours'))->update(['status' => 'processing']) - 涉及金额、库存等关键操作,先查后更或用数据库行锁(
for update)+ 版本号字段 - 对外发通知类任务,记录发送日志表并建唯一索引(如
order_id + event_type)
配套运维保障不可少
再好的逻辑,没日志、没权限、没监控,出问题就抓瞎:
- 所有 crontab 输出必须重定向到日志:
>> /var/log/cron-task.log 2>&1,别用/dev/null -
runtime/目录确保 crontab 执行用户(如www-data或root)有读写权限:chmod -R 775 runtime/ - 在命令类
execute()中返回明确状态码:成功返回0,失败返回非零值(便于 crontab 捕获告警) - 定期检查锁 key 是否异常残留,可用
redis-cli keys "lock:*"辅助排查



















