关键词:MySQL锁, InnoDB锁, 共享锁, 排他锁, 行锁, 表锁, 意向锁, 间隙锁, 死锁
你是否在面试中被问到"MySQL有哪些锁?"、“行锁和表锁有什么区别?”、“什么是意向锁?”、"InnoDB如何解决幻读?"这些问题看似简单,但涉及MySQL并发控制的核心机制。本文将从并发事务问题出发,深入剖析MySQL的各种锁类型,详解LBCC基于锁的并发控制,帮你彻底搞懂MySQL锁机制。
目录
1. 并发事务问题与解决方案
1.1 并发事务带来的问题
事务并发执行时可能带来的各种问题,最大的难点是:一方面要最大程度地利用数据库的并发访问,另一方面还要确保每个用户能以一致的方式读取和修改数据。
当一个事务进行读取操作,另一个同时进行改动操作时,可能发生脏读、不可重复读、幻读的问题。
1.2 两种解决方案
方案一:读操作MVCC,写操作加锁
一致性读(快照读):事务利用MVCC进行的读取操作,读取的是历史版本数据。所有普通的SELECT语句在READ COMMITTED、REPEATABLE READ隔离下都是一致性读。
- 优点:读-写操作彼此并不冲突,性能更高
- 缺点:读取的是历史版本,不是最新数据
方案二:读写操作都采用加锁
适用场景:业务不允许读取记录的旧版本,每次都必须读取最新版本。
示例:银行存款事务
- 读取账户余额
- 余额加上存款数额
- 写回数据库
在读取余额后,不希望别的事务再访问该余额,直到本次存款事务完成。这种读取记录时需要加锁的方式称为LBCC(Lock-Based Concurrency Control,基于锁的并发控制)。
加锁解决并发问题:
- 脏读:写记录时加锁,其他事务无法读取
- 不可重复读:读记录时加锁,其他事务无法修改
- 幻读:复杂一些,需要通过间隙锁解决
2. 锁定读(Locking Reads)
也称当前读,读取的是最新版本,并且对读取的记录加锁,阻塞其他事务同时改动相同记录。
2.1 哪些是当前读?
SELECT ... LOCK IN SHARE MODE(共享锁)SELECT ... FOR UPDATE(排他锁)UPDATE(排他锁)INSERT(排他锁/独占锁)DELETE(排他锁)- 串行化事务隔离级别下的SELECT
2.2 锁定读的SELECT语句
共享锁读取
SELECT * FROM test LOCK IN SHARE MODE;
- 为读取到的记录加S锁(共享锁)
- 允许其他事务继续获取这些记录的S锁
- 不允许其他事务获取这些记录的X锁
排他锁读取
SELECT * FROM test FOR UPDATE;
- 为读取到的记录加X锁(排他锁)
- 不允许其他事务获取这些记录的S锁或X锁
- 其他事务会阻塞,直到当前事务提交释放锁
2.3 写操作的锁
DELETE操作
对一条记录做DELETE操作的过程:
- 在B+树中定位到这条记录的位置
- 获取这条记录的X锁
- 执行delete mark操作
相当于一个获取X锁的锁定读。
INSERT操作
一般情况下,新插入一条记录的操作并不加锁,InnoDB通过隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问。
特殊情况INSERT也会获取锁,后面会详细说明。
UPDATE操作
在对一条记录做UPDATE操作时分为三种情况:
-
未修改键值且存储空间未变化
- 定位记录位置
- 获取记录的X锁
- 在原记录位置修改
- 相当于获取X锁的锁定读
-
未修改键值但存储空间变化
- 定位记录位置
- 获取记录的X锁
- 彻底删除旧记录(移入垃圾链表)
- 插入新记录(由INSERT的隐式锁保护)
-
修改了键值
- 相当于先做DELETE再做INSERT
- 按照DELETE和INSERT的规则加锁
3. 共享锁与排他锁
在使用加锁方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写、读-写或写-读情况中的操作相互阻塞。
3.1 锁的类型
| 锁类型 | 英文名 | 简称 | 使用场景 |
|---|---|---|---|
| 共享锁 | Shared Locks | S锁 | 事务要读取一条记录时 |
| 排他锁 | Exclusive Locks | X锁 | 事务要改动一条记录时 |
3.2 锁的兼容性
| 兼容性 | X锁 | S锁 |
|---|---|---|
| X锁 | ❌ 不兼容 | ❌ 不兼容 |
| S锁 | ❌ 不兼容 | ✅ 兼容 |
规则说明:
- S锁与S锁兼容:多个事务可以同时持有同一记录的S锁
- S锁与X锁不兼容:持有S锁时,其他事务无法获取X锁
- X锁与任何锁都不兼容:持有X锁时,其他事务无法获取任何锁
示例:
- 事务E1获取了记录的S锁,事务E2可以再获取S锁 ✅
- 事务E1获取了记录的S锁,事务E2想获取X锁会被阻塞 ❌
- 事务E1获取了记录的X锁,事务E2想获取S锁或X锁都会被阻塞 ❌
4. 锁的粒度:表锁与行锁
4.1 锁粒度的概念
| 锁类型 | 粒度 | 影响范围 | 别名 |
|---|---|---|---|
| 行锁 | 细 | 只影响一条记录 | 行级锁、记录锁 |
| 表锁 | 粗 | 影响整个表的所有记录 | 表级锁 |
4.2 表锁与行锁的比较
| 维度 | 表锁 | 行锁 |
|---|---|---|
| 锁定粒度 | 粗 | 细 |
| 加锁效率 | 快(直接锁表) | 慢(需要定位记录) |
| 冲突概率 | 高 | 低 |
| 并发性能 | 低 | 高 |
4.3 给表加S锁
如果一个事务给表加了S锁:
- ✅ 别的事务可以继续获得该表的S锁
- ✅ 别的事务可以继续获得该表中某些记录的S锁
- ❌ 别的事务不可以获得该表的X锁
- ❌ 别的事务不可以获得该表中某些记录的X锁
4.4 给表加X锁
如果一个事务给表加了X锁(独占整个表):
- ❌ 别的事务不可以获得该表的S锁
- ❌ 别的事务不可以获得该表中某些记录的S锁
- ❌ 别的事务不可以获得该表的X锁
- ❌ 别的事务不可以获得该表中某些记录的X锁
5. 意向锁
5.1 为什么需要意向锁?
问题场景:
- 给表加S锁前,需要确保表中没有正在维修的楼层(没有被加X锁的记录)
- 给表加X锁前,需要确保表中没有办公的楼层(没有被加S锁的记录)
效率问题:如果对表上锁时需要依次检查每一行是否被上锁,效率太慢!
5.2 意向锁的概念
InnoDB提出了**意向锁(Intention Locks)**来解决这个问题:
| 意向锁类型 | 英文名 | 简称 | 说明 |
|---|---|---|---|
| 意向共享锁 | Intention Shared Lock | IS锁 | 准备在某条记录上加S锁时,先在表级别加IS锁 |
| 意向独占锁 | Intention Exclusive Lock | IX锁 | 准备在某条记录上加X锁时,先在表级别加IX锁 |
5.3 意向锁的作用
意向锁的提出是为了在之后加表级别的S锁和X锁时,可以快速判断表中是否有记录被上锁,避免用遍历的方式查看表中有没有上锁的记录。
生活示例 - 共享Office大楼:
- 客户办公(行级S锁):先在大楼门口放IS锁,再到楼层门口放S锁
- 维修楼层(行级X锁):先在大楼门口放IX锁,再到楼层门口放X锁
- 投资人参观(表级S锁):看大楼门口有没有IX锁,有则等待
- 公司谈判(表级X锁):看大楼门口有没有IS或IX锁,有则等待
5.4 表级别锁的兼容性
| 兼容性 | X | IX | S | IS |
|---|---|---|---|---|
| X | ❌ | ❌ | ❌ | ❌ |
| IX | ❌ | ✅ | ❌ | ✅ |
| S | ❌ | ❌ | ✅ | ✅ |
| IS | ❌ | ✅ | ✅ | ✅ |
重要说明:
- IS和IX锁是兼容的:客户可以同时在不同楼层办公(IS+IS)
- IX和IX锁也是兼容的:多个维修工可以同时维修不同楼层(IX+IX)
- IS和IX锁只是为了判断表中是否有被占用的记录,在对表加S锁或X锁时才会用到
5.5 锁的组合性
| 组合性 | X | IX | S | IS |
|---|---|---|---|---|
| 表锁 | 有 | 有 | 有 | 有 |
| 行锁 | 有 | 无 | 有 | 无 |
注意:意向锁只有表级锁,没有行级锁。
6. MySQL中的行锁和表锁
6.1 不同存储引擎的锁支持
| 存储引擎 | 支持的锁类型 | 事务支持 | 适用场景 |
|---|---|---|---|
| MyISAM | 表锁 | ❌ | 只读或大部分读操作 |
| MEMORY | 表锁 | ❌ | 临时数据、缓存 |
| MERGE | 表锁 | ❌ | 只读场景 |
| InnoDB | 表锁 + 行锁 | ✅ | 高并发读写 |
6.2 其他存储引擎中的锁(MyISAM等)
MyISAM、MEMORY、MERGE只支持表级锁,且不支持事务。
特点:
- 一个会话对表执行SELECT,相当于加表级S锁
- 另一个会话执行UPDATE,相当于获取表级X锁,会被阻塞
- 同一时刻只允许一个会话对表进行写操作
Concurrent Inserts特性:MyISAM支持在读取时同时插入记录,提升插入速度。
6.3 InnoDB存储引擎中的锁
InnoDB既支持表锁,也支持行锁:
- 表锁:实现简单,占用资源少,但粒度粗
- 行锁:粒度更细,可以实现更精准的并发控制
6.3.1 InnoDB中的表级锁
表级别的S锁、X锁
InnoDB在一般情况下不会为表添加表级别的S锁或X锁。特殊情况下(如崩溃恢复)可能会用到。
手动获取表锁(不建议使用):
-- 加表级S锁
LOCK TABLES t READ;
-- 加表级X锁
LOCK TABLES t WRITE;
⚠️ 注意:尽量避免使用LOCK TABLES手动锁表,不会提供额外保护,只会降低并发能力。
元数据锁(MDL)
执行DDL语句(ALTER TABLE、DROP TABLE等)时,通过Server层的**元数据锁(Metadata Locks)**实现阻塞,而不是InnoDB的表级S锁/X锁。
表级别的IS锁、IX锁
- 对记录加S锁前,先在表级别加IS锁
- 对记录加X锁前,先在表级别加IX锁
IS锁和IX锁不能手动添加,只能由InnoDB存储引擎自行添加。
表级别的AUTO-INC锁
为AUTO_INCREMENT列生成递增值的两种实现:
-
AUTO-INC锁
- 执行插入语句时在表级别加锁
- 为每条记录分配递增值
- 语句执行结束后释放锁
- 适用于无法预计插入数量的语句(INSERT … SELECT等)
-
轻量级锁
- 生成AUTO_INCREMENT值时获取轻量级锁
- 生成值后立即释放锁
- 不需要等到整个语句执行完
- 适用于可以确定插入数量的语句
控制参数:innodb_autoinc_lock_mode
| 值 | 说明 |
|---|---|
| 0 | 一律采用AUTO-INC锁 |
| 1 | 混着来(默认):数量确定用轻量级锁,不确定用AUTO-INC锁 |
| 2 | 一律采用轻量级锁(主从复制场景不安全) |
-- 查看当前设置
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';
7. InnoDB中的行级锁
重要特点:InnoDB的行锁是通过给索引上的索引项加锁来实现的。这意味着:
只有通过索引条件检索数据,InnoDB才使用行级锁,否则将使用表锁。
7.1 行锁的使用条件
- 必须使用索引:执行计划真正使用了索引,才能使用行锁
- 小表可能使用表锁:如果MySQL认为全表扫描效率更高(如小表),就不会使用索引,此时使用表锁
- 范围查询加锁:用范围条件检索数据并请求锁时,给符合条件的索引项加锁
7.2 记录锁(Record Locks)
也叫记录锁,仅仅把一条记录锁上,官方类型名称:LOCK_REC_NOT_GAP。
示例:为number值为6的记录加记录锁
记录锁有S锁和X锁之分:
- 获取了S型记录锁后,其他事务可以继续获取S型记录锁,但不能获取X型记录锁
- 获取了X型记录锁后,其他事务既不能获取S型记录锁,也不能获取X型记录锁
7.3 间隙锁(Gap Locks)
7.3.1 什么是间隙锁
间隙锁(Gap Locks):对索引前后的间隙上锁,不对索引本身上锁。官方类型名称:LOCK_GAP。
作用:解决幻读问题。当事务在第一次执行读取操作时,幻影记录尚不存在,无法给这些记录加记录锁。间隙锁通过对间隙上锁,防止其他事务在间隙中插入新记录。
7.3.2 间隙锁示例
-- 会话1开启事务
BEGIN;
UPDATE teacher SET domain = 'JVM' WHERE number = 6;
这条语句会对以下区间加gap锁:
- 2 ~ 6 之间
- 6 ~ 10 之间
效果:
- 不允许别的事务在26和610的间隙中插入新记录
- 插入number=7的记录会被阻塞(落在6~10区间内)
- 插入number=70的记录可以成功(不在被锁区间)
-- 会被阻塞
INSERT INTO teacher VALUES(7, '晁', 'docker');
-- 可以成功
INSERT INTO teacher VALUES(70, '晁', 'docker');
7.3.3 间隙锁与幻读
在REPEATABLE READ隔离级别下,可以使用MVCC或加锁方案解决幻读问题:
- MVCC方案:一致性读,读取历史版本
- 加锁方案:使用间隙锁,阻止其他事务插入幻影记录
8. 死锁
8.1 死锁的概念
死锁:两个或两个以上的进程在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。
生活示例
A和B去按摩洗脚,都想同时做足底按摩和头部按摩:
- 13技师擅长足底按摩
- 14技师擅长头部按摩
死锁产生:
- A先抢到14(头部),想要13(足底)
- B先抢到13(足底),想要14(头部)
- 两人互不相让,产生死锁
死锁产生的条件
- 多操作者(M≥2个):单线程不会有死锁
- 争夺多个资源(N≥2个,且N≤M):单资源只会产生竞争,不会产生死锁
- 争夺资源的顺序不对:如果顺序相同,不会产生死锁
- 拿到资源不放手:占有且等待
8.2 MySQL中的死锁
死锁示例
会话1:
BEGIN;
SELECT * FROM teacher WHERE number = 1 FOR UPDATE; -- 获取number=1的X锁
会话2:
BEGIN;
SELECT * FROM teacher WHERE number = 3 FOR UPDATE; -- 获取number=3的X锁
会话1继续:
SELECT * FROM teacher WHERE number = 3 FOR UPDATE; -- 想要number=3的X锁,被阻塞
会话2继续:
SELECT * FROM teacher WHERE number = 1 FOR UPDATE; -- 想要number=1的X锁
结果:MySQL检测到死锁,结束会话2中事务的执行。
查看死锁信息
-- 查看InnoDB状态(包含死锁信息)
SHOW ENGINE INNODB STATUS\G
查看事务加锁情况
-- 查看系统变量
SHOW VARIABLES LIKE 'innodb_status_output_locks';
-- 设置为ON(默认OFF)
SET GLOBAL innodb_status_output_locks = ON;
开启后可以查看哪个事务对哪些记录加了哪些锁。
8.3 死锁的解决方案
- 设置超时时间:等待锁超时后自动回滚
- 死锁检测:InnoDB自动检测死锁,牺牲一个事务
- 调整事务顺序:让所有事务按相同顺序获取锁
- 减少事务持有锁的时间:尽快提交或回滚事务
- 使用更低的隔离级别:如READ COMMITTED
- 为表添加合适的索引:减少锁的粒度
总结
本文深入讲解了MySQL的锁机制,从并发问题出发,系统梳理了各种锁类型:
核心知识点:
-
并发解决方案:
- MVCC:读历史版本,读-写不冲突,性能高
- LBCC:读当前版本加锁,保证数据一致性
-
锁的类型:
- 共享锁(S锁):读锁,兼容S锁,不兼容X锁
- 排他锁(X锁):写锁,不兼容任何锁
- 意向锁(IS/IX):表级锁,用于快速判断表中是否有记录被加锁
-
锁的粒度:
- 表锁:粒度粗,冲突概率高,并发性能低
- 行锁:粒度细,冲突概率低,并发性能高
- 只有使用索引时,InnoDB才使用行锁,否则使用表锁
-
行锁类型:
- 记录锁(Record Locks):锁定单条记录
- 间隙锁(Gap Locks):锁定索引间隙,解决幻读问题
-
死锁:
- 产生条件:多操作者、多资源、顺序不对、持有不释放
- MySQL会自动检测死锁并牺牲一个事务
实际应用建议:
- 优先使用MVCC(一致性读),性能更好
- 需要读取最新数据且不允许并发修改时,使用
FOR UPDATE加X锁 - 需要防止幻读时,使用间隙锁(REPEATABLE READ自动使用)
- 避免长事务,减少锁持有时间
- 确保查询使用索引,否则会变成表锁
- 按固定顺序访问表,减少死锁概率
面试高频问题:
- MySQL有哪些锁?(S锁、X锁、IS锁、IX锁、行锁、表锁、记录锁、间隙锁)
- 行锁和表锁的区别?(粒度、并发性能、使用条件)
- 什么是意向锁?有什么作用?(表级锁,避免遍历检查行锁)
- InnoDB如何解决幻读?(MVCC或间隙锁)
- 什么情况下会产生死锁?如何解决?(循环等待,检测+牺牲)
- 为什么有时候查询会使用表锁而不是行锁?(没有使用索引)
希望这篇文章能帮助你彻底搞懂MySQL锁机制!如果觉得有帮助,欢迎点赞、收藏、关注~
推荐标签:
- MySQL
- InnoDB
- 锁机制
- 并发控制
- 行锁
- 表锁
- 死锁
- 面试

1万+

被折叠的 条评论
为什么被折叠?



