MySQL 数据库中的锁有哪些?一篇文章给你讲明白

0 阅读7分钟

做后端开发的朋友不管是在开发过程中还是在线上环境,亦或者是面试中,多多少少肯定都遇过 MySQL 锁相关的问题。

其实MySQL锁问题还挺常见的。比如线上业务突然卡顿、线上突然出现事务阻塞、死锁报错,这些问题查到最后发现基本都是 MySQL 锁冲突问题。是不是有时听别人说起行锁、表锁,自己却一头雾水?虽然MySQL 里的锁看起来五花八门,但其实 MySQL 的锁说复杂也复杂,说简单吧,理清分类逻辑就能理解了。

下面一张图告诉你从不同的维度理解MySQL的锁。 image

先从分类维度理清楚

MySQL 的锁说起来复杂,但其实按不同维度拆分一下就清晰多了。咱们可以从最常用的几个分类开始展开说:

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

这应该是最直观的分类了,毕竟锁定的范围直接影响并发性能。

  • 表级锁:开销小,加锁快,也不会出现死锁,但架不住锁定粒度大啊,锁冲突概率直接拉满,并发度就高不起来了。像 MyISAM 引擎就只支持表锁,InnoDB 在特殊情况(比如全表扫描没用到索引)下也会触发表锁。
  • 行级锁:InnoDB 的看家本领,锁定粒度最小,锁冲突概率最低,并发度自然最高。但是吧,它开销大、加锁慢,还容易出现死锁,毕竟每个事务都盯着不同的行,一不小心就互相等对方释放锁了。
  • 页级锁:这个用得不多,开销和加锁时间介于表锁和行锁之间,并发度也就一般般,主要是 BDB 引擎在用,咱们日常开发接触得少,了解就行。

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

这个维度是围绕读写操作的权限来的,核心就是解决 "能不能同时读"、"能不能同时写" 的问题:

  • 共享锁(S 锁 / 读锁):顾名思义,就是多个事务可以同时加的锁,加了之后只能读不能改。比如执行select ... lock in share mode,其他事务也能加 S 锁读,但想加写锁就得等了。
  • 排他锁(X 锁 / 写锁):这就霸道多了,一个事务加了 X 锁,其他事务啥锁都加不了,只能等着。像updatedelete语句,InnoDB 会自动给涉及的行加 X 锁,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)。
  • 按并发策略划分:悲观锁和乐观锁。悲观锁就是默认觉得会有冲突,先加锁再操作,比如前面说的 X 锁、S 锁;乐观锁则是默认没冲突,用版本号或者时间戳来控制,比如更新的时候加个where version = 1,只有版本匹配才更新,适合并发冲突少的场景。

几种具体的常用锁

光说分类太抽象,咱们可以看看实际开发中经常遇到的锁:

全局锁:锁整个数据库实例

听名字就知道范围有多大了,执行flush tables with read lock(FTWRL)就能给整个库加全局读锁,加了之后整个库就只读了,增删改、DDL、事务提交全都会被阻塞。 这个锁主要用在全库逻辑备份的时候,但要注意,InnoDB 可以用mysqldump --single-transaction来做一致性备份,不用加全局锁,因为它会启动一个事务,利用 MVCC 拿到一致性视图。但是吧,如果是 MyISAM 这种不支持事务的引擎,那还是得用 FTWRL。 这里要提个坑,别用set global readonly = true来代替 FTWRL,因为如果客户端异常断开,FTWRL 会自动释放锁,但readonly不会,搞不好就把库一直锁在那儿了。

元数据锁(MDL):表结构守护者

这个锁是 InnoDB 自动加的,你甚至感知不到它的存在,但它却很重要。比如你执行selectupdate这些 DML 操作,InnoDB 会自动加 MDL 读锁;执行alter table这种 DDL 操作,会加 MDL 写锁。 MDL 读锁之间不互斥,读锁和写锁、写锁和写锁是互斥的,这样就能防止一个事务在修改数据的时候,另一个事务突然改了表结构,导致数据不一致。 这里有个容易踩的坑:MDL 锁是在事务开始时申请,事务提交后才释放的。比如你开了一个事务,只是执行了一个select,这时候加了 MDL 读锁,然后你一直不提交事务,这时候别人要执行alter table就会被卡住,直到你提交事务。我之前就碰到过一次,排查了半天才发现是个没提交的事务占着 MDL 锁,真是血泪教训啊!

行级锁的细分:记录锁、间隙锁、临键锁

InnoDB 的行锁其实还有更细的划分,主要是为了解决幻读问题:

  • 记录锁:就是给某一行记录加锁,比如update user set name = '张三' where id = 1,如果 id 是主键,那就是给 id=1 的这行加记录锁。
  • 间隙锁:这个是 InnoDB 在 RR(可重复读)隔离级别下才有的,它锁住的是两个索引记录之间的间隙,比如表中 id 有 1、3、5,执行select * from user where id between 1 and 5 for update,会锁住 (1,3)、(3,5) 这两个间隙,防止其他事务在中间插入 id=2 或 4 的记录,解决幻读问题。
  • 临键锁:这个是记录锁 + 间隙锁的组合,默认情况下 InnoDB 在 RR 隔离级别下加的行锁都是临键锁。比如执行select * from user where id > 3 for update,会锁住 (3,5]、(5, +∞) 这些区间,既锁住了存在的记录,也锁住了可能插入的间隙。

最后聊聊锁的坑和优化建议

说了这么多,再给大家提几个注意事项:

  1. 行锁变表锁:InnoDB 的行锁是基于索引的,如果你的查询没用到索引,或者索引失效了,那 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 的锁虽然多,但只要理清分类,理解每种锁的适用场景和底层逻辑,遇到问题的时候就能快速定位了。比如线上出现锁等待,先看看是表锁还是行锁,再看看是哪个事务占着锁,一步步排查总能解决的。

最后想问下,你在开发中遇到过最头疼的锁问题是什么?欢迎在评论区交流。