mysql死锁本质是多个事务因争夺同一行锁且加锁顺序不一致,形成循环等待;workerman下易发是因长连接+定时任务与web请求并发竞争,根本解法是统一锁序、命中索引、降隔离级、加智能重试。

Workerman 里 MySQL 死锁不是框架问题,而是事务并发逻辑没对齐 —— 尤其在 Windows 下更容易暴露,但根子在 SQL 执行顺序和锁持有时间。
为什么 Workerman 定时任务一跑就触发死锁?
常见现象是:定时器每秒执行 UPDATE,网页端也同时更新同一行,几秒后报错 Deadlock found when trying to get lock 或 Lock wait timeout exceeded。这不是 Workerman 的 bug,而是两个独立连接(定时器进程 + Web 请求进程)以不同顺序或时机竞争同一行锁,InnoDB 检测到循环等待后强制回滚其中一个。
Windows 环境下更易复现,是因为默认的 innodb_lock_wait_timeout 是 50 秒,而 Workerman 进程常驻、连接复用,容易让锁等待堆积;Linux 下连接池/调度机制略有差异,掩盖了问题,但不代表不存在。
- 根本诱因是多个事务操作相同主键/索引值,但没有统一加锁路径
- 定时任务和 Web 请求属于不同会话(session),各自开启事务后,
SELECT FOR UPDATE或直接UPDATE都可能先占锁再等对方,形成 A→B→A 循环 - 如果 UPDATE 条件没走索引(比如用
WHERE name = 'xxx'但name无索引),InnoDB 会升级为表锁,死锁概率陡增
怎么让 UPDATE 不抢锁、不卡住?
核心思路是:**避免“先查再更”,改用原子性更强、锁范围更可控的方式**。尤其在 Workerman 这类长生命周期进程中,不能依赖应用层判断是否存在再决定是否更新。
- 用
INSERT ... ON DUPLICATE KEY UPDATE替代“查+判+更”三步逻辑,前提是字段有唯一索引(如order_no或user_id) - 对纯更新场景,确保
UPDATE语句 WHERE 条件命中主键或唯一索引,杜绝全表扫描;执行前用EXPLAIN确认type是const或eq_ref - 不要在事务里塞
sleep()、远程 API 调用、文件读写等耗时操作 —— Workerman 进程一旦阻塞,整个连接上的锁就一直挂着 - 如果必须分步操作(比如先
SELECT FOR UPDATE再业务计算再UPDATE),务必保证所有业务路径都按完全相同的顺序访问表和行(例如永远先锁users表,再锁orders表)
Workerman 进程里怎么安全重试死锁?
MySQL 检测到死锁后会返回错误码 1213(Deadlock found)或 1205(Lock wait timeout),这时不能直接抛异常,得捕获并重试。但要注意:重试不能无脑循环,否则可能雪崩。
- 在 Workerman 的
onWorkerStart或定时器回调中,用 try/catch 包裹事务逻辑,判断$e->getCode() === '1213'或=== '1205' - 加入退避策略:首次失败后
usleep(10000)(10ms),第二次失败usleep(50000),最多重试 3 次 - 重试前必须重新开启事务(
beginTransaction()),不能复用已回滚的事务上下文 - 记录日志时带上
$e->getMessage()和当前事务涉及的主键值,方便后续比对是不是热点数据引发
最容易被忽略的点:隔离级别和连接配置
Workerman 默认不会自动设置事务隔离级别,如果业务库全局是 REPEATABLE READ(MySQL 默认),间隙锁(Gap Lock)会让死锁更频繁 —— 尤其在范围查询或插入场景。而很多定时任务其实只需要读已提交即可。
- 在 Workerman 启动时,对专用连接显式执行
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 给定时任务单独配一个 MySQL 连接池(如使用
workerman/mysql),不要和 Web 请求共用同一套连接配置 - 调低该连接的锁等待超时:执行
SET innodb_lock_wait_timeout = 3(单位秒),比默认 50 更早失败、更快重试,避免线程卡死 - 检查
max_connections是否吃紧 —— Workerman 多进程 + 每个进程保持长连接,容易打满连接数,导致新请求排队等连接,间接拉长事务等待链











