《穿透 MySQL 索引专栏(六):从底层数据结构到千万级慢查询调优》
2026/5/9 11:37:12 网站建设 项目流程

这次,咱们来聊聊 MySQL 的锁。很多同学一听到锁就头大,其实只要按作用范围把它们分好类,理清各自的应用场景,一切就会豁然开朗。

为了让大家看得不那么枯燥,这篇博客我们直接采用 Q&A 的形式,直击痛点。不多 BB 了,发车!

在 MySQL 里,根据加锁的范围,锁大致可以分为三类:全局锁、表级锁和行级锁


一、 全局锁

Q1:全局锁是怎么用的?

要给整个数据库加上全局锁,只需要执行这条命令:

SQL

flush tables with read lock

执行之后,整个数据库就进入了只读状态。这时候如果有其他线程想执行以下操作,都会被无情阻塞:

  • 对数据的增删改:比如insertdeleteupdate

  • 对表结构的更改:比如alter tabledrop table

要释放全局锁,执行这条命令即可(当然,如果你当前会话断开了,全局锁也会自动释放):

SQL

unlock tables

Q2:全局锁的应用场景是什么?

全局锁主要用于全库逻辑备份。这样能保证在备份期间,不会因为数据的更新导致备份出来的文件跟预期不一致。

举个很现实的例子:假设你在备份期间不加锁。用户买了一件商品,业务逻辑是:1. 扣减用户余额;2. 扣减商品库存。 如果备份顺序刚好是:先备份了用户表 -> 用户发起购买 -> 再备份商品表。 结果就是:备份文件里,用户的钱没扣,但商品的库存却少了。等以后用这个备份文件恢复数据时,用户相当于白嫖了一件商品,这老板不得气死?加上全局锁,整个库不让改,就不会出现这种时间差导致的不一致了。

Q3:加全局锁会带来什么大坑?有替代方案吗?

缺点很明显:加上全局锁,意味着整个库在此期间只能读、不能写,业务直接大面积停滞。如果你的库有几百 GB,备份要几个小时,这段时间业务直接瘫痪。

有什么优雅的替代方案吗?有的!前提是你的存储引擎(比如 InnoDB)支持可重复读(Repeatable Read)的事务隔离级别。 在使用官方备份工具mysqldump时,加上-single-transaction参数。它会在备份前开启一个事务,利用MVCC(多版本并发控制)生成一个 Read View。整个备份期间都在读这个旧版本的视图,而其他业务线程依然可以正常对数据进行更新操作,互不干扰。

注意:对于 MyISAM 这种不支持事务的古老引擎,想备份保证一致性,就只能乖乖用全局锁了。


二、 表级锁

MySQL 里面的表级锁主要分为四种:表锁、元数据锁(MDL)、意向锁、AUTO-INC 锁。

1. 表锁

Q1:表锁怎么加?比如我们想对学生表(t_student)加锁:

SQL

-- 表级别的共享锁(读锁) lock tables t_student read; -- 表级别的独占锁(写锁) lock tables t_student write;

释放的话同样也是unlock tables或者等会话断开。

需要特别注意的是,表锁不仅限制别的线程,还会限制你自己!如果你的线程对表加了“共享读锁”,那么你自己接下来想写这张表,也会被报错阻止。 在 InnoDB 引擎下,千万别傻乎乎地去用表锁,它的颗粒度太大了,严重影响并发。InnoDB 的杀手锏是下面要讲的行锁。

2. 元数据锁(MDL)

Q2:什么是 MDL?我需要手动加吗?

不需要显示调用,MySQL 会自动帮你加。

  • 当你对表执行 CRUD(增删改查)操作时,会自动加MDL 读锁

  • 当你对表结构进行变更(比如加个字段、删个索引)时,会自动加MDL 写锁

它的作用是保证读写隔离:当有人在查表的时候,防止另一个愣头青突然把表结构给改了(比如删了个列)。

Q3:MDL 锁什么时候释放?长事务为什么会导致数据库崩溃?

MDL 锁要等到事务提交后才会释放。这就引出了生产环境一个极其经典的血案:长事务阻塞导致线程池爆满。

场景是这样的:

  1. 线程 A 开启了事务,执行了select(拿到 MDL 读锁),但一直没提交(长事务)

  2. 线程 B 正常select,没问题,因为读读不冲突。

  3. 线程 C 想修改表结构alter table,需要申请 MDL 写锁。但因为 A 的读锁还没释放,C 被阻塞,进入等待队列。

  4. 恐怖的事情来了:因为写锁的优先级高于读锁,一旦 C 在排队等写锁,后续所有想要查询这张表(申请读锁)的普通select请求,全部会被 C 挡在门外,全部阻塞!

  5. 如果这是一个高频查询的表,几秒钟内就会堆积成千上万个阻塞线程,数据库瞬间被打挂。

