MySQL锁机制
一、按锁的粒度(锁定范围)划分
这是最核心的一种分类方式,锁的粒度决定了系统的并发性能和开销。粒度越小,并发度越高,但管理锁的开销也越大。
全局锁
- 作用范围:整个数据库实例。
- 典型命令:
FLUSH TABLES WITH READ LOCK
(FTWRL)。 - 理解:它会让整个数据库处于只读状态,所有数据变更操作(DML)和表结构变更操作(DDL)都会被阻塞。
- 使用场景:非常少,主要用于全库逻辑备份。但请注意,在支持事务的引擎(如InnoDB)中,使用
mysqldump --single-transaction
进行一致性读备份是更好、更非阻塞的选择。
表级锁
- 作用范围:整张表。
- 理解:MySQL服务器层实现的锁,与存储引擎无关。锁定整张表后,其他会话对这张表的写操作会被阻塞。
- 分类:
- 表锁:
LOCK TABLES ... READ/WRITE
。显式使用,现在很少用。 - 元数据锁:Metadata Lock (MDL),这是最重要、最常见的表级锁。
- 作用:防止一个事务在读数据时,另一个会话修改了表结构(DDL操作),导致查询得到的结果不一致。
- 规则:
- 当对一个表做增删改查(DML) 操作时,会自动加MDL读锁(共享锁)。
- 当要对表做结构变更(DDL) 操作时,会自动加MDL写锁(排他锁)。
- 常见问题:长事务或未提交的事务一直占着MDL读锁,会导致后续所有的DDL操作(甚至后续的DML操作)被阻塞,整个表可能就完全写不入了。这是线上需要非常小心的问题。
- 表锁:
行级锁
- 作用范围:单行记录或行之间的间隙。
- 理解:这是InnoDB引擎实现高并发的基石。它只在存储引擎层实现,MyISAM引擎就不支持行锁。
- 分类(InnoDB的行锁是基于索引实现的!):
- 记录锁:锁定索引中的一条具体记录。
- 例如:
SELECT * FROM t WHERE id = 10 FOR UPDATE;
会在id=10
这条记录的索引上加记录锁,防止其他事务修改或删除它。
- 例如:
- 间隙锁:锁定一个索引记录之间的范围,但不包括记录本身。开区间,如
(5, 10)
。- 目的:解决幻读问题(在可重复读隔离级别下生效)。
- 例如:
SELECT * FROM t WHERE id BETWEEN 5 AND 10 FOR UPDATE;
不仅会锁住id=5和10的记录,还会锁住(5,10)这个区间,防止其他事务插入id=6,7,8,9的新记录。
- 临键锁:记录锁 + 间隙锁的组合。锁定一个范围,并且包含记录本身。左开右闭区间,如
(5, 10]
。- 这是InnoDB在可重复读(RR) 隔离级别下的默认行锁算法。
- 例如:如果索引有值10, 11, 13。那么执行
SELECT * FROM t WHERE id > 11 FOR UPDATE;
,可能会锁住(11, 13]
和(13, +∞]
这两个临键锁范围。
- 插入意向锁:一种特殊的间隙锁,表示事务打算在某个间隙插入记录。多个事务只要插入的位置不冲突(例如插入不同的值),它们可以同时持有插入意向锁。它的存在是为了提高插入并发度。
- 记录锁:锁定索引中的一条具体记录。
二、按锁的模式(兼容性)划分
这种分类描述了多个锁之间的相互关系。
共享锁
- 简称:S锁。
- 理解:又称为“读锁”。一个事务获取了一条记录的共享锁后,其他事务也可以获得这条记录的共享锁(大家可以一起读),但不能获得排他锁(不能有人修改)。
- 加锁方式:
SELECT ... LOCK IN SHARE MODE;
排他锁
- 简称:X锁。
- 理解:又称为“写锁”。一个事务获取了一条记录的排他锁后,其他事务既不能获得共享锁也不能获得排他锁(即不能读(这里指加锁读)也不能写)。
- 加锁方式:
SELECT ... FOR UPDATE;
以及UPDATE
,DELETE
,INSERT
语句会自动给涉及的行加排他锁。
兼容性矩阵:
当前锁 | 请求 S锁 | 请求 X锁 |
---|---|---|
持有 S锁 | ✅ 兼容 | ❌ 冲突 |
持有 X锁 | ❌ 冲突 | ❌ 冲突 |
- 意向锁
- 理解:意向锁是表级锁,它的存在是为了让行锁和表锁能够高效地共存。
- 目的:如果一个事务想要给某一行加S/X锁,它必须首先给这张表加上对应的意向锁。这样,当另一个事务想给整张表加表锁时,就不需要逐行检查是否有行锁冲突,只需检查表上是否有意向锁即可,大大提高了效率。
- 分类:
- 意向共享锁:IS锁。表示事务准备给表中的某些行加共享锁(S锁)。
- 意向排他锁:IX锁。表示事务准备给表中的某些行加排他锁(X锁)。
- 兼容性:意向锁之间是互相兼容的(因为大家只是“有意向”,实际锁定的行可能并不冲突)。但IX锁和表级S锁不兼容(因为表S锁要求整个表只读,而IX锁表示有事务想写)。
三、如何理解和应用
隔离级别是背景:锁的存在很大程度上是为了实现不同的事务隔离级别。
- 读未提交:通常不加锁。
- 读已提交:使用记录锁,但没有间隙锁,所以可能发生幻读。
- 可重复读:使用临键锁(记录锁+间隙锁),解决了幻读问题。
- 串行化:直接使用表级锁,并发度最低。
锁是基于索引的:这是理解行锁的关键!
- 有索引:InnoDB会只锁定满足条件的索引项和间隙。这是高效且高并发的方式。
- 无索引/未用索引:InnoDB无法精确定位到行,无奈之下会升级锁的粒度,比如锁住整个索引树,甚至退化为锁表。这是导致并发性能急剧下降的常见原因。所以,
WHERE
条件一定要用好索引!
死锁:行锁必然带来死锁问题。
- 原因:两个或多个事务互相等待对方释放锁。
- 例如:事务A锁了行1,试图锁行2;同时事务B锁了行2,试图锁行1。
- 解决方案:InnoDB有死锁检测机制,一旦发现死锁,会立即回滚其中一个代价最小的事务,让另一个事务完成。应用程序需要能处理这种异常并进行重试。
总结
锁类型 | 粒度 | 主要作用 | 备注 |
---|---|---|---|
全局锁 | 数据库 | 全库备份 | 尽量用--single-transaction 替代 |
表锁 | 表 | 服务器层通用锁 | 显式使用,不常用 |
MDL锁 | 表 | 防止读写和DDL冲突 | 非常重要,长事务是杀手 |
行锁-记录锁 | 行 | 锁定单行 | InnoDB高并发基础 |
行锁-间隙锁 | 间隙 | 解决幻读 | RR隔离级别特有 |
行锁-临键锁 | 行+间隙 | 默认行锁算法 | RR隔离级别 |
意向锁 | 表 | 协调行锁与表锁的关系 | 提高表锁检查效率 |
建议:
- 大多数场景下,使用InnoDB引擎和可重复读(RR) 隔离级别。
- 写SQL时,一定要确保
WHERE
条件能用上索引,避免行锁升级为表锁。 - 避免大事务,事务要尽快提交,这是预防MDL锁问题、死锁问题和锁等待的最有效方法。
- 分析复杂锁问题可以使用命令
SHOW ENGINE INNODB STATUS\G
查看最近的死锁信息。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论