这不是数据库配置低,而是多个workerman进程并发update同一行且事务含sleep()等阻塞操作,导致锁长期未释放;根本解法是禁用事务内sleep()、改用timer::add()异步延时,并确保update命中索引。

定时器执行SQL时卡住,报Lock wait timeout exceeded
这通常不是MySQL配置太低,而是多个Workerman进程(或定时器+Web请求)同时UPDATE同一行,且事务里混了sleep()、远程调用等阻塞操作。InnoDB检测到锁等待超时(默认50秒),就抛出Lock wait timeout exceeded; try restarting transaction。Windows下更明显,因为innodb_lock_wait_timeout没调小,但问题根子不在timeout值——在锁没及时释放。
- 定时器进程和Web请求是两个独立数据库连接,各自开启事务后,若都对
id=1执行UPDATE,又没统一加锁顺序,极易形成循环等待 - 如果UPDATE的WHERE条件没走索引(比如
WHERE status = 'pending'但status无索引),InnoDB会升级为表级锁,死锁概率陡增 -
sleep(2)写在事务里是致命错误:Workerman进程被卡住,连接不释放,锁就一直挂着
别在定时器里写sleep(),改用Timer::add()做异步延时
sleep()会让整个Worker进程停摆,后续所有IO、心跳、新连接全卡死。你看到的“卡几十秒”,其实是事务挂起+锁等待+超时三重叠加。
- 所有延时逻辑必须用
Timer::add()注册回调,例如Timer::add(30, [$handler, 'checkOrderTimeout']) - 回调函数内不能出现任何同步阻塞调用:
file_get_contents()、mysqli_query()、curl_exec()都得换成异步替代方案 - 需要链式延时(如“30秒后检查→再过10秒重试”),必须在第一次回调里再次调用
Timer::add(),而不是嵌套sleep() - 记得保存返回的
$timerId,并在业务完成或连接断开时主动Timer::del($timerId),否则内存泄漏+定时器堆积
UPDATE语句必须命中主键或唯一索引,否则锁范围失控
InnoDB行锁生效的前提是WHERE能精准定位单行。如果UPDATE走不了索引,就会锁整张表,让并发更新直接变成排队。
- 用
EXPLAIN SELECT * FROM orders WHERE order_no = 'xxx'确认type是const或eq_ref - 给业务中高频UPDATE的字段建唯一索引,比如
order_no、user_id,才能安全用INSERT ... ON DUPLICATE KEY UPDATE替代“查+判+更” - 禁止在事务里SELECT FOR UPDATE后再做耗时计算;真要分步锁,所有路径必须严格按相同顺序访问表(例如永远先锁
users再锁orders)
多Worker部署时,定时任务重复执行怎么办
Workerman不支持跨进程共享定时器,onWorkerStart里每个Worker都会注册一份,4个Worker就跑4次同一定时任务。
- 用Redis分布式锁控制入口:
SET lock:order_timeout "1" NX EX 30,抢到才执行,失败直接return - 或改用延迟消息:把订单ID和超时时间写入Redis ZSET(score=超时时间戳),另起一个常驻Worker轮询ZSET头元素,避免定时器精度误差和重复问题
- 定时器本身只负责“触发检查”,不负责“执行关单”,真正的DB更新和通知推送应交给Task Worker或独立服务处理
真正容易被忽略的是:没人清理的Timer::add()会在Worker内存里持续累积,跑几天后响应变慢、OOM、日志里却找不到明显线索。











