MySQL 间隙锁详解与实战分析
间隙锁(Gap Lock)是 InnoDB 存储引擎中用于解决并发事务中幻读问题的一种重要机制。它作用于索引记录之间的"间隙",也可作用于第一条记录之前或最后一条记录之后的空间。
间隙锁的核心特性
间隙锁并不锁定具体的某条记录,而是锁定记录之间的空间或者边界区域,从而防止其他事务在此区间内插入新数据,进而避免幻读的发生。
间隙锁的应用场景
考虑如下 SQL:
SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE;
这条语句不仅会对已有数据加锁,还会对 10 到 20 之间的间隙加锁,即使这些值并不存在于表中。这意味着任何试图插入值为 15 的操作都会被阻塞。
间隙锁的作用范围
- 记录间间隙:两个相邻索引记录之间的空间。
- 起始前间隙:第一条记录之前的无限空间。
- 结尾后间隙:最后一条记录之后的无限空间。
间隙锁与索引类型的关系
当使用唯一索引进行精确查找时,InnoDB 只会加记录锁(Record Lock),不会触发间隙锁。例如:
SELECT * FROM child WHERE id = 100;
但如果该字段不是唯一索引,则仍会施加间隙锁。
不同隔离级别下的处理方式
| 隔离级别 | 是否启用间隙锁 | 说明 |
|---|---|---|
| READ UNCOMMITTED | 否 | 允许脏读,无需间隙锁支持。 |
| READ COMMITTED | 否 | 仅锁定实际访问的数据行。 |
| REPEATABLE READ | 是 | 默认使用间隙锁来防止幻读。 |
| SERIALIZABLE | 间接实现 | 通过表级锁替代间隙锁,完全串行化执行。 |
为什么需要间隙锁?
为了保证在可重复读隔离级别下,同一事务多次读取相同范围的结果保持一致,避免出现新增数据导致的"幽灵"现象。例如:
SELECT * FROM employees WHERE age > 30 FOR UPDATE;
此语句将锁定所有 age 大于 30 的记录及它们之间的间隙,防止其他事务插入满足条件的新纪录。
间隙锁的影响
- 性能影响:增加系统开销,特别是在频繁插入数据的环境中。
- 并发性下降:部分写入操作会被挂起等待锁释放。
实验验证:间隙锁的行为
以下是一组测试用例,演示间隙锁的具体行为:
环境准备
-- 表结构示例
CREATE TABLE orders (
id INT PRIMARY KEY,
order_amount DECIMAL(10,2),
create_date DATETIME
);
案例一:基础范围查询
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 3 AND 7 FOR SHARE;
此时会锁定 id 为 3 和 7 之间的间隙,同时也会锁定 id=7 后的所有空间(若它是最大值)。尝试插入 id=4 或 id=8 将被阻塞。
案例二:小于某个值的查询
BEGIN;
SELECT * FROM orders WHERE id < 3 FOR SHARE;
这次会锁定从负无穷到 id=3 的整个区间,包括前面的所有间隙。插入 id=-5 会被拒绝。
案例三:非边界情况
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 3 AND 7 FOR SHARE;
如果表中还存在更大的 id 值(如 10),则 id=7 之后的间隙不会被锁定。
结论
间隙锁主要用于防止并发插入带来的幻读问题,在 REPEATABLE READ 隔离级别中尤为重要。但其副作用是可能降低系统的并发能力,因此在设计数据库应用时需权衡利弊。