当前位置:首页 > 文章列表 > 数据库 > 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
下一篇
下一篇
暂无
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    5904次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    6333次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    6142次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    8117次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    6631次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码