写点什么

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

作者:DonaldCen
  • 2025-12-05
    广东
  • 本文字数:5245 字

    阅读完需:约 17 分钟

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

大家好,我是程序员强子。

前面学习了 Mysql 四大金刚的 索引 & 日志,今天专注把 相关给弄明白~

来看看今天学习的知识点:

  • 锁分类:表锁/行锁/共享锁/排他锁 作用,原理

  • 锁等待:锁等待原因,排查工具,分析工具,最优实践

  • 死锁:产生条件,排查工具,排查流程,解决方案

平常大多数需求都是使用 InnoDB 引擎,因此锁也是只讨论 InnoDB 相关的~

系好安全带,发车啦~

按锁粒度分类

表级锁

分类

  • 意向锁(IS/IX 锁)

  • 元数据锁(MDL 锁)

  • 自增锁(AUTO-INC 锁)

意向锁

表级标记锁 ,标记表内是否有行锁

解决 表锁等待行锁释放 的效率问题

自动加锁 / 释放,无需手动干预

想象一下,如果没有意向锁:

加表锁时需要扫描表中所有行,确认是否有行级锁!!!

如果表数据量还很大的话,效率会有多低,不敢想~

触发场景:

  • 加行级 S 锁前 , 自动加 IS 锁

  • 加行级 X 锁前 ,自动加 IX 锁

行级锁

有哪些分类?

  • 记录锁(S/X 锁)

  • 间隙锁(GAP 锁)

  • 临键锁(Next-Key 锁)

行锁不是简单锁定 数据行,而是通过 索引 锁定 索引记录或区间

记录锁

  • 核心逻辑:仅锁定某一行的索引记录,不影响其他行,是最常用的行锁。

  • 触发条件:通过唯一索引(主键 / 唯一键) 精准匹配单行(如 WHERE id=1,id 是主键)


案例:

-- id是主键(唯一索引),触发记录锁BEGIN;UPDATE user SET name='a' WHERE id=1; -- 仅锁定id=1的行
复制代码

s

  • 只是开始,还没 commit;

  • 此时其他事务更新 id=1 会被阻塞,但更新 id=2 不受影响

间隙锁

  • 核心逻辑:锁定索引记录之间的 间隙(不含记录本身),防止其他事务在间隙中插入数据,解决 幻读隐患

  • 触发条件非唯一索引 上进行范围查询(如 BETWEEN、>、<)唯一索引范围查询(如 id > 10,id 是主键)

案例非唯一索引的间隙锁,表 user 有非唯一索引 idx_age(age),数据 age 为 [10, 20, 30]

-- 事务1:查询age在10-30之间,触发间隙锁BEGIN;SELECT * FROM user WHERE age BETWEEN 10 AND 30 FOR UPDATE;

复制代码
  • 锁定的间隙:(-∞,10)、(10,20)、(20,30)、(30,+∞)

  • 事务 2 尝试插入 age=15(在 (10,20) 间隙中)会被阻塞,直到事务 1 提交

临键锁

  • 核心逻辑:InnoDB 默认的行锁算法(RR 级别下),是 记录锁 + 间隙锁 的组合,既锁记录本身,也锁记录前后的间隙,彻底防止幻读

  • 触发条件:RR 级别下,通过非唯一索引进行查询

案例:Next-Key Lock 阻塞插入,更新表 user 非唯一索引 idx_age(age),数据 age=20

-- 事务1:查询age=20,触发Next-Key LockBEGIN;SELECT * FROM user WHERE age=20 FOR UPDATE;
复制代码
  • 锁定: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-XX-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 隔离级别下,范围查询触发间隙锁,导致插入 / 更新阻塞

锁等待分析工具

查看所有活跃事务

show processlist;
复制代码

深入分析锁与事务详情

show engine innodb status\G;
复制代码