避坑指南:在执行 DDL(表结构变更)之前,一定要先查一下有没有长事务在跑,如果有,可以考虑先把它kill掉再改表结构。

3. 意向锁(Intention Lock)

Q4:为什么要有意向锁?

当你在 InnoDB 里对某行记录加锁之前,引擎会先在“表级别”加一个对应的“意向锁”。

  • 准备加行级共享锁 -> 先加表级意向共享锁 (IS)

  • 准备加行级独占锁 -> 先加表级意向独占锁 (IX)

它的核心目的只有一个:为了快速判断表里有没有行锁。假设没有意向锁,现在有人想对这张表加“独占表锁”,他必须一行一行去遍历:第一行有锁吗?第二行有锁吗?…… 效率极低。 有了意向锁之后,他只要看一眼表上有没有“意向锁”。如果有,说明表里肯定有某行被锁了,直接等待即可,省去了遍历所有记录的麻烦。

(注:意向锁之间完全不冲突,它只和表锁冲突,不和行锁冲突。)

4. AUTO-INC 锁

Q5:自增主键底层的 AUTO-INC 锁是怎么玩的?

这是一种特殊的表级锁,用于保证AUTO_INCREMENT字段递增的连续性。 传统的 AUTO-INC 锁,是在执行插入语句时加上,插入语句执行完立马释放(不用等事务提交)。但如果是批量插入,这个表锁依然会严重影响其他事务的插入并发度。

后来 InnoDB 引入了更优的轻量级锁:申请完自增 ID 后立马释放锁,根本不等语句执行完。 这由参数innodb_autoinc_lock_mode控制:

  • = 0:用传统的 AUTO-INC 锁。

  • = 1:普通插入用轻量级锁,批量插入用 AUTO-INC 锁。

  • = 2:全部用轻量级锁(性能最高)

注意坑点:如果你用了mode = 2,并且 binlog 的格式是statement(记录原始 SQL),在主从复制时可能会出现主库和从库生成的自增 ID 顺序不一致的问题。最佳实践是:mode = 2搭配binlog_format = row,既能火力全开保证高并发,又能保证主从数据绝对一致。


三、 行级锁

行级锁是 InnoDB 的独门绝技(MyISAM 不支持)。普通的select是快照读,不加锁。如果你想显式加行锁,可以这样(称为锁定读):

SQL

-- 加共享锁 (S锁) select ... lock in share mode; -- 加独占锁 (X锁) select ... for update;

(必须在开启事务的前提下使用!)

行级锁主要有三类:

1. Record Lock(记录锁)

仅仅锁住一条记录。 S 锁和 S 锁兼容;X 锁和谁都互斥(写写互斥、读写互斥)。 比如select * from t where id = 1 for update,就给 id=1 的这行加上了 X 型记录锁。

2. Gap Lock(间隙锁)

只存在于可重复读(Repeatable Read)隔离级别,用来解决幻读问题。 它不锁具体的记录,而是锁住两个记录之间的“间隙”。 比如给 (3, 5) 加上了间隙锁,这时候别人想insert一条 id = 4 的数据,直接被阻塞。(注:间隙锁之间是完全兼容的,无论是 X 还是 S 型,大家都可以给同一个间隙加锁,因为它们的目的都是一样的——防止别人插数据进来。)

3. Next-Key Lock(临键锁)

这是 InnoDB 默认的行锁基本单位。它就是Record Lock + Gap Lock 的终极合体。 它既锁住范围,又锁住记录本身。比如范围是(3, 5]的 Next-Key Lock,它不仅不让别人往 3 到 5 之间插数据,连 id=5 这条记录本身也给你锁死,别人改不了。它既保护了现有数据,又防住了幻读。

4. 插入意向锁(Insert Intention Lock)

名字里有“意向锁”,但它不是表锁,而是一种特殊的间隙锁(属于行级)。 当一个事务想要插入新数据,发现这个位置已经被别人加了间隙锁(或 Next-Key Lock),它就会阻塞等待,并生成一个“插入意向锁”。 它锁住的其实是一个点,代表“我想在这里插数据,但我现在只能排队”。不同事务在同一个间隙内插入不同的位置,生成的插入意向锁彼此不冲突。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询