Webman 无开箱即用分布式定时器,Timer::add 和 workerman/crontab 均因无跨进程协调机制导致多实例重复执行;需用 Redis 锁或独立单进程 Scheduler 实现单点调度,并注意时钟同步、Redis 高可用及协程阻塞问题。

Webman 本身没有开箱即用的“分布式定时器”,直接用 Timer::add 或 workerman/crontab 必然在多进程/多实例下重复执行,且无法保证单点触发 —— 这不是配置问题,是设计边界。
为什么 Timer::add 和 Crontab 都不能直接用于分布式场景
两者都依赖本地 Worker 进程的事件循环,没有任何跨进程协调机制:
-
Timer::add在每个子进程里独立注册,Worker::$processCount = 4就会执行 4 次,且返回的 ID 只在当前进程有效,Timer::del($id)对其他进程完全无效 -
workerman/crontab虽支持 6 位表达式,但仍是单进程内串行回调;若部署 3 台 Webman 实例,同一任务就会每分钟跑 3 次 - 所有定时逻辑都在内存中,进程重启后全部丢失,不持久、不可追溯、无法手动干预
- 没有失败重试、无执行记录、无超时控制 —— 出了问题只能看日志盲猜
用 Redis + Lua 实现带锁的单点调度
核心思路:让所有实例竞争一个 Redis 锁,抢到的实例才真正执行任务,其余跳过。关键不是“谁先跑”,而是“谁有资格跑”。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 用
SET key value NX EX 30命令争锁,过期时间设为任务预期最大耗时(如 30 秒),避免死锁 - 锁 value 必须唯一(建议用
$_SERVER['HOSTNAME'].':'.getmypid()),方便排查和强制释放 - 执行前先
GET key校验自己是否仍持锁,防止任务执行超时后被别的实例覆盖 - 任务体必须是幂等的 —— 即使锁失效导致偶发双跑,也不应引发数据错乱
示例片段(放在 onWorkerStart 中):
use Redis;
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
Timer::tick(5, function () use ($redis) {
$lockKey = 'cron:cleanup_expired_sessions';
$lockValue = $_SERVER['HOSTNAME'] . ':' . getmypid();
// 尝试加锁,最多持有 30 秒
if ($redis->set($lockKey, $lockValue, ['nx', 'ex' => 30])) {
try {
cleanupExpiredSessions();
} finally {
// 确保只有自己能删自己的锁
if ($redis->get($lockKey) === $lockValue) {
$redis->del($lockKey);
}
}
}
});
更稳的做法:把定时逻辑抽成独立单进程 Worker
与其让所有 Webman 实例都参与调度竞争,不如明确划分职责 —— 专门起一个只干定时任务的进程,且强制 $processCount = 1。
- 新建
app/process/Scheduler.php,在config/process.php中注册它为独立进程 - 配置该进程的
eventLoop为Workerman\Events\Swoole::class,启用高精度协程驱动 - 禁用所有业务中间件、路由、HTTP/WebSocket 服务,只保留
onWorkerStart里的Timer::tick或Timer::after - 通过 Redis Pub/Sub 或消息队列(如 Kafka/RabbitMQ)把任务分发给其他 Webman 实例处理,避免 Scheduler 进程承担业务逻辑
别忽略的三个硬伤点
即使用了 Redis 锁或独立进程,以下三点仍极易被绕过:
- 时钟不同步:多台机器系统时间差 > 1 秒,会导致锁误判或任务漏跑 —— 必须统一 NTP 时间源,
ntpq -p定期检查 - Redis 单点故障:锁服务挂了,所有定时任务停摆 —— 生产环境务必用 Redis Sentinel 或 Cluster,且加降级逻辑(如本地 fallback 计数器)
- 协程阻塞:Scheduler 进程里调了
file_get_contents或没加timeout的 cURL,整个 tick 就卡住 —— 所有 I/O 必须用协程版(Co\Http\Client、Co\Redis)并设超时


















