单条更新语句执行后触发死锁错误的异常场景排查
带左连接的UPDATE语句更新成功后仍触发死锁的成因分析
问题背景
有一条在多微服务中并行执行的SQL更新语句,偶尔会触发死锁(符合预期),但出现边缘场景:某服务的语句更新记录状态后仍抛出死锁错误,而并行的另一个服务语句执行正常、未更新任何记录(目标记录已处于pending状态)。代码实现如下:
const reserveActionForExecution = async (id: number): Promise<boolean> => { try { const query = ` UPDATE articles a1 LEFT JOIN articles a2 ON a1.linkedArticleId = a2._id SET a1.status = ?, a1.updated_time = ? WHERE a1._id = ? AND a1.status = ? AND a1.updated_time = ? AND (a2.status = 'open' OR a2.status IS NULL) ` const result = await client.query(query, ['pending', Date.now(), id, 'open', originalUpdatedTime]) return result } catch (err: any) { if (extra.code === 'ER_LOCK_DEADLOCK')) return false throw err } }
核心成因分析
你观察到的“更新成功+死锁错误”看似矛盾,本质是InnoDB锁机制、死锁回滚策略,或代码/观察细节偏差导致的,具体拆解如下:
1. 单语句事务的锁执行逻辑
你的UPDATE语句属于隐式事务(自动提交模式),执行流程是:
- 先对
a1表中符合_id、status、updated_time条件的记录加排他锁(X锁) - 再对
a2表中关联的记录加共享锁(S锁,仅用于读取a2.status做判断) - 所有锁获取完成后,执行
a1的更新操作 - 提交事务,释放所有锁
死锁只会发生在锁获取阶段(前两步),而非更新完成后。当死锁触发时,MySQL会选择一个事务回滚:
- 若被回滚的是当前服务的事务:
a1的更新会被撤销,记录保持open状态,代码捕获死锁错误返回false - 若被回滚的是并行的其他事务:当前事务会继续执行完成更新,不会抛出错误,代码返回受影响行数(
true)
2. 你观察到的边缘场景的真实原因
你看到的“更新成功+死锁错误”大概率是以下两种情况之一:
- 时间差导致的误判:当前服务的事务第一次执行触发死锁被回滚,代码返回
false;此时并行服务的事务因死锁解除(当前事务释放锁),继续执行并成功更新了a1的状态为pending,你误以为是当前服务的事务完成了更新。 - 代码错误处理的笔误:代码中
extra.code应为err.code,变量名写错会导致死锁错误未被捕获而直接抛出;此时若死锁被回滚的是其他事务,当前事务已成功完成更新,但代码错误抛出异常,让你产生“更新成功后抛死锁”的错觉。
3. 左连接带来的锁范围扩大风险
你的左连接条件(a2.status = 'open' OR a2.status IS NULL)存在锁范围扩大的隐患:
- 若
a2.status未建立索引,InnoDB会扫描整个a2表,对所有a2记录加S锁,大幅提升锁冲突概率。 - 当
a1.linkedArticleId为NULL时,左连接会匹配所有a2的NULL行,导致InnoDB锁定大量无关记录,进一步增加死锁可能性。
验证与优化建议
- 修正代码错误处理的变量名:将
extra.code改为err.code,确保死锁错误被正确捕获。 - 查看InnoDB死锁日志:执行
SHOW ENGINE INNODB STATUS,日志会详细记录死锁发生时的事务、锁持有/等待情况,明确哪个事务被回滚。 - 优化索引:给
a2.status和a1.linkedArticleId建立联合索引,缩小锁的范围,减少死锁触发概率。
内容的提问来源于stack exchange,提问作者SkinnyBetas
相关产品推荐
相关产品推荐

