首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >「深度好文」MySQL 全局锁、表锁、行锁、间隙锁、临键锁详解,架构师必备高并发下的锁优化策略

「深度好文」MySQL 全局锁、表锁、行锁、间隙锁、临键锁详解,架构师必备高并发下的锁优化策略

作者头像
码哥字节
发布2025-11-24 18:29:13
发布2025-11-24 18:29:13
10
举报
文章被收录于专栏:后端架构师后端架构师

读者你好,我是《Redis 高手心法》畅销书作者,可以叫我码

余弦:码哥,我今天面试被问到 “事务并发执行会带来什么问题,并发安全如何解决呢?MySQL 有哪些锁?”

今天我要跟你聊聊 MySQL 的锁。数据库锁设计的初衷是处理并发问题。

并发事务访问相同记录的情况大致可以划分以下几种:

  • 读-读情况:即并发事务相继读取相同的记录。读取操作本身不会对记录有一毛钱影响,并不会引起什么问题,所以允许这种情况的发生。
  • 写-写情况:即并发事务相继对相同的记录做出改动。

作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。

而锁就是用来实现这些访问规则的重要数据结构。

根据加锁的范围,MySQL 的锁可以分为全局锁、表锁和行锁。

对于MyISAMMEMORYMERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的。

全局锁

余弦:什么是全局锁?干嘛用的?

全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。

当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞

  • 数据更新语句(数据的增删改)
  • 数据定义语句(包括建表、修改表结构等)
  • 更新类事务的提交语句。

全局锁的适应场景之一,做全库的逻辑备份。把整个库的表数据都查出来存储为文本。

余弦:让整库都只读,在备份期间都不能执行更新,业务基本上就得停摆。这怎么办?

是的。

  • 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
  • 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。

不加锁产生的问题

比如手机卡,购买套餐信息。这里分为两张表 u_acount (用于余额表),u_pricing (资费套餐表)。

  1. u_account 表中数据 用户 A 余额:300;u_pricing 表中数据 用户 A 套餐:空
  2. 发起备份,备份过程中先备份 u_account 表,备份完了这个表,这个时候 u_account 用户余额是 300。
  3. 这个时候套用户购买了一个资费套餐 100,餐购买完成,写入到 u_print 套餐表购买成功,备份期间的数据。
  4. 备份完成。

可以看到备份的结果是,u_account 表中的数据没有变, u_pricing 表中的数据 已近购买了资费套餐 100.

哪这时候用这个备份文件来恢复数据的话,用户 A 赚了 100 ,用户是不是很舒服啊。但是你的想想公司利益啊。

也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个数据是逻辑不一致的。

表级锁

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

表锁的语法是 lock tables … read/write。与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。

另一类表级的锁是 MDL(metadata lock)。MDL 不需要显式使用,在访问一个表的时候会被自动加上。

当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。

不过请尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。

InnoDB的厉害之处还是实现了更细粒度的行锁,关于表级别的大家了解一下就罢了。

余弦:在使用MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT属性。之后在插入记录时,可以不指定该列的值,系统会自动为它赋上递增的值。 这是什么锁?

比方说我们有一个表:

