找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4956

积分

1

好友

683

主题
发表于 前天 01:58 | 查看: 14| 回复: 0

做后端开发,无论是在日常编码、处理线上问题还是准备面试,都绕不开MySQL锁这个话题。

线上业务突然卡顿、事务莫名阻塞、甚至直接报出死锁错误,这些问题追根溯源,往往都与锁冲突脱不开干系。我们常听到“行锁”、“表锁”这些词,但MySQL的锁机制究竟是如何工作的?虽然看起来名目繁多,但只要理清分类逻辑,理解起来并不困难。

下面这张图帮你从不同维度快速建立对MySQL锁的认识。
MySQL锁分类维度思维导图

先从分类维度理清楚

MySQL的锁机制看似复杂,但只要按几个核心维度进行拆分,脉络就会变得清晰。

1. 按锁定粒度分:表、行、页级锁

这是最直观的分类方式,锁定的范围直接决定了并发性能。

  • 表级锁:开销小,加锁快,且不会出现死锁。但锁定粒度太大,锁冲突概率极高,严重限制了并发度。MyISAM引擎仅支持表锁,而InnoDB引擎在全表扫描等未使用索引的情况下,也可能退化为表锁。
  • 行级锁:这是InnoDB引擎的“杀手锏”,锁定粒度最小,锁冲突概率最低,因此能提供最高的并发性能。然而,其开销大、加锁慢,并且容易产生死锁问题。
  • 页级锁:锁定粒度介于表锁和行锁之间,开销和并发性能也居中。主要在BDB引擎中使用,日常开发中接触较少,了解即可。

2. 按锁的级别分:共享、排他、意向锁各司其职

这个维度围绕着读写操作的权限,核心是解决“能否并发读”和“能否并发写”的问题。

  • 共享锁(S锁 / 读锁):允许多个事务同时获取。事务持有共享锁后,只能读取数据,不能修改。例如执行 select ... lock in share mode 语句。其他事务可以继续加共享锁来读,但若想加写锁则必须等待。
  • 排他锁(X锁 / 写锁):具有排他性。一个事务获取某数据的排他锁后,其他事务无法再对该数据加任何类型的锁(包括共享锁和排他锁),只能等待。updatedelete 语句,InnoDB会自动为涉及的行加排他锁;select ... for update 则可以手动显式加排他锁。
  • 意向锁(IS锁 / IX锁):这是InnoDB引入的一种表级锁,用于快速判断表中是否存在行锁,可以视为一种“前置声明”。例如,事务要给某行加共享锁(S锁),需要先取得该表的意向共享锁(IS锁);要给某行加排他锁(X锁),则需要先取得该表的意向排他锁(IX锁)。当另一个事务想给整个表加锁时,只需检查表上是否存在与之冲突的意向锁即可,无需逐行检查,大大提升了效率。

3. 其他实用分类

除了上述两个核心维度,还有几种常见的分类视角:

  • 按操作划分:DML锁(针对数据的增删改查操作)和DDL锁(针对表结构的修改操作)。例如执行 alter table 时,就会获取DDL写锁,这会阻塞所有其他的DML操作。
  • 按加锁方式划分:自动锁(如InnoDB执行 update 时自动为记录加X锁)和显式锁(如手动执行 lock tablesselect ... 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层自动管理,用户通常感知不到,但其作用至关重要。当你执行 selectupdate 等DML操作时,系统会自动为该表加上MDL读锁;执行 alter table 等DDL操作时,则会加上MDL写锁。

MDL读锁之间不互斥,但读锁与写锁、写锁与写锁之间是互斥的。这有效防止了一个事务在查询或修改数据时,表结构被另一个事务意外更改,从而保障了数据一致性。

这里有个容易踩坑的细节:MDL锁在语句开始时申请,但直到事务提交后才释放。假设你开启了一个事务,仅执行了一个 select 查询(此时已获取MDL读锁),但未提交事务。此时,另一个会话尝试执行 alter table 修改表结构,就会因为申请MDL写锁被阻塞,直到你的长事务提交。排查线上DDL阻塞问题时,长事务持有的MDL锁是重点怀疑对象之一。
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, +∞)这个开区间。

记录锁、间隙锁与临键锁示意图

常见问题与优化建议

最后,分享几个锁相关的注意事项和优化思路。

  1. 行锁升级为表锁:InnoDB的行锁是依赖于索引实现的。如果SQL语句没有使用到索引,或者索引失效导致全表扫描,InnoDB就会退化为表锁。例如 update user set name = '张三' where name = '李四',若name字段无索引,则会锁定整个表,严重影响并发。

  2. 死锁问题:行锁场景下容易发生死锁。例如,事务A持有行1的锁并等待行2,事务B持有行2的锁并等待行1,两者互相等待形成死锁。InnoDB默认开启了死锁检测,检测到死锁后会主动回滚代价较小的事务。也可以通过参数 innodb_lock_wait_timeout 设置锁等待超时时间(默认50秒)。

  3. 合理选择隔离级别:间隙锁和临键锁虽然解决了幻读,但也增加了锁冲突的概率。如果业务场景可以容忍幻读,将事务隔离级别设置为读已提交(RC),InnoDB就不会使用间隙锁,可以在一定程度上提升并发性能。

总而言之,MySQL的锁机制是一个系统性的知识体系。理清分类、理解每种锁的设计目的和应用场景,当线上出现锁等待或死锁时,你就能像侦探一样,从“表锁还是行锁”、“谁持有锁”、“锁住了什么范围”等角度层层深入,快速定位并解决问题。




上一篇:Java调用Python的5种实战方法:从命令行到HTTP接口详解
下一篇:MySQL三表以上JOIN的性能陷阱与阿里规范的三种替代方案解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-5 19:06 , Processed in 0.550290 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表