在数据库系统中,锁是协调多线程并发访问资源的核心机制。对于MySQL的InnoDB存储引擎而言,锁的实现尤为复杂且精妙。它不仅需要保证数据的一致性,还要在高并发场景下尽可能提升性能。今天,我们就由浅入深,通过大量案例拆解,把InnoDB的锁机制彻底讲明白。
提示:本文基于MySQL 5.7及8.0版本,默认隔离级别为可重复读(REPEATABLE READ)。
一、为什么需要锁?——从宏观视角理解锁的意义
在计算机世界里,锁是一种协调多个进程或线程并发访问同一资源的机制。在数据库中,数据本身就是被所有用户共享的核心资源。如何保证数据在并发访问时的一致性和有效性,是所有数据库必须解决的问题;同时,锁的粒度也直接影响着数据库的并发性能。
不同存储引擎的锁策略对比:
| 引擎 | 锁粒度 | 开销 | 加锁速度 | 死锁 | 并发度 |
|---|---|---|---|---|---|
| MyISAM | 表级锁 | 小 | 快 | 不会出现 | 低 |
| InnoDB | 行级锁(默认) | 大 | 慢 | 会出现 | 高 |
适用场景:
表级锁:更适合以查询为主、并发写入较少的应用(如报表系统)。
行级锁:更适合有大量并发更新、且每次只影响少量数据的应用(如电商交易系统)。
二、InnoDB 锁的基本类型:共享锁与排他锁(S锁和X锁)
InnoDB 实现了标准的行级锁,主要分为两种基本类型:
1. 共享锁(Shared Lock,简称S锁)
别称:读锁。
特性:允许持有锁的事务读取一行数据。
兼容性:多个事务可以同时持有同一行数据的S锁(S锁与S锁兼容)。
加锁方式:
SELECT ... LOCK IN SHARE MODE。
2. 排他锁(Exclusive Lock,简称X锁)
别称:写锁。
特性:允许持有锁的事务更新或删除一行数据。
兼容性:X锁与任何锁都不兼容(S锁和X锁均冲突)。
加锁方式:
自动:
DELETE、UPDATE、INSERT语句默认添加X锁。手动:
SELECT ... FOR UPDATE。
核心规则:
如果事务T1持有行r的S锁,另一个事务T2请求行r的X锁,T2必须等待。如果T1持有X锁,则T2无论请求S锁还是X锁都必须等待。
三、表级意向锁(Intention Locks)——多粒度锁定的桥梁
InnoDB 不仅支持行锁,也支持表锁(如LOCK TABLES ... WRITE)。为了实现多粒度锁定,InnoDB 引入了意向锁。意向锁是表级别的锁,由存储引擎内部自动维护,无需用户干预。
意向锁的类型:
意向共享锁(Intention Shared Lock,IS锁):事务打算在表中的某些行上加S锁(如
SELECT ... FOR SHARE)。意向排他锁(Intention Exclusive Lock,IX锁):事务打算在表中的某些行上加X锁(如
SELECT ... FOR UPDATE)。
表级锁兼容性矩阵
| 兼容性 | X | IX | S | IS |
|---|---|---|---|---|
| X | 冲突 | 冲突 | 冲突 | 冲突 |
| IX | 冲突 | 兼容 | 冲突 | 兼容 |
| S | 冲突 | 冲突 | 兼容 | 兼容 |
| IS | 冲突 | 兼容 | 兼容 | 兼容 |
设计意图:意向锁的主要目的是表明“有人正在锁定表中的某一行,或者即将锁定某一行”。当要加表锁(如X表锁)时,数据库只需检查是否存在IX或IS锁即可,无需逐行遍历检查是否有行锁,从而极大提升了效率。
四、揭开行锁的真面目:锁住的到底是什么?
这是很多开发者最困惑的问题。我们需要明白,InnoDB 的行锁实际上是索引记录锁(Index-Record Lock)。锁不是直接加在行记录上的,而是加在索引上的。
1. 有主键索引的情况
结论:锁住的是主键索引(聚簇索引)上的值。
验证:如果你
SELECT * FROM t2 WHERE id = 1 FOR UPDATE;,其他事务试图修改id=1或基于主键id=1的更新会被阻塞,但修改id=3不受影响。
2. 无主键/无索引的情况
现象:即使你只更新一条记录,也可能会锁住全表所有记录,导致并发能力急剧下降。
原因:如果表中没有索引,InnoDB 会创建一个隐藏的聚簇索引,并将全表的所有记录都锁定,以进行全表扫描。因此,给查询和更新建立合适的索引是防止锁冲突的关键。
3. 利用唯一索引(非主键)
结论:除了锁住唯一索引本身,还会锁住对应的聚簇索引(主键)。
示例:如果有
uk_name唯一索引,执行SELECT * FROM t3 WHERE name = '张三' FOR UPDATE;,不仅uk_name上的“张三”记录会被加锁,背后的主键id记录也会被加锁。这是为了防止在修改行时,其他事务通过主键删除该行。
五、锁的粒度与范围:Record、Gap 与 Next-Key
假设我们有一张表t4,id为主键,数据记录为:1, 4, 7, 10。
1. Record Lock(记录锁)
定义:锁定索引中的某一条具体记录。
触发条件:针对唯一性索引(主键/唯一键)进行等值查询,且记录存在。
示例:
SELECT * FROM t4 WHERE id = 4 FOR UPDATE;效果:锁住
id=4的这行记录,其他事务不能修改或删除它,但可以插入id=5(因为没有锁间隙)。
2. Gap Lock(间隙锁)
定义:锁定索引记录之间的间隙,或是第一条记录前、最后一条记录后的间隙。
核心作用:防止幻读(Phantom Read)。
触发条件:查询记录不存在。
关键特性:间隙锁之间是兼容的,即两个事务可以持有同一个间隙的锁,互不阻塞。
示例分析:
SELECT * FROM t4 WHERE id = 6 FOR UPDATE;(记录6不存在)锁定的间隙:
(4, 7)。效果:阻止其他事务在
(4, 7)之间插入数据(如id=5或id=6),但不影响查询或修改id=4和id=7的记录。SELECT * FROM t4 WHERE id > 20 FOR UPDATE;锁定的间隙:
(10, +∞),阻止任何大于10的数据插入。
3. Next-Key Lock(临键锁)
定义:记录锁(Record Lock)+间隙锁(Gap Lock)的组合。
锁定范围:左开右闭的区间
(上一个值, 当前值]。默认行为:在RR(可重复读)隔离级别下,InnoDB 默认使用 Next-Key Lock 来锁定扫描到的索引范围,以防止幻读。
示例:数据为 1, 4, 7, 10。
可能的 Next-Key 区间为:
(-∞, 1],(1, 4],(4, 7],(7, 10],(10, +∞)。
实战:
SELECT * FROM t4 WHERE id > 5 AND id < 9 FOR UPDATE;锁定的范围:锁定区间
(4, 7]和(7, 10]。效果:不仅锁住了记录
7和10,还锁住了(4,7)和(7,10)的间隙。所以,你无法插入6,也无法插入8或9。
4. Insert Intention Lock(插入意向锁)
定义:一种特殊的间隙锁,在
INSERT操作之前设置。设计意图:提高并发插入的性能。
原理:如果两个事务要在同一个间隙(如
(4,7))中插入数据,但插入的位置不同(例如插入5和6),它们各自持有插入意向锁,不需要互相等待,可以并发插入(除非间隙上有其他冲突的锁存在)。
六、不同SQL语句的加锁规则总结(基于RR隔离级别)
根据MySQL官方文档的说明,不同的SQL语句在加锁时有着严格的规则:
| SQL操作 | 锁类型 | 说明 |
|---|---|---|
| 普通SELECT | 无锁 | 快照读(MVCC)。但在SERIALIZABLE级别下会加共享Next-Key锁。 |
| SELECT ... LOCK IN SHARE MODE | 共享锁(S锁) | 在扫描到的索引记录上加S锁(通常为Next-Key Lock)。 |
| SELECT ... FOR UPDATE | 排他锁(X锁) | 在扫描到的索引记录上加X锁(通常为Next-Key Lock)。 |
| UPDATE / DELETE | 排他锁(X锁) | 对扫描到的每条记录加X锁(Next-Key Lock)。 |
| INSERT | 排他锁(X锁) | 对插入的行加记录锁(不是Next-Key),之前加插入意向锁。 |
| 唯一索引等值查询 | Record Lock(记录锁) | 如果记录存在,只锁该记录,不加间隙锁,性能最佳。 |
| 非唯一索引/范围查询 | Next-Key Lock | 锁定扫描到的区间和记录,防止幻读。 |
关键点:如果你的查询没有走索引,InnoDB 会进行全表扫描,从而对表中的每一行都加上 Next-Key Lock,阻塞全表的插入操作。这就是为什么频繁出现锁等待和死锁时,优化 SQL 索引是第一要务。
七、总结与避坑指南
锁是索引在加,不是行记录在加:务必确保
WHERE条件能用到索引,否则行锁变表锁。RR级别下间隙锁虽好,但容易锁住范围:如果业务可以接受“读已提交”的隔离级别(RC),可以考虑将隔离级别设为
READ-COMMITTED。在RC级别下,间隙锁会被禁用(除了外键约束和重复键检查),从而减少锁冲突,但需要注意区分“当前读”和“快照读”。锁等待与死锁:
锁等待:事务A持有锁,事务B等待。通常设置
innodb_lock_wait_timeout超时参数。死锁:两个事务互相持有对方需要的资源。InnoDB 会自动检测死锁,并回滚持有较少行锁的事务(即代价较小的事务)。
常用排查工具:
SHOW ENGINE INNODB STATUS;查看最近一次死锁信息。information_schema.INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS三表联合查询。