更底层的锁信息(如锁结构事务日志

锁等待优化最佳实践

  • 索引优化:避免行锁降级为表锁

  • 事务优化:缩短锁持有时间

  • 隔离级别选择高并发读写场景(如电商):选择 RC 隔离级别,关闭间隙锁 / 临键锁,减少锁冲突;数据一致性要求高的场景(如金融):选择 RR 隔离级别,接受少量锁冲突,确保无幻读。

  • 锁策略优化:减少资源竞争分散锁竞争:将热点数据拆分(如库存表按商品 ID 分表),避免同一行数据被高频修改;乐观锁替代悲观锁:读多写少场景(如商品详情查询),用版本号 / 时间戳实现乐观锁,避免悲观锁竞争。

死锁

死锁产生的 4 个必要条件

  • 互斥条件:锁是排他的(如 X 锁),同一时间只能被一个事务持有;

  • 持有并等待:事务持有一个锁后,未释放又请求新的锁;

  • 不可剥夺条件:事务持有的锁不能被其他事务强制剥夺,只能主动释放;

  • 循环等待条件:多个事务形成「A 等 B → B 等 C → C 等 A」的循环依赖。

死锁排查工具

核心工具

show engine innodb status\G;
复制代码

日志关键信息解读

LATEST DETECTED DEADLOCK------------------------2025-12-02 10:00:00 0x7f1234567890TRANSACTION:TRANSACTION 12345, ACTIVE 0 sec insertingmysql tables in use 1, locked 1LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)MySQL thread id 10, OS thread handle 12345, query id 6789 localhost root updateINSERT INTO order (user_id, product_id, status) VALUES (100, 200, 'PENDING')  -- 事务1执行的SQLTRANSACTION HOLDS THE LOCK(S):  -- 事务1持有的锁RECORD LOCKS space id 5 page no 3 n bits 72 index idx_user_id of table `test`.`order` trx id 12345 lock_mode X locks gap before rec  -- 持有 idx_user_id 索引的间隙X锁RECORD LOCKS space id 5 page no 3 n bits 72 index PRIMARY of table `test`.`order` trx id 12345 lock_mode X locks rec but not gap  -- 持有主键的记录X锁TRANSACTION WAITS FOR THIS LOCK(S):  -- 事务1等待的锁RECORD LOCKS space id 5 page no 4 n bits 72 index idx_product_id of table `test`.`order` trx id 12345 lock_mode X locks gap before rec  -- 等待 idx_product_id 索引的间隙X锁
TRANSACTION:TRANSACTION 67890, ACTIVE 0 sec insertingmysql tables in use 1, locked 1LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)MySQL thread id 11, OS thread handle 67890, query id 6790 localhost root updateINSERT INTO order (user_id, product_id, status) VALUES (200, 100, 'PENDING') -- 事务2执行的SQLTRANSACTION HOLDS THE LOCK(S): -- 事务2持有的锁RECORD LOCKS space id 5 page no 4 n bits 72 index idx_product_id of table `test`.`order` trx id 67890 lock_mode X locks gap before rec -- 持有 idx_product_id 索引的间隙X锁RECORD LOCKS space id 5 page no 4 n bits 72 index PRIMARY of table `test`.`order` trx id 67890 lock_mode X locks rec but not gap -- 持有主键的记录X锁TRANSACTION WAITS FOR THIS LOCK(S): -- 事务2等待的锁RECORD LOCKS space id 5 page no 3 n bits 72 index idx_user_id of table `test`.`order` trx id 67890 lock_mode X locks gap before rec -- 等待 idx_user_id 索引的间隙X锁
WE ROLL BACK TRANSACTION 67890 -- InnoDB 终止的事务
复制代码

关键信息提取:

  • 两个事务的 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 记录所有死锁(而非仅最近一次)

set global innodb_print_all_deadlocks=ON;  -- 重启失效,需写入 my.cnf
复制代码

排查流程

步骤 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

// 持有 user_id=1 的 X 锁UPDATE user SET balance=balance-100 WHERE user_id=1 ;// 请求 user_id=2 的 X 锁;UPDATE user SET balance=balance+100 WHERE user_id=2 ;
复制代码

事务 T2

//持有 user_id=2 的 X 锁UPDATE user SET balance=balance-100 WHERE user_id=2;//请求 user_id=1 的 X 锁;UPDATE user SET balance=balance+100 WHERE user_id=1;
复制代码

结果: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 秒),缩短锁等待时间,减少死锁概率事务重试机制:业务层捕获死锁异常,实现幂等重试

总结

今天把四大金刚之三的 好好研究了一遍~~

  • 总结了锁的分类,按颗粒度,按锁模式,按实现策略等待

  • 还总结了一下锁等待,产生的原因,分析的工具,最佳实践等

  • 以及死锁 条件,排查工具,流程等~

明天把 最后一个金刚:事务 好好研究一下~~

如果觉得帮到你们,烦请加个点赞关注推荐 三连,感谢感谢~~

熟练度刷不停,知识点吃透稳,下期接着练~

发布于: 刚刚阅读数: 3
用户头像

DonaldCen

关注

有个性,没签名 2019-01-13 加入

跟我在峡谷学Java 公众号:程序员悟空的宝藏乐园

评论

发布
暂无评论
Java 王者修炼手册【Mysql 篇 - 锁】:吃透 MySQL 行锁 + 间隙锁 + 意向锁 底层机制,了解死锁解决方案_死锁_DonaldCen_InfoQ写作社区