
开发同学在工作中经常遇到这些问题:数据库明明没报错,为什么接口突然卡顿?为什么简单的更新语句一直未运行完毕?为什么数据会莫名其妙多出或丢失?
这些问题的核心根源,90%都和MySQL锁机制相关。锁是MySQL实现事务隔离、保证数据安全、处理并发操作的核心机制。
本文就一起探讨一下表锁、行锁、间隙锁、元数据锁(MDL)用途及避坑点等(本文基于RR隔离级别进行举例)。
一、锁的核心分类与基础规则
MySQL锁按照锁定粒度(范围大小),从大到小分为三类,再加上特殊的元数据锁,构成全部核心锁体系:
同时所有锁都遵循两个基础规则,新手一定要牢记:
补充核心知识点:行锁、间隙锁是InnoDB引擎专属,MyISAM引擎只有表锁,这也是InnoDB能支持事务、并发更强的核心原因。
二、表锁:最简单、最粗暴的全表锁定
1. 通俗概念
表锁就是一旦加锁,锁住整张数据表。不管你只改一行数据,还是查一行数据,整张表都会被锁定,其他事务必须排队等待。
2. 核心特点
3. 表锁的两种类型
表锁分为读锁(共享锁)和写锁(排他锁):
4. 案例演示
我们新建一张测试表,用于所有案例演示:
-- 创建测试表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO users(name,age) VALUES ('张三',18),('李四',20),('王五',22);
案例1:手动加表读锁
-- 事务1:给users表加读锁
LOCK TABLES users READ;执行后:本事务和其他事务都可以正常查询数据,但无法修改、插入、删除数据。
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> LOCK TABLES users READ;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from users;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 张三 | 18 |
| 2 | 李四 | 20 |
| 3 | 王五 | 22 |
+----+--------+------+
3 rows in set (0.00 sec)
mysql> update users set age=19 where id =1;
ERROR 1099 (HY000): Table 'users' was locked with a READ lock and can't be updated
本事务:

其他事务:

案例2:手动加表写锁
加表级写锁前,所有的其他事务必须结束(提交或回滚),也不能有加读锁的操作,否则加不少表级写锁。
-- 解锁命令
UNLOCK TABLES;
-- 事务1:给users表加写锁
LOCK TABLES users WRITE;
执行后:只有事务1能读写表数据,其他所有事务的查询、修改、新增全部阻塞,直到锁释放。

5. 避坑点
在MySQL默认的事务隔离级别可重复读(Repeatable-Read)级别下的InnoDB引擎的表,如果SQL语句没有走索引,行锁会自动升级为表锁!这是新手开发最常踩的坑。建议生产环境在能接受的情况下事务隔离级别使用读已提交级别(READ-COMMITTED)。
举例:update users set age=19 where name='张三'(name无索引),看似只改一行,实际锁住全表,导致整个表的写阻塞,其他事务无法更新其他记录的数据。

6. 适用场景
数据量极小、并发极低、整表操作场景,比如全表备份、批量初始化数据,日常业务开发极少使用。
三、行锁:InnoDB高并发的核心(重点)
1. 通俗概念
行锁就是只锁住需要操作的某一行数据,其他行的数据完全不受影响,其他事务可以正常操作表中其他数据,并发能力极强。
2. 核心前提
行锁必须依赖索引!!!如果操作语句没有命中索引,InnoDB无法定位具体行,行锁直接升级为表锁,并发直接失效。
3. 行锁的两种类型
4. 实操案例
以下所有行锁案例,默认开启事务(MySQL默认手动事务需开启),事务不提交,锁不会释放。
案例1:排他锁(增删改默认触发)
事务1执行(开启事务,不提交):
begin; -- 或者用begin;
UPDATE users SET age=19 WHERE id=1; -- id是主键索引,仅锁id=1这一行此时事务2执行:
begin;
UPDATE users SET age=25 WHERE id=1; -- 阻塞,被行锁锁住
UPDATE users SET age=25 WHERE id=2; -- 正常执行,不受影响
结论:行锁只锁当前操作行,不影响其他行,并发性能优异。
案例2:手动加共享锁
事务1:
begin;
SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE; -- 手动加读锁
事务2:
begin;
SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE; -- 正常执行,读锁共享
UPDATE users SET age=20 WHERE id=1; -- 阻塞,读锁阻塞写操作
提交事务释放锁:
COMMIT;5. 高频坑点
6. 适用场景
日常99%的业务并发场景,比如用户修改资料、订单状态更新、库存扣减等,是InnoDB默认的锁机制。
四、间隙锁:解决幻读的特殊锁
1. 先搞懂:什么是幻读?
幻读就是同一个事务内,两次相同的查询,结果行数不一样,莫名多出新数据。
举例:事务1查询age在18-22的数据,查出3条;事务2偷偷插入一条新数据并提交;事务1再次查询,多出1条数据,就像“幻觉”一样。
2. 间隙锁通俗概念
行锁是“锁已有数据行”,间隙锁是“锁数据之间的空白区间”,不锁已有数据,只禁止其他事务在这个区间插入新数据,专门用来彻底解决幻读问题。
核心前提:仅InnoDB、RR(可重复读)隔离级别生效(MySQL默认隔离级别),RC隔离级别没有间隙锁。
3. 配套知识点:临键锁
InnoDB RR级别默认使用临键锁,临键锁 = 行锁 + 间隙锁,既锁当前行,又锁前后间隙,是范围查询的默认锁机制。
4. 实操案例
实操案例可以参考历史文章
5. 常见踩坑点
范围查询(>、<、between、like)会触发间隙锁/临键锁,锁定范围远大于查询数据,容易造成莫名阻塞,这是很多线上卡顿的核心原因。
6. 作用总结
牺牲微小的并发性能,彻底解决RR隔离级别的幻读问题,保证事务查询结果一致性。
五、元数据锁(MDL):表结构的隐形守护者
1. 通俗概念
很多人只知道行锁、表锁,却忽略了MDL元数据锁。
MDL锁是MySQL自动加的表级锁,不需要手动操作,只要你对表做读写操作,就会自动加MDL锁,用来防止“一边读写数据,一边修改表结构”导致的数据错乱。
2. 核心规则
3. 经典实操坑点案例
场景:事务1开启后,查询users表,但是迟迟不提交事务
begin;
SELECT * FROM users; -- 加MDL读锁,不释放此时DBA执行改表语句:
ALTER TABLE users ADD COLUMN phone VARCHAR(11);结果:改表语句永久阻塞,后续所有查询、更新users表的接口全部卡死!

原因:长事务持有MDL读锁不释放,MDL写锁(改表)无法获取,形成死阻塞。
4. 避坑点
六、 总结
1. 常见问题总结
Q1. 为什么有行锁还需要间隙锁?
A:行锁只能锁住已存在的数据,无法阻止其他事务插入新数据,间隙锁专门填补这个空白,彻底解决幻读。
Q2. 为什么没索引会变成表锁?
A: 行锁依赖索引定位具体行,无索引时MySQL不知道锁哪一行,就回锁定全表,保证数据安全。
Q3. MDL锁可以手动关闭吗?
A: 不可以,MDL是MySQL底层强制机制,目的是保护表结构安全,需要通过优化事务时长避免阻塞;大表做变更时(且不能online DDL时)建议使用pt-osc等工具操作
2. 避坑总结