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

64 阅读12分钟

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

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

2043539.jpg

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

  • 锁分类:表锁/行锁/共享锁/排他锁 作用,原理
  • 锁等待:锁等待原因,排查工具,分析工具,最优实践
  • 死锁:产生条件,排查工具,排查流程,解决方案

平常大多数需求都是使用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 Lock
BEGIN;
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 0x7f1234567890
TRANSACTION:
TRANSACTION 12345, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK 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 update
INSERT INTO order (user_id, product_id, status) VALUES (100, 200, 'PENDING')  -- 事务1执行的SQL
TRANSACTION 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 inserting
mysql tables in use 1, locked 1
LOCK 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 update
INSERT INTO order (user_id, product_id, status) VALUES (200, 100, 'PENDING')  -- 事务2执行的SQL
TRANSACTION 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

    • 将多个更新合并为一条 SQL
    • UPDATE 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];

  • 事务 T1

    • SELECT * FROM order WHERE create_time BETWEEN '10:00' AND '11:00' FOR UPDATE
    • 触发临键锁,锁定间隙 (10:30, 11:00);
  • 事务 T2

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

总结

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

  • 总结了锁的分类,按颗粒度,按锁模式,按实现策略等待
  • 还总结了一下锁等待,产生的原因,分析的工具,最佳实践等
  • 以及死锁 条件,排查工具,流程等~

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

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

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