当前位置:首页 > 文章列表 > 数据库 > MySQL > MySQL InnoDB 死锁实战:别只会等 lock wait timeout

MySQL InnoDB 死锁实战:别只会等 lock wait timeout

来源:MySQL 官方文档 2026-06-02 11:27:09 0浏览 收藏

线上遇到 MySQL 锁等待,很多人的第一反应是把 innodb_lock_wait_timeout 调大。这个动作有时候像给发烧的人多盖一床被子:看起来在处理问题,实际上可能让连接堆得更久,把线程池和连接池一起拖下水。

这篇我按一次转账服务的事故复盘来写,重点不背锁类型名词,而是讲怎么复现、怎么找到阻塞者、怎么读 performance_schema.data_locksdata_lock_waits,以及为什么死锁不是“彻底消灭”,而是要减少概率并让业务能安全重试。适用版本:MySQL 8.0 / 8.4,存储引擎默认按 InnoDB 讨论。

MySQL InnoDB 死锁与锁等待排查思维导图
思维导图:死锁排查不是只看错误码,要把事务顺序、锁等待、诊断视图和重试策略串起来。

业务场景:转账接口偶发 1213

假设有一张账户表,转账时从 A 扣钱、给 B 加钱。两个用户互相转账,或者后台批量调账和用户转账同时发生,就可能出现事务拿锁顺序相反。

CREATE TABLE accounts (
  id BIGINT PRIMARY KEY,
  balance DECIMAL(12,2) NOT NULL
) ENGINE=InnoDB;

事务 A 先锁 id=1,再锁 id=2;事务 B 先锁 id=2,再锁 id=1。两个事务都拿着对方想要的锁,谁也走不下去,这就是很典型的死锁。

先复现:别一上来就改参数

会复现,排查就成功了一半。两个会话分别执行下面的 SQL,很容易制造一个事务顺序相反的现场。

-- 会话 A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 会话 B
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;

如果时序刚好卡住,MySQL 会检测到死锁并回滚其中一个事务,客户端通常会收到 ERROR 1213 (40001): Deadlock found when trying to get lock。如果不是死锁,而只是等不到锁,常见的是 ERROR 1205: Lock wait timeout exceeded

死锁和锁等待不是一回事

死锁是互相等待,MySQL 检测到后会挑一个事务回滚。锁等待是一个事务等另一个事务释放锁,不一定形成环。前者通常要业务重试,后者要先找谁挡住了路。

所以报警里看到 1213 和 1205,处理思路不一样。1213 要看事务顺序和重试策略;1205 要定位阻塞者、长事务、慢 SQL 或未提交事务。别拿一个超时时间参数糊所有问题。

MySQL InnoDB 锁等待排查流程图
流程图:先定位阻塞者,再看事务和锁模式,最后才决定改 SQL、改索引还是改业务顺序。

看最后一次死锁现场

MySQL 官方建议用 SHOW ENGINE INNODB STATUS 查看最近一次 InnoDB 用户事务死锁。这个输出很长,但你要抓住几块:哪个事务等待哪个锁,持有哪些锁,执行的是哪条 SQL,最后谁被回滚。

SHOW ENGINE INNODB STATUS\G

如果死锁很频繁,只看最后一次不够,可以考虑临时打开 innodb_print_all_deadlocks,让死锁信息写到错误日志里。注意这是排障手段,不是长期替代监控的方式,打开前要确认日志量和敏感 SQL 风险。

SET GLOBAL innodb_print_all_deadlocks = ON;

MySQL 8.x 更推荐用 performance_schema 看锁

在线上排查锁等待,我更喜欢先查 performance_schema.data_lock_waits,它能把等待者和阻塞者关联起来;再连 data_locks 看锁在哪个库、哪个表、哪个索引、什么锁模式。

SELECT
  w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
  w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx
FROM performance_schema.data_lock_waits AS w;

再把事务 ID 和锁表关联起来看细节:

SELECT
  ENGINE_TRANSACTION_ID,
  OBJECT_SCHEMA,
  OBJECT_NAME,
  INDEX_NAME,
  LOCK_MODE,
  LOCK_STATUS
FROM performance_schema.data_locks
WHERE LOCK_STATUS IN ('WAITING','GRANTED');

排查时不要只看 LOCK_MODE,还要看 INDEX_NAME。很多锁范围过大的问题,本质是 SQL 没走到合适索引,导致 InnoDB 锁住了比你以为更多的记录或范围。

MySQL InnoDB 死锁 SQL 案例图
案例图:事务顺序相反最容易制造环形等待,线上要统一加锁顺序并准备重试。

间隙锁为什么会让人误判

在 InnoDB 的默认可重复读隔离级别下,范围查询加锁时可能出现 next-key lock,也就是记录锁加间隙锁。你明明只想改一行,结果因为条件没有好索引,锁住了一段范围,其他插入或更新也跟着排队。

