做后端开发,无论是在日常编码、处理线上问题还是准备面试,都绕不开MySQL锁这个话题。
线上业务突然卡顿、事务莫名阻塞、甚至直接报出死锁错误,这些问题追根溯源,往往都与锁冲突脱不开干系。我们常听到“行锁”、“表锁”这些词,但MySQL的锁机制究竟是如何工作的?虽然看起来名目繁多,但只要理清分类逻辑,理解起来并不困难。
下面这张图帮你从不同维度快速建立对MySQL锁的认识。

先从分类维度理清楚
MySQL的锁机制看似复杂,但只要按几个核心维度进行拆分,脉络就会变得清晰。
1. 按锁定粒度分:表、行、页级锁
这是最直观的分类方式,锁定的范围直接决定了并发性能。
- 表级锁:开销小,加锁快,且不会出现死锁。但锁定粒度太大,锁冲突概率极高,严重限制了并发度。MyISAM引擎仅支持表锁,而InnoDB引擎在全表扫描等未使用索引的情况下,也可能退化为表锁。
- 行级锁:这是InnoDB引擎的“杀手锏”,锁定粒度最小,锁冲突概率最低,因此能提供最高的并发性能。然而,其开销大、加锁慢,并且容易产生死锁问题。
- 页级锁:锁定粒度介于表锁和行锁之间,开销和并发性能也居中。主要在BDB引擎中使用,日常开发中接触较少,了解即可。
2. 按锁的级别分:共享、排他、意向锁各司其职
这个维度围绕着读写操作的权限,核心是解决“能否并发读”和“能否并发写”的问题。
- 共享锁(S锁 / 读锁):允许多个事务同时获取。事务持有共享锁后,只能读取数据,不能修改。例如执行
select ... lock in share mode 语句。其他事务可以继续加共享锁来读,但若想加写锁则必须等待。
- 排他锁(X锁 / 写锁):具有排他性。一个事务获取某数据的排他锁后,其他事务无法再对该数据加任何类型的锁(包括共享锁和排他锁),只能等待。
update、delete 语句,InnoDB会自动为涉及的行加排他锁;select ... for update 则可以手动显式加排他锁。
- 意向锁(IS锁 / IX锁):这是InnoDB引入的一种表级锁,用于快速判断表中是否存在行锁,可以视为一种“前置声明”。例如,事务要给某行加共享锁(S锁),需要先取得该表的意向共享锁(IS锁);要给某行加排他锁(X锁),则需要先取得该表的意向排他锁(IX锁)。当另一个事务想给整个表加锁时,只需检查表上是否存在与之冲突的意向锁即可,无需逐行检查,大大提升了效率。
3. 其他实用分类
除了上述两个核心维度,还有几种常见的分类视角:
- 按操作划分:DML锁(针对数据的增删改查操作)和DDL锁(针对表结构的修改操作)。例如执行
alter table 时,就会获取DDL写锁,这会阻塞所有其他的DML操作。
- 按加锁方式划分:自动锁(如InnoDB执行
update 时自动为记录加X锁)和显式锁(如手动执行 lock tables 或 select ... for update)。
- 按并发策略划分:悲观锁和乐观锁。悲观锁假定会发生冲突,先加锁再操作,如行锁、表锁;乐观锁假定冲突很少发生,通过版本号或时间戳实现,如在更新时附加
where version = 1 的条件,仅当版本匹配时才更新,适用于读多写少、冲突概率低的场景。
几种具体的常用锁
了解分类后,我们来看看实际开发中频繁接触的几种具体锁。
全局锁:锁住整个数据库实例
顾名思义,其锁定范围是整个数据库。执行 flush tables with read lock (FTWRL) 命令即可为整个库加上全局读锁。此后,库处于只读状态,任何数据变更(DML)、结构变更(DDL)以及事务提交都会被阻塞。
此锁主要用于全库逻辑备份。但要注意,对于支持事务的InnoDB引擎,通常使用 mysqldump --single-transaction 参数进行一致性备份,它通过开启事务利用MVCC获得一致性视图,无需加全局锁。而对于MyISAM这类不支持事务的引擎,则仍需使用FTWRL。
这里有一个常见误区:不要用 set global readonly = true 来代替FTWRL做备份锁定。因为客户端异常断开后,FTWRL会自动释放锁,而 readonly 状态不会自动恢复,可能导致数据库长时间处于不可写状态。
元数据锁(MDL):表结构的守护者
MDL锁由MySQL Server层自动管理,用户通常感知不到,但其作用至关重要。当你执行 select、update 等DML操作时,系统会自动为该表加上MDL读锁;执行 alter table 等DDL操作时,则会加上MDL写锁。
MDL读锁之间不互斥,但读锁与写锁、写锁与写锁之间是互斥的。这有效防止了一个事务在查询或修改数据时,表结构被另一个事务意外更改,从而保障了数据一致性。
这里有个容易踩坑的细节:MDL锁在语句开始时申请,但直到事务提交后才释放。假设你开启了一个事务,仅执行了一个 select 查询(此时已获取MDL读锁),但未提交事务。此时,另一个会话尝试执行 alter table 修改表结构,就会因为申请MDL写锁被阻塞,直到你的长事务提交。排查线上DDL阻塞问题时,长事务持有的MDL锁是重点怀疑对象之一。

行级锁的细分:记录锁、间隙锁、临键锁
InnoDB的行锁在实现上还有更精细的划分,主要是为了解决“幻读”问题。
- 记录锁:锁定索引中的一条具体记录。例如,
update user set name = '张三' where id = 1,如果id是主键索引,那么就会对id=1的这条记录加记录锁。
- 间隙锁:锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录,从而解决幻读。仅在可重复读(RR)隔离级别下生效。例如,表中id有1, 3, 5,执行
select * from user where id between 1 and 5 for update,不仅会锁住id=1,3,5的记录,还会锁住(1,3)和(3,5)这两个间隙,阻止插入id=2或4的记录。
- 临键锁:记录锁与间隙锁的组合。在RR隔离级别下,InnoDB默认加的行锁就是临键锁,它会锁定记录本身以及该记录之前的间隙。例如,
select * from user where id > 3 for update,会锁定(3,5]这个“左开右闭”区间以及(5, +∞)这个开区间。

常见问题与优化建议
最后,分享几个锁相关的注意事项和优化思路。
-
行锁升级为表锁:InnoDB的行锁是依赖于索引实现的。如果SQL语句没有使用到索引,或者索引失效导致全表扫描,InnoDB就会退化为表锁。例如 update user set name = '张三' where name = '李四',若name字段无索引,则会锁定整个表,严重影响并发。
-
死锁问题:行锁场景下容易发生死锁。例如,事务A持有行1的锁并等待行2,事务B持有行2的锁并等待行1,两者互相等待形成死锁。InnoDB默认开启了死锁检测,检测到死锁后会主动回滚代价较小的事务。也可以通过参数 innodb_lock_wait_timeout 设置锁等待超时时间(默认50秒)。
-
合理选择隔离级别:间隙锁和临键锁虽然解决了幻读,但也增加了锁冲突的概率。如果业务场景可以容忍幻读,将事务隔离级别设置为读已提交(RC),InnoDB就不会使用间隙锁,可以在一定程度上提升并发性能。
总而言之,MySQL的锁机制是一个系统性的知识体系。理清分类、理解每种锁的设计目的和应用场景,当线上出现锁等待或死锁时,你就能像侦探一样,从“表锁还是行锁”、“谁持有锁”、“锁住了什么范围”等角度层层深入,快速定位并解决问题。
|