大家好,我是程序员强子。
前面学习了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 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-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 隔离级别下,范围查询触发间隙锁,导致插入 / 更新阻塞
锁等待分析工具
查看所有活跃事务
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 秒),缩短锁等待时间,减少死锁概率
- 事务重试机制:业务层捕获死锁异常,实现幂等重试
总结
今天把四大金刚之三的锁 好好研究了一遍~~
- 总结了锁的分类,按颗粒度,按锁模式,按实现策略等待
- 还总结了一下锁等待,产生的原因,分析的工具,最佳实践等
- 以及死锁 条件,排查工具,流程等~
明天把 最后一个金刚:事务 好好研究一下~~
如果觉得帮到你们,烦请加个点赞关注推荐 三连,感谢感谢~~
熟练度刷不停,知识点吃透稳,下期接着练~