前言
应用卡顿?数据库死锁?这些问题的背后,往往藏着 MySQL"锁"这个神秘角色。它既是维持数据秩序的交通警察,也可能是造成拥堵的固执保镖。让我来带你一文看懂全局锁、表级锁与行级锁,让你彻底告别并发烦恼。
全局锁
想象一下,你们公司要年底盘点,老板大手一挥:"都别动!所有货物都不准动,我要看看今年到底赚了多少钱!"
这时候,MySQL 里的 "全局锁" 就登场了。它最霸道,一旦你开启了 "全局锁",整个数据库就变成了"只读"模式。
- 作用场景: 主要就是为了"全库逻辑备份"。你想想,如果不把整个 "仓库" 都锁住,你这边刚数完A货架的货,那边销售又卖出去两个,这账不就乱了吗?所以,必须锁上 "仓库大门",暂停所有"写"操作(增、删、改),让你安安心心地把所有数据复制一份带走。
- 如何使用: 开启全局锁非常简单,只需要一条命令就可以让 MySQL 变成只读模式。
- 开启全局锁:
FLUSH TABLES WITH READ LOCK
- 释放锁:
UNLOCK TABLES
- Tips:当你开启全局锁的会话关闭之后也会自动将锁释放。
- 开启全局锁:
表级锁
在 MySQL 中表级锁分为:表锁 、元数据锁 、意向锁 、AUTO-INC 锁。
表锁
MySQL 的表锁有两种,分别叫读锁 / 共享锁(Table Read Lock)、写锁 / 独占锁(Table Write Lock)。
- 读锁 (Table Read Lock)
- 想象一下:在一个餐厅中,服务员对店里的客人说:"大家可以进来参观和阅读菜单(Read ),但不许任何人点菜(Write )!"这个时候很多歌想要"参观" / "阅读"(Read )菜单都可以进入这个包厢;这个时候服务员自己也只能看(Read )不能点菜(Write ),任何想要"点菜"(Write )的客人都会被拦在门外,必须等门锁解除后才能"点菜"(Write)。
- 这把锁也叫共享锁,当一个会话(Session)给一张表加上了读锁,这个会话自己只能读不能写 ,其他会话也只能读不能写 ,大家都可以共享读取的权限,任何写入操作(
INSERT
,UPDATE
,DELETE
)都会被阻塞,直到锁被释放。 - 如何使用 :
- 开启读锁 / 共享锁:
LOCK TABLES table_name READ
- 释放锁:
UNLOCK TABLES
- 开启读锁 / 共享锁:
- 写锁(Table Write Lock)
- 想象一下:在一个餐厅中,厨师对店里的所有人说:"我要修改菜单(Write )了,任何人都不许点菜(Write )!看一眼菜单(Read )都不行!"这个时候,只有这位厨师自己可以在包厢里随意查看修改菜单(Write、Read )。其他任何人,无论是想要看菜单(Read ),还是想要点菜(Write)的人统统都被拦在门外,只能在门口排队等着。
- 这把锁也叫独占锁,当一个会话给一张表加上了写锁,这个会话自己既可以读也可以写 ,其他任何会话的所有操作(读和写)都会被完全阻塞,直到锁被释放。
- 如何使用 :
- 开启写锁 / 独占锁:
LOCK TABLES table_name WRITE
- 释放锁:
UNLOCK TABLES
- 开启写锁 / 独占锁:
元数据锁
MySQL 的元数据锁,是 MySQL 非常重要的一个锁机制,元数据指的是"数据的数据",在 MySQL 中指的是数据库对象的定义,比如:表结构、视图、触发器、索引、存储过程......等等。而元数据锁就是用来保护这些元数据的,确保当前线程在数据被访问的时候,不会被其他线程修改。
想象一下:当您在A餐桌点菜吃饭时(执行查询等事务 ),系统会自动给这张桌子加上一个"使用中"的标记,这就是 共享元数据锁 。这个标记不影响其他客人来A桌吃饭(多个查询可并行 ),但它唯一的作用就是告诉老板:桌子有人用,您暂时不能改造或丢掉它(执行ALTER/DROP TABLE
等操作)。
但问题也随之而来:如果您这顿饭吃得特别久(长事务未结束),那么想改造桌子的老板就只能一直等待。更糟的是,所有想来A桌的新客人(新的查询请求),看到老板在等,也必须排队,最终导致整个餐厅门口大排长龙,谁也进不来。
MySQL 元数据锁住要目的是为了解决 DML(数据操作语言)和 DDL(数据定义语言)操作之间的冲突问题。当一个事务在使用某个表的元数据时,元数据锁会阻止其他会话对这个表的结构进行修改。MySQL 元数据锁主要分为两种类型:
- 共享元数据库锁 :
- 当执行 DML 操作时(如
SELECT
,INSERT
,UPDATE
,DELETE
)会对表加上共享锁。共享锁之间是兼容的,多个事务可以同时持有同一张表的共享 MDL,互不影响,保证了 DML 操作的并发性。
- 当执行 DML 操作时(如
- 排他元数据锁 :
- 当执行 DDL 操作时(如
ALTER TABLE
,DROP TABLE
,TRUNCATE TABLE
)会对表加上排他锁。排他锁与任何其他元数据锁(包括共享锁和排他锁)都是互斥的,这意味着,如果要对一个表执行 DDL,必须等到所有正在使用该表的事务(即使是只读查询)全部结束。反之,如果一个表上有 DDL 操作正在等待或执行,那么其他所有想访问该表的事务(包括SELECT
)都必须等待。
- 当执行 DDL 操作时(如
意向锁
MySQL的意向锁(Intention Lock)是一种由 InnoDB 引擎自动管理的 表级锁,但它本身不锁定任何具体的数据行。它的核心作用是一个"信号"或"意向声明",用来协调不同粒度的锁(表锁与行锁)之间可能发生的冲突。
意向锁的出现主要是为了解决性能问题。想象一下,当一个事务想锁定整张表时(如LOCK TABLES
),如果没有意向锁,数据库就必须遍历表中的每一行去检查是否存在行锁,这对于大表来说是灾难性的。
有了意向锁,过程就变得高效:当一个事务要给某几行加行锁(如SELECT ... FOR UPDATE
)时,它会先在表上加上一个意向排他锁。之后,当另一个事务想锁整张表时,只需检查表上是否存在意向锁,就能立刻知道内部有行锁,从而避免了全表扫描。
AUTO-INC 锁
MySQL的自增锁(AUTO-INC Lock)是一种特殊的表级锁,专用于保护AUTO_INCREMENT
自增列,确保在多事务并发INSERT
时,自增ID能够唯一、有序地分配,避免冲突。
它的具体行为由innodb_autoinc_lock_mode
参数控制,直接影响数据库的并发性能。主要有三种模式:
- 传统模式(
innodb_autoinc_lock_mode
= 0) :在整个INSERT
语句执行期间都持有表级的AUTO-INC锁,直到语句结束。这种方式最安全但并发性能最差。 - 连续模式(
innodb_autoinc_lock_mode
= 1) :这是一种混合模式。对于可以预知插入行数的简单INSERT
,它使用一个轻量级的锁在内存中快速分配ID后就释放,不阻塞其他事务。但对于INSERT ... SELECT
等无法预知行数的批量插入,仍会使用传统的表锁,以保证分配的ID块是连续的。 - 交错模式(
innodb_autoinc_lock_mode
= 2):完全放弃表级锁,全部使用轻量级锁。这种模式下并发性能最好,但代价是单条批量插入语句产生的自增ID可能不再连续。
行锁
在 MySQL 中行锁分为:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁 (Next-Key Lock)。
记录锁
- 记录锁(Record Lock)顾名思义,就是会锁定单条记录的锁
- 记录锁有两种实现:共享锁(S 锁)和排他锁(X 锁)
- 如何加锁 :
- 共享锁:
- 需要手动添加。
- MySQL 8.0+ 推荐:
SELECT ... FOR SHARE;
- 旧版语法(仍可用):
SELECT ... LOCK IN SHARE MODE;
- MySQL 8.0+ 推荐:
- 需要手动添加。
- 排他锁:
- 自动加锁 :执行
INSERT
,UPDATE
,DELETE
操作时,InnoDB 会自动为涉及的行加上X锁。 - 手动加锁 :执行
SELECT ... FOR UPDATE;
- 自动加锁 :执行
- 共享锁:
间隙锁
- 间隙锁(Gap Lock )不锁定任何记录本身,它锁定的是索引记录之间的"间隙 ",其设计的核心目的是为了在 可重复读(Repeatable Read)隔离级别下,防止幻读 的发生。
- 如何加锁 :
- 使用范围查询 (如
BETWEEN
,>
,<
)或者查询一个不存在的记录时,会触发间隙锁。
- 使用范围查询 (如
临键锁
- 临键锁 (Next-Key Lock) 是 记录锁 + 间隙锁 的组合,它会锁定一个索引记录本身,以及该记录之前的那个间隙。
- 临键锁是 InnoDB 在可重复读(Repeatable Read)隔离级别下的默认锁定算法。它既能防止数据被修改,又能防止幻读。
- 如何加锁 :
- 表中有
id
为 10 和 20 的记录,SELECT * FROM users WHERE id < 15 FOR UPDATE;
会锁定id=10
的记录(记录锁)以及id=10
之前的间隙(-∞, 10]
(间隙锁),组合成一个临键锁。这样,其他事务既不能修改id=10
的记录,也不能在(-∞, 10)
这个区间插入新数据。
- 表中有