它的本质是两个或多个事务在执行过程中因争夺资源而造成的一种互相等待 (Circular Wait)的现象。若无外力干涉它们都将无法推进。在 PHP MySQL 架构中这通常不是 PHP 代码本身的逻辑死锁而是数据库层面InnoDB 引擎的行锁/间隙锁冲突通过 PHP 的事务边界暴露出来。如果把死锁比作窄桥相遇事务 A占据了桥的左半边想往右走请求右半边的锁。事务 B占据了桥的右半边想往左走请求左半边的锁。结果谁也动不了僵持不下。MySQL 的裁判机制InnoDB 检测到死锁后会主动牺牲其中一个事务通常是代价较小的那个抛出Deadlock found when trying to get lock异常让另一个事务继续执行。一、产生根因死锁的四个必要条件只要同时满足以下四个条件死锁必然发生互斥条件 (Mutual Exclusion)资源行记录一次只能被一个事务占用。持有并等待 (Hold and Wait)事务 A 持有资源 1同时申请资源 2。不可剥夺 (No Preemption)资源不能被强行抢占只能由持有者主动释放。循环等待 (Circular Wait)A 等 BB 等 A形成环路。 核心洞察在数据库中我们很难打破前三个条件这是 ACID 的要求所以我们主要通过打破“循环等待”来预防死锁——即规定所有事务以相同的顺序访问资源。二、InnoDB 锁机制看不见的杀手PHP 开发者常误以为只有SELECT ... FOR UPDATE才加锁其实不然。1. 行锁 (Record Lock)机制锁定索引记录。前提必须通过索引检索数据。如果没有索引InnoDB 会退化为表锁极易导致性能崩溃和死锁。2. 间隙锁 (Gap Lock)机制锁定索引记录之间的“间隙”防止其他事务插入新记录解决幻读问题。场景REPEATABLE READ隔离级别下范围查询WHERE id 10不仅锁住存在的记录还锁住间隙。死锁陷阱两个事务分别锁定不同的间隙但都想向对方锁定的间隙插入数据或者更新边界记录可能形成死锁。3. 临键锁 (Next-Key Lock)机制行锁 间隙锁。锁定记录本身及其前面的间隙。影响这是 InnoDB 默认的最强锁粒度也是死锁的高发区。4. 意向锁 (Intention Lock)机制表级锁表示“我打算给某行加锁”。作用提高多粒度锁共存时的效率通常不直接导致死锁但参与锁兼容性判断。三、PHP 触发场景代码是如何引爆地雷的场景 1经典的 AB-BA 顺序不一致业务转账。用户 A 转给用户 B。线程 1DB::beginTransaction();// 1. 锁定 AUser::where(id,1)-lockForUpdate()-first();sleep(1);// 模拟耗时增加并发碰撞概率// 2. 尝试锁定 B - 阻塞因为线程 2 已锁定 BUser::where(id,2)-lockForUpdate()-first();DB::commit();线程 2DB::beginTransaction();// 1. 锁定 BUser::where(id,2)-lockForUpdate()-first();sleep(1);// 2. 尝试锁定 A - 阻塞因为线程 1 已锁定 AUser::where(id,1)-lockForUpdate()-first();DB::commit();结果死锁。线程 1 等线程 2线程 2 等线程 1。场景 2间隙锁冲突 (Insert Deadlock)业务批量插入唯一索引数据。线程 1INSERT INTO users (uid) VALUES (10);- 获取 uid10 的间隙锁。线程 2INSERT INTO users (uid) VALUES (10);- 也获取 uid10 的间隙锁兼容。冲突当两者都试图将间隙锁升级为排他行锁写入数据时发现对方也持有间隙锁互相等待形成死锁。场景 3索引失效导致锁升级代码User::where(name, john)-lockForUpdate()-first();问题如果name字段没有索引。后果InnoDB 扫描全表对每一行都加上锁。风险极大增加锁冲突概率甚至导致整个表被锁死其他事务全部阻塞。场景 4PHP 事务内调用外部 API代码DB::beginTransaction();$orderOrder::find(1)-lockForUpdate();// 锁定订单Http::post(http://external-service/pay);// 远程调用耗时 5s$order-statuspaid;$order-save();DB::commit();风险锁持有时间过长5秒。虽然不一定是死锁但极易引发长时间阻塞导致后续请求堆积最终触发数据库连接池耗尽或超时表现为系统假死。四、排查与解决像侦探一样破案1. 捕捉现场命令SHOW ENGINE INNODB STATUS;关键部分LATEST DETECTED DEADLOCK。解读Transaction 1: 正在做什么 SQL持有什么锁等待什么锁。Transaction 2: 正在做什么 SQL持有什么锁等待什么锁。Victim: 哪个事务被回滚了。2. 解决方案策略 A固定访问顺序 (Ordering)原则所有事务必须按照相同的主键/索引顺序访问资源。实施在转账例子中始终先锁定 ID 小的用户再锁定 ID 大的用户。$ids[$id1,$id2];sort($ids);// 确保顺序一致User::where(id,$ids[0])-lockForUpdate()-first();User::where(id,$ids[1])-lockForUpdate()-first();策略 B降低隔离级别 (谨慎使用)原理READ COMMITTED级别下InnoDB 不使用间隙锁Gap Lock只使用行锁。效果大幅减少死锁概率。代价可能出现幻读。需评估业务是否允许。策略 C优化索引原则确保WHERE条件和ORDER BY字段都有索引避免锁升级。行动EXPLAIN分析 SQL确保 type 是ref或range而不是ALL。策略 D缩短事务粒度原则快进快出。行动不要在事务中进行 HTTP 请求、复杂计算、文件 IO。将非数据库操作移到事务外。尽量一次性批量更新减少交互次数。策略 E重试机制 (Retry)原理既然死锁是概率事件且 InnoDB 会自动回滚其中一个那么应用层捕获异常并重试即可。代码$maxRetries3;for($i0;$i$maxRetries;$i){try{DB::transaction(function(){// 业务逻辑});break;// 成功则退出}catch(\Illuminate\Database\QueryException$e){if(strpos($e-getMessage(),Deadlock)!false$i$maxRetries-1){continue;// 重试}throw$e;// 其他错误或重试耗尽抛出}} 总结原子化“死锁”全景图维度关键点行动指南根因循环等待固定资源访问顺序锁类型行锁、间隙锁、临键锁理解 RR 级别下的间隙锁行为索引无索引导致表锁/全表扫描确保查询走索引事务持有时间过长事务内禁止 IO/HTTP快进快出兜底死锁不可避免应用层实现重试机制监控SHOW ENGINE INNODB STATUS定期分析死锁日志优化高频冲突 SQL终极心法死锁的本质是“并发秩序”的缺失。它不是 Bug而是高并发下的物理必然。别试图消灭死锁要管理它。通过顺序、索引、短事务和重试将死锁的概率降到可接受范围。于竞争中见秩序于异常中见韧性以协议为纲解僵局之牛于高并发中求稳定之真。行动指令今日版检查代码搜索项目中所有的DB::transaction或beginTransaction。审计逻辑是否有嵌套事务事务内是否有 HTTP 请求多个表更新是否有固定顺序查看日志登录数据库运行SHOW ENGINE INNODB STATUS\G看最近是否有死锁记录。优化索引对高频更新的 SQL 进行EXPLAIN确保没有全表扫描。添加重试为核心交易接口添加死锁重试逻辑。