1. [深度解密] 锁机制详解¶
数据库锁机制是面试中区分“初级工程师”与“高级工程师”的分水岭。很多同学只知道“行锁”和“表锁”,但对于它们背后的协作机制、加锁规则以及死锁问题知之甚少。本章将带你深入 MySQL 锁的世界。
1.1 锁的全局观¶
在 MySQL 中,锁可以按照粒度粗略分为三类:全局锁、表级锁**和**行级锁。
- 全局锁:锁定整个数据库实例,通常用于全库逻辑备份。命令是
Flush tables with read lock (FTWRL)。 - 表级锁:锁定整张表,开销小,加锁快,不会出现死锁;但锁定粒度大,发生锁冲突的概率最高,并发度最低。MyISAM 引擎默认使用表级锁。
- 行级锁:锁定某一行数据,开销大,加锁慢,会出现死锁;但锁定粒度最小,发生锁冲突的概率最低,并发度也最高。InnoDB 引擎默认使用行级锁。
1.2 表级锁:不仅仅是 Lock Tables¶
除了显式的 LOCK TABLES 命令外,MySQL 还有两种非常重要的表级锁:元数据锁 (MDL) 和 意向锁 (Intention Lock)。
1.2.1 元数据锁 (MDL)¶
MDL (Metadata Lock) 不需要显式使用,在访问一个表的时候会被自动加上。它的作用是保证读写的正确性。 * 当对一个表做增删改查操作时,加 MDL 读锁。 * 当要对表做结构变更操作(如 ALTER TABLE)时,加 MDL 写锁。
面试坑点:给一个小表加字段,为什么会导致整个库挂掉?
解析:MDL 锁是系统默认加的。如果一个长事务正在查询表 A(持有 MDL 读锁),此时你执行 ALTER TABLE A,由于写锁优先级高,它会阻塞后续所有的读写操作,导致该表完全不可用,甚至拖垮整个库。
1.2.2 意向锁 (Intention Lock)¶
这是面试中极容易被忽视的一个概念。
问题:假设事务 A 获取了表中某一行数据的排他锁(行锁),此时事务 B 想要获取整张表的排他锁(表锁),它该怎么办? 常规思路:事务 B 需要遍历整张表,查看每一行是否被锁住。这效率太低了!
意向锁的解决方案: 意向锁是由 InnoDB 引擎自己维护的,用户无法手动操作。 * 意向共享锁 (IS):事务打算给数据行加行级共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。 * 意向排他锁 (IX):事务打算给数据行加行级排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
核心作用:意向锁是**表级锁**。它的存在是为了**快速判断表中是否有记录被加锁**。当事务 B 想要加表锁时,只需要检查表上是否有意向锁,而不需要遍历每一行。
1.3 行级锁:InnoDB 的杀手锏¶
InnoDB 的行锁是通过给**索引**上的索引项加锁来实现的。这意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!
1.3.1 共享锁 (S锁) 与 排他锁 (X锁)¶
- 共享锁 (Shared Lock, S锁):又称读锁。允许一个事务去读一行,阻止其他事务获得该行的排他锁。
- 用法:
SELECT ... LOCK IN SHARE MODE;
- 用法:
- 排他锁 (Exclusive Lock, X锁):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务获得该行的共享锁和排他锁。
- 用法:
SELECT ... FOR UPDATE;或UPDATE,DELETE,INSERT语句自动加 X 锁。
- 用法:
兼容性矩阵:
| 锁类型 | S锁 (共享锁) | X锁 (排他锁) |
|---|---|---|
| S锁 | 兼容 | 冲突 |
| X锁 | 冲突 | 冲突 |
1.4 幻读的克星:间隙锁与 Next-Key Lock¶
在可重复读 (Repeatable Read) 隔离级别下,InnoDB 为了解决**幻读**问题,引入了更复杂的锁机制。
1.4.1 什么是幻读?¶
幻读是指在同一个事务中,前后两次查询同一个范围,后一次查询看到了前一次查询没有看到的行(通常是其他事务新插入的行)。
1.4.2 间隙锁 (Gap Lock)¶
间隙锁锁定的是两个索引记录之间的**间隙**,或者是第一条记录之前的间隙,或者是最后一条记录之后的间隙。 * 作用:防止其他事务在这个间隙中插入新记录。 * 特性:间隙锁之间是不冲突的。多个事务可以同时持有同一个间隙的间隙锁。
1.4.3 临键锁 (Next-Key Lock)¶
临键锁是 行锁 + 间隙锁 的组合。它既锁定了记录本身,也锁定了记录之前的间隙。
* 区间:Next-Key Lock 锁定的是一个**前开后闭**的区间,例如 (5, 10]。
* 默认行为:InnoDB 在 RR 隔离级别下,默认使用 Next-Key Lock 进行加锁。
案例解析:
假设表 t 中有记录 id = 5, 10, 15。
执行 SELECT * FROM t WHERE id > 5 AND id < 15 FOR UPDATE;
InnoDB 会加上 Next-Key Lock:(5, 10] 和 (10, 15]。
此时,插入 id = 7 或 id = 12 都会被阻塞,从而避免了幻读。
1.5 死锁与解决方案¶
什么是死锁? 死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
经典死锁案例: * 事务 A 锁住了行 1,想要申请行 2 的锁。 * 事务 B 锁住了行 2,想要申请行 1 的锁。 * 两者互相等待,无休无止。
解决方案:
1. 超时等待:设置 innodb_lock_wait_timeout 参数。当等待时间超过阈值时,其中一个事务回滚。
2. 死锁检测(推荐):开启 innodb_deadlock_detect = on。InnoDB 会主动检测死锁环,一旦发现死锁,会选择回滚持有最少排他锁的事务,让其他事务得以执行。
如何避免死锁? * 以固定的顺序访问表和行。 * 大事务拆小,缩短持有锁的时间。 * 在同一个事务中,尽可能一次锁定所有需要的资源。