Webman多实例定时任务重复执行的根本原因是Crontab为本地调度、无跨机协调能力,必须用Redis分布式锁解决:key需含时间维度(如task:sync_order:20260522),value须唯一标识上下文(如getmypid().'_'.uniqid('', true)),加锁用SET NX EX原子命令,解锁必须用Lua脚本校验value后删除,且所有实例须连接同一Redis实例。

Webman 定时任务在多实例部署时重复执行,根本不是配置写错了,而是没意识到 workerman/crontab 本身不带分布式协调能力——每个 worker 进程都独立解析、注册、触发自己的定时器,只要进程数 > 1,且任务配置在共享进程(如 config/process.php 中统一注册),就必然重复。
为什么加 count => 1 也拦不住重复?
很多人试过把进程数设成 1,结果线上 Docker 部署多个容器、或 K8s 启动多个 Pod,还是重复执行。这是因为:
-
count => 1只控制单机内该进程启动几个副本,对跨机器/跨容器完全无效 - Webman 的
Crontab是纯本地事件循环调度,不感知其他机器上的同名任务 - 常见误判:看到日志里“同一秒出现两条记录”,第一反应是本地进程重复,实际是三个容器各自跑了一次
必须用 Redis 分布式锁,但 value 和 key 设计有坑
锁不是加了就行,value 写死或没区分上下文,等于没锁:
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
-
$value必须唯一标识当前执行上下文:推荐拼接getmypid().'_'.uniqid('', true),不能只用uniqid()(多 worker 下可能撞) -
$key要含时间维度:比如任务叫sync_order,就用task:sync_order:20260522或task:sync_order:hour_20,避免不同周期任务互相干扰 -
$ttl必须大于任务最大预期耗时(含 DB 查询、HTTP 调用、网络抖动),宁可设长,别设短;设短了锁自动释放,别的实例就趁虚而入
解锁必须用 Lua 脚本,别信 GET + DEL 两步走
直接 $redis->del($key) 是高危操作,GET 和 DEL 之间存在竞态窗口:
- A 拿到锁,执行慢超时,Redis 自动删 key
- B 在 A 超时后抢到锁,开始执行
- A 此时刚好执行完,调用
del—— 删的是 B 的锁 - 后果:后续所有请求都绕过锁,数据错乱
- 正确解法:
$redis->eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", 1, $key, $value)
定时任务去重的真正入口不在进程文件,而在任务逻辑块
很多团队把锁加在 php webman task:run --name=xxx 的命令入口,这是错的:
- 命令行是新 PHP 进程,
uniqid()和pid完全不可控,锁 key 失效 - 真正要保护的是“处理数据”那段代码,比如
Db::table('orders')->where('status', 'pending')->update(...) - 锁必须在真正查库/写库前加,在操作完成后立即释放,中间不能有长耗时非关键逻辑
- 所有实例必须连同一个 Redis 实例(注意云 Redis 的 DB 编号、集群分片、读写分离配置)
最易被忽略的一点:锁的粒度和业务节奏必须匹配。按天跑的任务,key 带日期就够了;但每 10 秒轮询一次 Redis 状态的任务,key 得精确到秒,否则同一秒内多个实例仍会并发进入。

