代码语言:javascript
复制
CREATE TABLE t (
    id INT NOT NULL AUTO_INCREMENT,
    c VARCHAR(100),
    PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;

系统实现这种自动给AUTO_INCREMENT修饰的列递增赋值的原理主要是两个:

  • 采用表级别AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。
    • 这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
    • 如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用INSERT ... SELECTREPLACE ... SELECT或者LOAD DATA这种插入语句,一般是使用AUTO-INC锁为AUTO_INCREMENT修饰的列生成对应的值。
  • 轻量级的锁,这种方式可以避免锁定表,可以提升插入性能:
    • 在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。
    • 如果我们的插入语句在执行前就可以确定具体要插入多少条记录,比方说我们上边举的关于表t的例子中,在语句执行前就可以确定要插入 2 条记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。

行锁

行锁,也称为记录锁,顾名思义就是在记录上加的锁。这是最复杂的锁,前面的只是开胃菜。

一个行锁玩出了各种花样,也就是把行锁分成了各种类型。

MySQL 的行级锁是 InnoDB 存储引擎实现高并发的核心技术之一。它允许在保证数据一致性的同时,大幅提升数据库的并发处理能力。

下面这张表格汇总了行级锁的主要类型和核心特点,可以帮助你快速建立整体印象。

锁类型

英文名

锁定对象

主要作用

常见触发方式

记录锁

Record Lock

索引中的单条记录

防止其他事务修改或删除被锁定的记录

SELECT ... FOR UPDATE或更新语句通过索引精准匹配到一条存在记录时

间隙锁

Gap Lock

索引记录之间的间隙

防止其他事务在间隙中插入新数据,从而避免“幻读”

范围查询(如 WHERE id BETWEEN 5 AND 10)或查询不存在的唯一记录时

临键锁

Next-Key Lock

记录锁 + 间隙锁,锁定一个左开右闭的区间

既防止幻读,又保证当前读的数据一致性,是 InnoDB 在可重复读隔离级别下的默认锁算法

范围查询或在非唯一索引上的等值查询时

共享锁与排他锁

从锁的互斥性来看,行级锁分为共享锁和排他锁,它们的兼容关系是理解锁冲突的基础。

  • 共享锁(Shared Lock, S Lock):也称为“读锁”。
    • 作用:允许多个事务同时读取同一行数据。
    • 互斥性:一个事务持有共享锁后,其他事务可以继续加共享锁来读取数据,但不能加排他锁来修改数据
    • 加锁方式:使用 SELECT ... LOCK IN SHARE MODE;SELECT ... FOR SHARE;
  • 排他锁(Exclusive Lock, X Lock):也称为“写锁”。
    • 使用 SELECT ... FOR UPDATE;显式加锁。
    • 自动加锁UPDATE, DELETE, INSERT语句会自动对其操作的记录加排他锁。
    • 作用:允许一个事务读写某行数据。
    • 互斥性:一个事务持有排他锁后,其他事务不能再对该行加任何类型的锁(包括共享锁和其他排他锁),直到该锁被释放。
    • 加锁方式

它们的兼容关系可以总结为下表:

当前已持有的锁

请求 共享锁 (S)

请求 排他锁 (X)

共享锁 (S)

✔️ 兼容

❌ 冲突

排他锁 (X)

❌ 冲突

❌ 冲突

行锁的底层实现和特性

行锁基于索引实现

这是理解行锁最核心的一点。InnoDB 的行锁是加在索引项上的,而不是直接加在物理数据行上的。这意味着:

  • 有效使用行锁的前提:你的 WHERE条件必须能够有效命中索引。
  • 否则退化为表锁:如果查询条件不能使用索引,MySQL 将进行全表扫描,从而对所有记录加锁,实际效果等同于锁住了整个表,会严重降低并发性能

两阶段锁协议(Two-Phase Locking, 2PL)

InnoDB 遵循此协议,锁的操作分为两个阶段:

  • 加锁阶段:在事务执行过程中,根据需要逐步获取锁。
  • 解锁阶段:直到事务提交(COMMIT)或回滚(ROLLBACK)时,一次性释放所有在该事务中获取的锁。
  • 重要提示:由于锁是在事务结束后才释放,为了减少锁冲突和提高并发性,应尽量将最可能引起冲突的写操作(如 SELECT ... FOR UPDATE)放在事务的后面执行,以缩短排他锁的持有时间。

意向锁(Intention Locks)

意向锁是表级锁用于快速判断表内是否有被锁定的行,从而避免为了检查行锁而需要遍历每一行的低效操作

  • 意向共享锁(IS):表示事务准备在表中的某些行上加共享锁。
  • 意向排他锁(IX):表示事务准备在表中的某些行上加排他锁。意向锁之间是兼容的(例如,多个事务可以同时对一个表持有 IX 锁),但它们与表级共享锁(S)和排他锁(X)存在互斥关系。意向锁由 InnoDB 自动管理,无需用户干预

行锁类型可视化

理解行锁的关键在于区分其三种基本类型。下图通过一个数据索引的例子,清晰展示了三种锁的锁定范围差异,这是理解所有高级锁概念的基础。

图解说明

  • 记录锁(Record Lock):像一把钥匙只锁一个特定的座位(如id=10)。它确保在更新或删除某条确切存在的记录时,其他事务无法修改或删除它。
  • 间隙锁(Gap Lock):锁住的是座位与座位之间的“空档”(如(10, 15))。它的存在仅仅是为了防止插入(防止幻读),但不会阻止其他事务修改这个区间内已存在的记录(例如,如果id=12存在,你仍然可以修改它)。
  • 临键锁(Next-Key Lock):这是 InnoDB 在可重复读(RR)隔离级别下默认的锁算法。它是记录锁和间隙锁的结合,锁定一个左开右闭的区间。例如,(5, 10]意味着它锁定了id=10这条记录,也锁定了510之间的间隙。这能有效防止在10之前插入新数据(解决幻读),同时保护10这条记录本身。

以下通过示例和场景进一步解释这三种锁。

记录锁(Record Lock)

它锁住的是索引项。例如,执行 SELECT * FROM users WHERE id = 10 FOR UPDATE;会在 id=10这个索引项上加一个排他型的记录锁,防止其他事务修改或删除这行数据。

间隙锁(Gap Lock)

它锁住的是索引项之间的“空隙”,以防止其他事务在这个空隙中插入新数据,从而解决“幻读”问题。

间隙锁只在可重复读(REPEATABLE READ)及以上隔离级别生效

  • 示例:假设一张表 usersid字段有值 5, 10, 15。
  • 事务 A 执行:SELECT * FROM users WHERE id BETWEEN 10 AND 15 FOR UPDATE;
  • 它不仅会锁住 id=1015的记录,还会锁住它们之间的间隙 (10, 15)
  • 此时事务 B 尝试执行 INSERT INTO users (id) VALUES (12);会被阻塞,因为 12落在了被锁定的间隙内。

临键锁(Next-Key Lock)

它是 InnoDB 在可重复读(REPEATABLE READ)隔离级别下默认使用的锁算法

它相当于一个 记录锁 + 间隙锁,锁定一个左开右闭的区间 (previous_index, current_index]

  • 示例:同样对于 id值为 5, 10, 15 的表。
  • 事务 A 执行:SELECT * FROM users WHERE id > 10 FOR UPDATE;
  • 它可能锁住的区间包括 (10, 15](15, +∞)
  • 这既防止了在 (10, 15)区间内插入新的 id=12,也防止了修改或删除 id=15的现有记录,同时还防止了插入任何大于 15 的新 ID。

死锁与最佳实践

死锁是如何产生的呢?

行级锁虽然提升了并发度,但也带来了死锁的风险。当两个或多个事务互相等待对方释放锁时,就会形成死锁。

理解 MySQL 中事务的加锁流程以及死锁如何形成,是构建高并发应用的基石。

下面我们通过一个清晰的流程图来展示一个安全的事务加锁/解锁全过程,然后深入剖析几种典型的死锁场景。

事务加锁与解锁完成流程

首先要明确一个核心概念:两阶段锁协议。它规定锁的操作分为两个阶段:

  1. 加锁阶段:在事务执行过程中,根据需要逐步获取锁。
  2. 解锁阶段:直到事务提交(COMMIT)或回滚(ROLLBACK)时,一次性释放所有在该事务中获取的锁。

下面的序列图清晰地展示了一个安全、无冲突的事务加锁与解锁流程。

流程解读

  • 加锁:事务 A 首先开启事务,然后执行一条 SELECT ... FOR UPDATE语句,意图锁定某行数据(例如行 1)进行更新。此时,它会向锁管理器申请该行的排他锁(X Lock)
  • 执行:成功获得锁后,事务 A 可以安全地执行修改操作。在此期间,其他事务(如事务 B)如果尝试获取同一行的排他锁或共享锁,都会被阻塞。
  • 解锁:当事务 A 提交(COMMIT)后,进入解锁阶段,一次性释放它持有的所有锁。
  • 后续:锁被释放后,之前被阻塞的事务 B 会被唤醒,并获得它所需要的锁,继续执行。

这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。

典型死锁场景详解

这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。

场景 1:共享锁升级导致的死锁

这是非常经典的死锁情况,常发生在先读后写的业务逻辑中。

时间点

事务 A

事务 B

T1

SELECT * FROM accounts WHERE id=1 LOCK IN SHARE MODE; (获得 id=1 的S 锁)

T2

SELECT * FROM accounts WHERE id=1 LOCK IN SHARE MODE; (也获得 id=1 的S 锁)

T3

UPDATE accounts SET balance = balance - 100 WHERE id=1; (尝试将S 锁升级为 X 锁,但需等待事务 B 的 S 锁释放,阻塞)

T4

UPDATE accounts SET balance = balance - 50 WHERE id=1; (也尝试将S 锁升级为 X 锁,但需等待事务 A 的 S 锁释放)

死锁形成

  1. 在 T3 时刻,事务 A 需要事务 B 释放 S 锁才能升级。
  2. 在 T4 时刻,事务 B 需要事务 A 释放 S 锁才能升级。
  3. 双方互相等待,形成循环等待,死锁产生。
场景 2:顺序交叉访问导致的死锁

当多个事务以不同的顺序访问和锁定资源时,极易发生死锁。

时间点

事务 A

事务 B

T1

UPDATE accounts SET ... WHERE id=1; (成功获得 id=1 的X 锁)

T2

UPDATE accounts SET ... WHERE id=2; (成功获得 id=2 的X 锁)

T3

UPDATE accounts SET ... WHERE id=2; (尝试获取 id=2 的 X 锁,被事务 B 阻塞)

T4

UPDATE accounts SET ... WHERE id=1; (尝试获取 id=1 的 X 锁,被事务 A 阻塞)

死锁形成

  • 事务 A 在等待事务 B 释放 id=2 的锁。
  • 事务 B 在等待事务 A 释放 id=1 的锁。
  • 循环等待再次形成,死锁发生。
场景 3:Gap 锁冲突导致的死锁

可重复读(REPEATABLE READ)隔离级别下,MySQL 会使用间隙锁(Gap Lock)来防止幻读,这也可能引发更复杂的死锁。

假设 accounts表 id 有 1, 5, 10 三个值,存在间隙 (1,5), (5,10)。

时间点

事务 A

事务 B

T1

SELECT * FROM accounts WHERE id=3 FOR UPDATE; (在间隙(1,5)上加了Gap 锁)

T2

SELECT * FROM accounts WHERE id=4 FOR UPDATE; (同样在间隙(1,5)上加了Gap 锁,Gap 锁之间不互斥,成功)

T3

INSERT INTO accounts (id, ...) VALUES (3, ...); (尝试插入,需要获取插入意向锁,与事务 B 的 Gap 锁冲突,阻塞)

T4

INSERT INTO accounts (id, ...) VALUES (4, ...); (同样需要获取插入意向锁,与事务 A 的 Gap 锁冲突)

死锁形成

  • 事务 A 的插入在等待事务 B 释放(1,5)间隙上的锁。
  • 事务 B 的插入在等待事务 A 释放(1,5)间隙上的锁。
  • 循环等待形成,死锁发生。

死锁的处理与预防

nnoDB 存储引擎内置了死锁检测机制。当检测到死锁时,它会选择一个回滚代价较小的事务(通常是影响行数较少的事务)进行回滚,并让另一个事务继续执行。

被回滚的事务会收到 ERROR 1213 (40001): Deadlock found错误。

核心预防策略
  1. 固定访问顺序:在应用设计中,保证所有事务以相同的顺序访问数据行。例如,约定更新账户时总是按 id 从小到大处理,可以避免场景二的死锁
  2. 避免在事务中加锁:如果业务允许,使用乐观锁(如版本号机制)替代悲观锁,从根本上减少锁竞争
  3. 保持事务小巧且快速大事务会长时间持有锁,增加死锁概率。应将大事务拆分为小事务
  4. 为查询创建合适的索引:如果查询条件未使用索引,InnoDB 可能会锁住更多记录(甚至全表),大幅增加死锁风险。
  5. 使用较低的隔离级别:如果业务能接受,将隔离级别设为读已提交(READ COMMITTED),此级别下 InnoDB 不会使用间隙锁(Gap Lock),可减少死锁。

事务的加锁解锁遵循“两阶段锁协议”,提交或回滚时释放所有锁。死锁的本质是事务间形成了对锁资源的循环等待

通过理解其原理并采用固定的资源访问顺序、使用乐观锁、减小事务粒度等预防措施,可以显著降低死锁发生概率。

希望这些图解和说明能帮助你更深入地理解 MySQL 的锁机制。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码哥跳动 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 全局锁
    • 不加锁产生的问题
  • 表级锁
  • 行锁
    • 共享锁与排他锁
    • 行锁的底层实现和特性
    • 行锁类型可视化
  • 死锁与最佳实践
    • 事务加锁与解锁完成流程
    • 典型死锁场景详解
      • 场景 1:共享锁升级导致的死锁
      • 场景 2:顺序交叉访问导致的死锁
      • 场景 3:Gap 锁冲突导致的死锁
    • 死锁的处理与预防
      • 核心预防策略
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档