跳转至

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 = 7id = 12 都会被阻塞,从而避免了幻读。

1.5 死锁与解决方案

什么是死锁? 死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象。

经典死锁案例: * 事务 A 锁住了行 1,想要申请行 2 的锁。 * 事务 B 锁住了行 2,想要申请行 1 的锁。 * 两者互相等待,无休无止。

解决方案: 1. 超时等待:设置 innodb_lock_wait_timeout 参数。当等待时间超过阈值时,其中一个事务回滚。 2. 死锁检测(推荐):开启 innodb_deadlock_detect = on。InnoDB 会主动检测死锁环,一旦发现死锁,会选择回滚持有最少排他锁的事务,让其他事务得以执行。

如何避免死锁? * 以固定的顺序访问表和行。 * 大事务拆小,缩短持有锁的时间。 * 在同一个事务中,尽可能一次锁定所有需要的资源。