Java 王者修炼手册【Mysql 篇 - 锁】:吃透 MySQL 行锁 + 间隙锁 + 意向锁 底层机制,了解死锁解决方案

大家好,我是程序员强子。
前面学习了 Mysql 四大金刚的 索引 & 日志,今天专注把 锁 相关给弄明白~
来看看今天学习的知识点:
锁分类:表锁/行锁/共享锁/排他锁 作用,原理
锁等待:锁等待原因,排查工具,分析工具,最优实践
死锁:产生条件,排查工具,排查流程,解决方案
平常大多数需求都是使用 InnoDB 引擎,因此锁也是只讨论 InnoDB 相关的~
系好安全带,发车啦~
按锁粒度分类
表级锁
分类
意向锁(IS/IX 锁)
元数据锁(MDL 锁)
自增锁(AUTO-INC 锁)
意向锁
表级标记锁 ,标记表内是否有行锁
解决 表锁等待行锁释放 的效率问题
自动加锁 / 释放,无需手动干预
想象一下,如果没有意向锁:
加表锁时需要扫描表中所有行,确认是否有行级锁!!!
如果表数据量还很大的话,效率会有多低,不敢想~
触发场景:
加行级 S 锁前 , 自动加 IS 锁;
加行级 X 锁前 ,自动加 IX 锁
行级锁
有哪些分类?
记录锁(S/X 锁)
间隙锁(GAP 锁)
临键锁(Next-Key 锁)
行锁不是简单锁定 数据行,而是通过 索引 锁定 索引记录或区间
记录锁
核心逻辑:仅锁定某一行的索引记录,不影响其他行,是最常用的行锁。
触发条件:通过唯一索引(主键 / 唯一键) 精准匹配单行(如 WHERE id=1,id 是主键)
案例:
s
只是开始,还没 commit;
此时其他事务更新 id=1 会被阻塞,但更新 id=2 不受影响。
间隙锁
核心逻辑:锁定索引记录之间的 间隙(不含记录本身),防止其他事务在间隙中插入数据,解决 幻读隐患
触发条件在 非唯一索引 上进行范围查询(如 BETWEEN、>、<)唯一索引范围查询(如 id > 10,id 是主键)
案例:非唯一索引的间隙锁,表 user 有非唯一索引 idx_age(age),数据 age 为 [10, 20, 30]
锁定的间隙:(-∞,10)、(10,20)、(20,30)、(30,+∞)
事务 2 尝试插入 age=15(在 (10,20) 间隙中)会被阻塞,直到事务 1 提交
临键锁
核心逻辑:InnoDB 默认的行锁算法(RR 级别下),是 记录锁 + 间隙锁 的组合,既锁记录本身,也锁记录前后的间隙,彻底防止幻读
触发条件:RR 级别下,通过非唯一索引进行查询
案例:Next-Key Lock 阻塞插入,更新表 user 非唯一索引 idx_age(age),数据 age=20
锁定:age=20 的记录(记录锁) + (10,20)、(20,30)间隙(间隙锁)
事务 2 插入 age=15((10,20) 间隙)→ 阻塞;
事务 2 更新 age=20 的行 → 阻塞;
事务 2 插入 age=25((20,30) 间隙)→ 阻塞
按锁模式分类
共享锁
多个事务可同时持有(S-S 兼容)
但阻塞排他锁(S-X 互斥),即「可读不可写」
触发方式是怎么样的?
隐式:无(InnoDB 读操作默认通过 MVCC 实现,不加 S 锁,避免阻塞);
显式:SELECT ... LOCK IN SHARE MODE(手动加锁,事务结束释放)
排他锁
仅允许一个事务持有(X-X、X-S 均互斥),阻塞所有其他读写操作,即「独占读写权」
怎么触发的?
隐式:InnoDB 事务内的 INSERT/UPDATE/DELETE 自动加「行级 X 锁」(基于索引定位记录);
显式 SELECT ... FOR UPDATE(手动加行级 X 锁)LOCK TABLES t WRITE(手动加表级 X 锁)
X 锁是并发瓶颈核心,需要优化
需尽量缩小锁定范围,用行锁而非表锁
减少持有时间,事务尽量短
按实现策略分类
悲观锁
InnoDB 行锁、表锁 都是属于悲观锁
适用场景:写操作多、冲突概率高的场景
优点:逻辑简单,无需处理冲突重试,数据一致性强;
缺点:锁开销存在,高并发下可能出现阻塞、死锁,需优化索引和事务大小
乐观锁
假设并发冲突极少,不加锁直接操作
提交时通过版本号/ 时间戳 检测冲突,冲突则重试
适用场景:读操作多、写操作少、冲突概率低的场景
优点:无锁开销,并发能力极强,无死锁风险;
缺点:需业务层处理重试逻辑,冲突频繁时会导致大量重试
版本号法
表新增 version 字段,更新时 UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?
成功则版本号递增,失败则重试;
时间戳法
表新增 update_time 字段(TIMESTAMP 类型)
更新时 WHERE id=? AND update_time=?,对比时间戳是否一致。
锁等待
锁等待与死锁的区别
锁等待:单向阻塞(T2 等 T1,T1 不依赖 T2),超时后会自动终止,无数据不一致风险;
死锁:双向循环阻塞(T1 等 T2 的锁,T2 等 T1 的锁),InnoDB 会检测到并主动终止其中一个事务(触发 deadlock_detected),可能导致数据回滚。
锁等待产生的核心原因
资源竞争:高并发更新同一行 / 范围数据被频繁修改(如秒杀库存、订单状态更新);
锁粒度不当:无索引/没命中索引导致行锁降级为表锁,引发全表阻塞;
长事务:事务持有锁时间过长(如事务内包含 IO 操作、第三方接口调用);
锁类型冲突:如事务 A 持有 S 锁,事务 B 请求 X 锁(S-X 互斥);
间隙锁 / 临键锁:RR 隔离级别下,范围查询触发间隙锁,导致插入 / 更新阻塞
锁等待分析工具
查看所有活跃事务
深入分析锁与事务详情
更底层的锁信息(如锁结构、事务日志)
锁等待优化最佳实践
索引优化:避免行锁降级为表锁
事务优化:缩短锁持有时间
隔离级别选择高并发读写场景(如电商):选择 RC 隔离级别,关闭间隙锁 / 临键锁,减少锁冲突;数据一致性要求高的场景(如金融):选择 RR 隔离级别,接受少量锁冲突,确保无幻读。
锁策略优化:减少资源竞争分散锁竞争:将热点数据拆分(如库存表按商品 ID 分表),避免同一行数据被高频修改;乐观锁替代悲观锁:读多写少场景(如商品详情查询),用版本号 / 时间戳实现乐观锁,避免悲观锁竞争。
死锁
死锁产生的 4 个必要条件
互斥条件:锁是排他的(如 X 锁),同一时间只能被一个事务持有;
持有并等待:事务持有一个锁后,未释放又请求新的锁;
不可剥夺条件:事务持有的锁不能被其他事务强制剥夺,只能主动释放;
循环等待条件:多个事务形成「A 等 B → B 等 C → C 等 A」的循环依赖。
死锁排查工具
核心工具
日志关键信息解读
关键信息提取:
两个事务的 TRANSACTION ID(12345、67890);
各自执行的 SQL(均为插入订单);
「持有锁(HOLDS THE LOCK (S))」:锁类型(记录锁 / 间隙锁)、索引名、锁模式(X 锁);
「等待锁(WAITS FOR THIS LOCK (S))」:循环等待的锁资源;
「ROLL BACK TRANSACTION」:被终止的事务(需关注该事务对应的业务是否需要重试)。
日志工具
InnoDB 会将死锁日志写入 MySQL 错误日志,即使重启数据库也不会丢失,适合长期分析
查看错误日志路径:show variables like 'log_error';
日志中搜索 DEADLOCK 关键字,可找到历史死锁记录;
建议开启 innodb_print_all_deadlocks=ON(默认 OFF),让 InnoDB 记录所有死锁(而非仅最近一次)
排查流程
步骤 1:发现死锁
业务层面:部分请求失败(如订单创建失败、转账失败),日志中出现「Deadlock found when trying to get lock; try restarting transaction」;
数据库层面:执行 show engine innodb status\G 发现「LATEST DETECTED DEADLOCK」记录。
步骤 2:提取死锁核心信息
参与死锁的所有事务(TRANSACTION ID);
每个事务执行的 SQL 语句(明确业务操作);
每个事务持有锁和等待锁的类型(记录锁 / 间隙锁)、索引名、锁模式(X/S);
被 InnoDB 终止的事务(WE ROLL BACK TRANSACTION)
步骤 3:分析死锁产生的条件
对照死锁的 4 个必要条件,判断哪个条件可被打破:
是「循环等待」(如两个事务交叉锁不同行)?
是「锁粒度不当」(如间隙锁导致范围冲突)?
是「持有并等待」(如事务持有锁后未释放,又请求新锁)?
步骤 4:确定解决方案
根据根因,选择对应的解决方案
统一锁顺序
优化索引
降低隔离级别
常见死锁场景与解决方案
交叉锁
事务间交叉请求同一组行锁
现象是什么?
两个事务同时更新 / 删除两组数据,但锁顺序相反,形成循环等待
事务 T1
事务 T2
结果:T1 等 T2 的 user_id=2 锁,T2 等 T1 的 user_id=1 锁,触发死锁。
解决方案是什么?
核心方案:统一锁顺序所有事务对同一组资源的锁定顺序保持一致(如按 user_id 升序锁定);优化后 T1/T2 均先锁 user_id=1,再锁 user_id=2,避免循环等待;
辅助方案:合并 SQL 将多个更新合并为一条 SQLUPDATE user SET balance=CASE user_id WHEN 1 THEN balance-100 WHEN 2 THEN balance+100 END WHERE user_id IN (1,2))仅持有一次锁,避免分步请求
间隙锁 / 临键锁冲突
现象是什么?
RR 隔离级别下,范围查询触发间隙锁 / 临键锁,多个事务插入同一间隙时形成死锁
表 order 有非唯一索引 idx_create_time,数据 create_time 为 [10:00, 10:30, 11:00];
事务 T1SELECT * FROM order WHERE create_time BETWEEN '10:00' AND '11:00' FOR UPDATE 触发临键锁,锁定间隙 (10:30, 11:00);
事务 T2INSERT INTO order (create_time) VALUES ('10:45')请求插入意向锁,被 T1 的临键锁阻塞;
事务 T1 继续执行 INSERT INTO order (create_time) VALUES ('10:50')请求插入意向锁,被 T2 的插入意向锁阻塞(循环等待),触发死锁
死锁通用方案
打破「循环等待条件」:统一锁顺序所有事务对同一组资源(表、行、索引)的锁定顺序保持一致
打破「互斥条件」:用乐观锁替代悲观锁适用场景:读多写少、冲突概率低
优化锁粒度:缩小锁定范围能锁行不锁表,能锁单行不锁范围
其他通用方案开启死锁检测,默认 innodb_deadlock_detect=ON 设置合理的锁超时:innodb_lock_wait_timeout(默认 50 秒),缩短锁等待时间,减少死锁概率事务重试机制:业务层捕获死锁异常,实现幂等重试
总结
今天把四大金刚之三的锁 好好研究了一遍~~
总结了锁的分类,按颗粒度,按锁模式,按实现策略等待
还总结了一下锁等待,产生的原因,分析的工具,最佳实践等
以及死锁 条件,排查工具,流程等~
明天把 最后一个金刚:事务 好好研究一下~~
如果觉得帮到你们,烦请加个点赞关注推荐 三连,感谢感谢~~
熟练度刷不停,知识点吃透稳,下期接着练~
版权声明: 本文为 InfoQ 作者【DonaldCen】的原创文章。
原文链接:【http://xie.infoq.cn/article/55fa9186af116fc830a4e6a1b】。文章转载请联系作者。







评论