Hyperf协程中嵌套事务易死锁,根本原因不是协程本身,而是MySQL不支持真正嵌套事务,SAVEPOINT共享同一事务锁空间,且ROLLBACK TO SAVEPOINT不释放行锁;需禁用嵌套、统一加锁顺序、启用innodb_print_all_deadlocks并优化索引。

Hyperf协程里嵌套事务为什么更容易死锁
不是协程本身导致死锁,而是协程放大了框架用SAVEPOINT模拟“嵌套事务”时的锁共享问题。MySQL根本不支持真正嵌套事务,所有SAVEPOINT都在同一个事务上下文里,锁空间完全共用。一个事务在sp1更新user_id=100,又在sp2更新order_id=500;另一个事务反向操作——立刻触发死锁。
更隐蔽的是:ROLLBACK TO SAVEPOINT只回滚数据变更,行锁不会释放,直到外层COMMIT或ROLLBACK。你看到代码“回滚了内层”,其实锁还挂着。
- 检查
SHOW ENGINE INNODB STATUS里的LATEST DETECTED DEADLOCK段,确认是否出现两个事务交叉等待同一组主键 - 运行
EXPLAIN看所有UPDATE语句是否走const或range索引,避免ALL扫描触发表锁 - 查表结构,确认
WHERE条件字段(尤其是外键、子查询关联字段)都有索引,否则间隙锁会扩大锁定范围
如何重构Hyperf里带嵌套逻辑的事务
核心是消灭“嵌套感”,让事务边界清晰、加锁顺序确定、锁持有时间最短。别依赖框架的DB::transaction()嵌套写法。
-
拆成独立事务:把原内层逻辑抽成单独方法,显式调用
DB::transaction(),用幂等设计兜底(如INSERT ON DUPLICATE KEY UPDATE) -
合并为单事务 + 强制排序:若必须原子性,就把所有
UPDATE/SELECT FOR UPDATE合在一个事务里,并加ORDER BY id ASC(或其他唯一索引字段),防止优化器随机决定加锁顺序 -
应用层加锁兜底:对高冲突资源(如账户余额、库存),在事务外用
Redis锁(SET lock_key value NX PX 5000)抢占,成功再进DB事务;失败直接重试或降级
Hyperf中必须启用的MySQL配置项
不配这些,死锁日志永远看不到完整现场,你只能靠猜。
- 在
my.cnf里设置innodb_print_all_deadlocks = ON并重启MySQL,否则error log只记最近一次死锁 - 设
log_error_verbosity = 3,确保死锁详情完整输出到/var/log/mysql/error.log - 在Hyperf的
config/autoload/database.php中为连接配置'timeout' => 5和'sticky' => false,防连接复用残留事务状态
最容易被忽略的Hyperf死锁细节
Hyperf协程调度不会中断正在执行的SQL事务,哪怕你在事务里写了Coroutine::sleep(0.1),锁也一直挂着。事务块里混入HTTP请求、Redis读写、日志记录,等于人为延长锁持有时间。
另外,框架生成的SAVEPOINT名字如果重复(比如都叫trans),会导致ROLLBACK TO SAVEPOINT行为异常,进一步扰乱锁状态。检查你的Job类或Service方法,确认每个SAVEPOINT名唯一且可追溯。



