这类问题最容易被误判成“数据库偶尔抽风”。实际根因可能是:范围条件没索引、索引列顺序不对、事务里先查范围再更新、或者批量任务一口气锁太多行。

修复思路一:统一加锁顺序

转账这种场景,我会强制按账户 id 从小到大加锁。无论 A 转 B,还是 B 转 A,事务内部都按同一个顺序锁定账户。这样能显著降低互相反向等待的概率。

-- 先锁较小 id,再锁较大 id
SELECT id FROM accounts
WHERE id IN (1, 2)
ORDER BY id
FOR UPDATE;

拿到锁之后再按业务方向扣减和增加余额。注意,ORDER BY 只是表达意图,真正落地时要确认执行计划和索引,避免扫描范围扩大。

修复思路二:让锁更小、更短

事务里不要夹杂远程调用、复杂计算、慢日志写入、用户交互等待。锁一旦拿到,就尽快完成数据库内的关键更新并提交。长事务不是慢一点而已,它会把别人的锁等待一起拉长。

另外,更新条件必须尽量命中精确索引。比如按订单号更新就建订单号唯一索引,按用户和状态批量处理就要评估复合索引。没有合适索引的 UPDATE ... WHERE,很容易让锁范围超出预期。

修复思路三:业务必须能重试

官方文档也强调,死锁并不表示数据库坏了。InnoDB 默认会检测死锁并回滚一个事务,应用需要能捕获错误并重试。关键是重试必须幂等,不能把扣款、发券、写流水这种副作用重复执行。

我的习惯是:数据库事务里只做可回滚的数据修改;外部消息、通知、回调放到事务成功后,或者用事务消息/流水状态保证幂等。遇到 1213 可以短暂退避后重试 1 到 3 次,超过次数就记录现场。

上线前检查什么

  • 同一类业务是否统一加锁顺序,尤其是转账、库存调拨、批量状态流转。
  • 事务里是否包含远程调用、sleep、复杂循环或大批量更新。
  • 更新条件是否命中合适索引,是否可能扩大锁范围。
  • 是否有 1213 死锁重试和 1205 锁等待超时的不同处理。
  • 是否能通过 data_lock_waits 快速看到 blocking_trx。
  • 上线后是否观察死锁次数、锁等待时间、活跃连接数和慢事务。

我的排查顺序

第一,看报警是 1213 还是 1205。第二,用 SHOW ENGINE INNODB STATUS 拿最近死锁现场。第三,用 performance_schema.data_lock_waits 找等待者和阻塞者。第四,回到业务代码看事务边界和加锁顺序。第五,再决定改索引、改 SQL、拆事务还是补重试。

这个顺序能避免一个常见误区:一上来就调大超时,结果把问题藏得更深。锁问题不是靠等解决的,是靠缩短事务、缩小锁范围、统一顺序和安全重试解决的。

最后聊两句

MySQL InnoDB 死锁不是罕见事故,它是并发写入系统里迟早会遇到的正常现象。真正区分工程质量的,不是系统永远不死锁,而是死锁发生时能不能快速定位、自动重试、保住数据一致性。

我的建议很朴素:把每一次死锁当成一次事务设计 review。把 SQL、锁等待图、事务顺序、索引和重试结果都留下来。下次类似问题来时,你就不是靠猜,而是靠证据往前推。

参考资料:MySQL 8.4 Reference Manual:Deadlocks in InnoDBMySQL 8.4 Reference Manual:data_locks TableMySQL 8.4 Reference Manual:InnoDB Locking

版本声明
本文转载于:MySQL 官方文档 如有侵犯,请联系study_golang@163.com删除
MySQL EXPLAIN ANALYZE 实战:慢查询别只盯着 possible_keysMySQL EXPLAIN ANALYZE 实战:慢查询别只盯着 possible_keys
上一篇
MySQL EXPLAIN ANALYZE 实战:慢查询别只盯着 possible_keys
MySQL 在线 DDL 实战:ALTER TABLE 别把业务卡在 MDL 上
下一篇
MySQL 在线 DDL 实战:ALTER TABLE 别把业务卡在 MDL 上
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    158次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    87次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    46次使用
  • PromptHero官网:AI提示词搜索、优化与学习平台,支持Midjourney/Stable Diffusion
    PromptHero
    PromptHero是专业的AI提示词搜索引擎与优化平台,支持Stable Diffusion、Midjourney等主流模型。提供海量提示词库、分类搜索、在线课程及社区互动,助力用户高效生成高质量AI图像与文本。
    26次使用
  • OpenArt免费开源指南:Stable Diffusion Prompt Book提示词手册详解
    Stable Diffusion Prompt Book
    深入解析OpenArt推出的Stable Diffusion Prompt Book,这本免费的开源提示词指南涵盖从基础语法到高级技巧,提供风格化词库与参数建议,助您优化AI绘画生成效果。
    29次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码