webman 不支持毫秒级高并发定时提醒,应弃用 workerman/crontab,改用 redis zset 延迟队列+独立 cli 进程消费,并配合 swoole/swow 协程驱动、外置状态存储与失败重试机制。

Webman 本身不支持毫秒级、高并发、可运维的定时消息提醒,硬用 workerman/crontab 会卡死 Worker 进程、丢失任务、调度漂移严重——这不是配置问题,是架构层级的不匹配。
别在 Worker 进程里跑定时提醒逻辑
workerman/crontab 是单进程内事件循环驱动的轻量调度器,它不是独立服务,也不带状态持久化。一旦提醒逻辑涉及 DB 查询、HTTP 调用或 sleep,就会阻塞整个 Worker,导致 WebSocket 消息堆积、连接超时、心跳断连。
- 所有耗时操作(哪怕只是
file_get_contents或pdo->query)必须剥离出 Worker 主循环 - 不能把「查用户明天生日」和「发短信」写在同一个
Crontab回调里 - 每新增一个提醒规则就 new 一个
Crontab实例?错。多个实例共享同一事件循环,一个卡住,全部延迟 - 表达式写成
'0 0 * * *'(5位)?它会被静默解释为「每天 0 分钟执行一次」,不是凌晨 0 点
用 Redis ZSet + 独立 Worker 实现精准投递
真正能扛住每秒数百次提醒触发、支持失败重试和时间校准的方案,是绕过 crontab,手写基于 Redis ZSet 的延迟队列,并由专用 CLI 进程消费。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 投递时用
$redis->zAdd('remind:queue', time() + 3600, json_encode(['user_id'=>123, 'tpl'=>'birthday'])),score 必须是绝对时间戳(秒级) - Worker 进程里不用
sleep(1)轮询,改用usleep(200000)+ZRANGEBYSCORE ... LIMIT 1+ZREM原子操作,避免重复消费 - 每次取出任务后先
SETNX remind:processing:{id} 1 EX 300做幂等锁,防止进程崩溃导致重复触发 - 失败任务不要直接
nack回队列——ZSet 不支持重入,应写回新 score(如time()+60),并记录失败原因到remind:fail:20260729Hash
毫秒级提醒必须换协程驱动
如果业务真需要「提前 500ms 推送会议开始提醒」,workerman/crontab 的最小粒度是 1 秒,且依赖底层 event loop 精度。默认 Select 或 Event 驱动实际调度间隔约 100ms,根本不可控。
- 必须在
config/process.php中显式指定eventLoop => Workerman\Events\Swoole::class(Swoole v5.0+)或Workerman\Events\Swow::class(Swow v1.4+) - 禁用
Fiber驱动:PHP 原生 Fiber 在高负载下易发生调度漂移,不适用于提醒类强时效场景 - 用
Co\Timer::tick(500, function () { ... })替代Crontab,但仅限于纯内存计算、无 I/O 的极简逻辑;任何 DB/HTTP 操作仍需投递到异步队列 - 验证是否生效:在
onWorkerStart打印get_class(Workerman\Events\EventInterface::getInstance()),必须输出Swoole或Swow
提醒状态必须外置存储,不能靠内存
用户关闭提醒、修改时间、批量停用某类模板——这些操作要求实时生效。若状态只存在 Worker 进程内存里,reload 后全丢,多实例部署更无法同步。
- 所有开关状态存 Redis:
SET remind:switch:123 0(0=关闭,1=开启),读取时用GET+PIPELINE批量查 - 已触发过的提醒 ID 记录到
remind:history:20260729Set,防止同天重复推送 - 用户设备 token 变更后,旧 token 必须立刻失效:用
DEL remind:token:123清缓存,而非等 TTL 自然过期 - 别用 MySQL 存待触发列表——写入压力大、查询慢、锁表风险高;ZSet 已足够支撑百万级提醒调度
最容易被忽略的是「提醒失败后的兜底动作」:没人看日志,也没人清失败队列,结果某天凌晨三点批量重试,短信网关被打爆。真正的高性能,不在于投得多快,而在于错得明白、 recover 得及时。










