MySQL锁机制全解析:共享锁、排他锁、意向锁、行锁、表锁

 更新时间:2026年11月11日 08:48:54   作者:大尚来也  
线上数据库最常见的几类故障,几乎都和锁有关,本文把 MySQL(InnoDB)锁体系按 锁粒度 → 锁模式 → 兼容性 → 实际触发场景 → 死锁分析​ 的顺序拆开讲透,配大量可复现示例,适合当锁问题的排查手册,需要的朋友可以参考下

引言

线上数据库最常见的几类故障,几乎都和锁有关:

  • 接口大面积超时,但 SQL 并不慢,SHOW PROCESSLIST 里一排 Waiting for table metadata lock
  • 批量更新接口偶发死锁,日志里抛出 Deadlock found when trying to get lock
  • 一张小表执行 ALTER TABLE 卡了半小时,所有读写全部阻塞

这些问题的根源,是开发阶段对 MySQL 锁机制缺乏系统认知——分不清什么时候加行锁、什么时候退化成表锁、共享锁和排他锁怎么互斥、意向锁到底解决什么问题。

本文把 MySQL(InnoDB)锁体系按 锁粒度 → 锁模式 → 兼容性 → 实际触发场景 → 死锁分析​ 的顺序拆开讲透,配大量可复现示例,适合当锁问题的排查手册。

一、为什么需要锁:并发带来的三类问题

没有锁控制,多个事务并发操作同一份数据会出现:

问题

现象

例子

脏读​

读到别的事务未提交的数据

事务 A 改了余额还没提交,事务 B 读到了

不可重复读​

同一事务内两次读同一行,结果不同

事务 A 读余额为 100,事务 B 扣了 50 提交,事务 A 再读变成 50

幻读​

同一事务内两次范围查询,行数变了

事务 A 查 WHERE age > 20 返回 5 行,事务 B 插入一条 age=25 提交,事务 A 再查返回 6 行

MySQL 通过 事务隔离级别 + 锁机制​ 组合解决这些问题。InnoDB 默认隔离级别是 REPEATABLE READ,在这个级别下,靠行锁 + 间隙锁解决幻读。

二、MySQL 锁的整体分类框架

先建立全局视角,后面逐个展开:

MySQL 锁体系(InnoDB) ├── 按粒度分 │ ├── 表级锁 │ │ ├── 表锁(手动 LOCK TABLES) │ │ ├── 元数据锁 MDL(自动,DDL 相关) │ │ └── 意向锁 IS / IX(自动,协调表锁和行锁) │ └── 行级锁 │ ├── 记录锁 Record Lock(锁具体行) │ ├── 间隙锁 Gap Lock(锁范围间隙) │ └── 临键锁 Next-Key Lock(记录锁 + 间隙锁,默认) ├── 按模式分 │ ├── 共享锁 S(读锁) │ └── 排他锁 X(写锁) └── 特殊锁 ├── 自增锁 AUTO-INC └── 谓词锁(空间索引,极少用)

三、表级锁:表锁与元数据锁(MDL)

3.1 表锁(手动控制)

MyISAM 时代的主力锁,InnoDB 下不推荐使用。

-- 加读锁(共享)
LOCK TABLES users READ;
-- 加写锁(排他)
LOCK TABLES users WRITE;
-- 解锁(释放当前会话所有表锁)
UNLOCK TABLES;

特点:

  • 加读锁:本会话和其他会话都能读,本会话不能写,其他会话写会阻塞
  • 加写锁:本会话可读写,其他会话读写全部阻塞
  • 粒度粗,并发低,InnoDB 一般只在 ALTER TABLE 等 DDL 时隐式使用

3.2 元数据锁 MDL(Metadata Lock)

自动加的,不需要你写任何命令。

-- 事务 A(未提交)
BEGIN;
SELECT * FROM users WHERE id = 1;   -- 加 MDL 读锁
-- 事务 B(同时执行)
ALTER TABLE users ADD COLUMN age INT;  -- 需要 MDL 写锁,被阻塞!
-- 事务 C(同时执行)
SELECT * FROM users WHERE id = 2;   -- 需要 MDL 读锁,排在 B 后面,也被阻塞!

这就是线上最常见的"加个字段,整个表卡死"的原因。

MDL 规则:

  • 读操作加 MDL 读锁,写操作加 MDL 写锁
  • 读锁之间不互斥,读写锁、写写锁互斥
  • 事务提交才释放 MDL 锁

生产教训:长事务 + DDL = 全表阻塞。执行 DDL 前先查 SHOW PROCESSLIST 和 information_schema.innodb_trx 确认没有长事务。

四、行级锁:记录锁、间隙锁、临键锁

InnoDB 的核心锁机制,只锁需要的行(或间隙),并发度高。

4.1 记录锁(Record Lock)

锁索引上的具体记录。

-- 对 id=5 这一行加记录锁
SELECT * FROM users WHERE id = 5 FOR UPDATE;

其他事务不能修改 id=5 这一行,但可以操作其他行。

4.2 间隙锁(Gap Lock)

锁住两条记录之间的"间隙",防止其他事务往里面插入数据。

-- 假设表中 id 有 1, 5, 10
-- 下面语句锁住 (5, 10) 这个间隙
SELECT * FROM users WHERE id BETWEEN 6 AND 9 FOR UPDATE;

效果:其他事务执行 INSERT INTO users (id) VALUES (7) 会被阻塞。

目的:防止幻读。​ 事务 A 范围查询后,事务 B 不能往这个范围里插新行。

4.3 临键锁(Next-Key Lock)

InnoDB 默认行锁算法 = 记录锁 + 间隙锁。

锁住:当前记录 + 前面到上一条记录之间的间隙。

索引值:  1    5    10   20
临键锁范围:
  id=5 的锁覆盖 (1, 5]  -- 包含 5 本身
  id=10 的锁覆盖 (5, 10]
-- 默认 RR 隔离级别下
SELECT * FROM users WHERE id > 5 AND id < 15 FOR UPDATE;

锁住 (5, 10] 和 (10, 15) 范围,其他事务不能插入 id=7、12,也不能修改 id=10。

RC 隔离级别(READ COMMITTED)下没有间隙锁,只锁命中的行。所以 RC 下可能出现幻读,但并发度更高。很多互联网公司用 RC + 业务层兜底来换取性能。

五、共享锁(S)与排他锁(X):读写互斥的核心规则

5.1 基本概念

锁类型

别名

用途

加锁方式

共享锁 S

读锁

事务读数据,允许其他事务也读

SELECT ... LOCK IN SHARE MODE

排他锁 X

写锁

事务修改数据,其他事务不能读也不能写

SELECT ... FOR UPDATE / UPDATE / DELETE / INSERT

5.2 互斥规则

S锁    X锁
S锁    兼容    互斥
X锁    互斥    互斥

简单记:只要有一方是 X 锁,就互斥。

5.3 实战示例

-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- 加 S 锁
-- 事务 B(同一时刻)
SELECT * FROM users WHERE id = 1;                     -- ✅ 普通读走快照,不阻塞
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- ✅ 加 S 锁,兼容
SELECT * FROM users WHERE id = 1 FOR UPDATE;          -- ❌ 要加 X 锁,被阻塞
UPDATE users SET name='x' WHERE id = 1;               -- ❌ 要加 X 锁,被阻塞
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;           -- 加 X 锁
-- 事务 B
SELECT * FROM users WHERE id = 1;                     -- ✅ 普通读走 MVCC 快照,不阻塞
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- ❌ 要加 S 锁,被阻塞
UPDATE users SET name='x' WHERE id = 1;               -- ❌ 要加 X 锁,被阻塞

注意:普通 SELECT(不加锁)在 RR 和 RC 隔离级别下走 MVCC 快照读,不加任何锁,所以不会被阻塞。这是 InnoDB 高并发的关键设计。

六、意向锁(IS / IX):为什么需要它、怎么工作

6.1 问题:表锁和行锁怎么共存?

假设没有意向锁:

  • 事务 A 对 users 表的第 5 行加了行级 X 锁
  • 事务 B 要对 users 表加表级写锁

事务 B 怎么知道能不能加?它必须逐行扫描整个表检查有没有行锁——代价不可接受。

6.2 意向锁的作用

意向锁是表级锁,由 InnoDB 自动加,不需要手动干预。

  • 事务 A 想对某行加 S 锁 → InnoDB 自动先对表加 意向共享锁 IS
  • 事务 A 想对某行加 X 锁 → InnoDB 自动先对表加 意向排他锁 IX

这样事务 B 来加表锁时,只需要检查表上有没有 IX / IS 就行,不用逐行扫描。

6.3 实际效果

-- 事务 A
BEGIN;
UPDATE users SET name='x' WHERE id = 1;   -- 自动加 IX 锁 + 行级 X 锁
-- 事务 B
LOCK TABLES users WRITE;                  -- 需要表级 X 锁,和 IX 互斥,被阻塞

意向锁不阻塞行级操作,只和表级锁互斥。

七、锁的兼容性矩阵(一张表记牢)

已有 \ 请求

IS

IX

S(表锁)

X(表锁)

IS​

✅ 兼容

✅ 兼容

✅ 兼容

❌ 互斥

IX​

✅ 兼容

✅ 兼容

❌ 互斥

❌ 互斥

S(表锁)​

✅ 兼容

❌ 互斥

✅ 兼容

❌ 互斥

X(表锁)​

❌ 互斥

❌ 互斥

❌ 互斥

❌ 互斥

行级锁之间用临键锁规则判断,不走这张表。

八、锁的触发条件:不同 SQL 到底加了什么锁

这是生产排查最关键的部分。

8.1 前提条件

行锁只在走索引时生效。​ 没走索引 = 锁全表(退化成表级锁)。

8.2 各 SQL 加锁汇总(RR 隔离级别)

SQL 类型

索引情况

加锁行为

SELECT ...

任意

不加锁(MVCC 快照读)

SELECT ... LOCK IN SHARE MODE

走唯一索引命中单行

行级 S 锁(记录锁)

同上

走普通索引/范围

临键锁(Next-Key Lock)

SELECT ... FOR UPDATE

走唯一索引命中单行

行级 X 锁(记录锁)

同上

走普通索引/范围

临键锁

UPDATE / DELETE

走唯一索引命中单行

行级 X 锁

同上

走普通索引/范围

临键锁

INSERT

插入位置

行级 X 锁 + 插入意向间隙锁

8.3 退化成表锁的典型场景

-- name 没有索引
UPDATE users SET age = 20 WHERE name = '张三';

InnoDB 只能全表扫描逐行检查,每行都加 X 锁,效果等价于锁全表。其他事务对所有行的修改都被阻塞。

排查口诀:行锁没走索引 = 表锁。

九、死锁:产生条件、复现与排查

9.1 死锁的四个必要条件

  1. 互斥:资源不能共享
  2. 持有并等待:持有锁 A,等待锁 B
  3. 不可剥夺:锁只能主动释放
  4. 循环等待:事务 A 等 B,事务 B 等 A

9.2 经典死锁复现

-- 表结构
CREATE TABLE accounts (
  id INT PRIMARY KEY,
  balance DECIMAL(10,2)
);
-- 数据: id=1 balance=100, id=2 balance=200
-- 事务 A
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;  -- 锁 id=1
-- 事务 B(同时)
BEGIN;
UPDATE accounts SET balance = balance + 10 WHERE id = 2;  -- 锁 id=2
-- 事务 A(继续)
UPDATE accounts SET balance = balance + 10 WHERE id = 2;  -- 等事务 B 释放 id=2
-- 事务 B(继续)
UPDATE accounts SET balance = balance - 10 WHERE id = 1;  -- 等事务 A 释放 id=1
-- 💥 死锁!MySQL 检测到后回滚其中一个事务

9.3 死锁排查

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G

输出中找 LATEST DETECTED DEADLOCK 段:

*** (1) TRANSACTION:
UPDATE accounts SET balance = balance + 10 WHERE id = 2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 123 page no 3 ...
*** (2) TRANSACTION:
UPDATE accounts SET balance = balance - 10 WHERE id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 123 page no 3 ...

9.4 死锁预防策略

  1. 固定访问顺序:所有事务按相同顺序访问表和行(如永远先锁 id 小的)
  2. 缩短事务:事务里不做 RPC 调用、文件 IO
  3. 降低隔离级别:RC 级别无间隙锁,死锁概率大幅下降
  4. 加索引:避免全表扫描锁
  5. 重试机制:应用层捕获死锁异常,自动重试

十、锁问题的排查工具与命令

10.1 查看当前锁等待

-- MySQL 8.0+
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;
-- 老版本
SELECT * FROM information_schema.innodb_locks;
SELECT * FROM information_schema.innodb_lock_waits;

10.2 查看阻塞的线程

SHOW FULL PROCESSLIST;
-- 关注 State 列:
-- Waiting for table metadata lock → MDL 等待
-- Waiting for lock → 行锁等待

10.3 查看长事务

SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started ASC;

10.4 查看 MDL 锁

SELECT * FROM performance_schema.metadata_locks;

10.5 杀掉阻塞会话

KILL <thread_id>;

十一、生产环境锁优化实践

11.1 索引是锁优化的第一要务

-- ❌ 无索引,锁全表
UPDATE orders SET status = 'cancelled' WHERE user_phone = '13800138000';
-- ✅ 加索引,只锁命中的行
ALTER TABLE orders ADD INDEX idx_phone (user_phone);

11.2 控制事务粒度

// ❌ 事务里调外部接口
@Transactional
public void placeOrder() {
    updateInventory();      // 持有行锁
    callPaymentAPI();       // 网络调用耗时 3 秒,锁一直不释放
    updateOrderStatus();
}
// ✅ 拆分
public void placeOrder() {
    updateInventory();      // 短事务
    callPaymentAPI();       // 无锁
    updateOrderStatus();    // 短事务
}

11.3 避免热点行竞争

-- ❌ 高并发下秒杀同一行
UPDATE inventory SET stock = stock - 1 WHERE item_id = 1;
-- ✅ 拆分成多行,分散竞争
-- 提前创建 item_id=1 的 10 行库存记录,随机选一行扣减
UPDATE inventory SET stock = stock - 1
WHERE item_id = 1 AND stock > 0
LIMIT 1;

11.4 合理选择隔离级别

-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置为 READ COMMITTED(无间隙锁,并发高)
SET session transaction isolation level READ COMMITTED;

互联网业务常用 RC:避免间隙锁带来的锁等待,幻读问题由业务层兜底(如唯一索引约束)。

十二、速查总结表

概念

一句话

关键记忆点

表锁

锁整张表

粒度粗,并发低

行锁

锁索引上的行

必须走索引,否则退化表锁

S 锁

读锁,共享

和 S 兼容,和 X 互斥

X 锁

写锁,排他

和谁都互斥

IS / IX

表级意向锁

自动加,协调表锁和行锁

记录锁

锁具体行

唯一索引等值命中

间隙锁

锁范围不锁行

防插入,防幻读

临键锁

记录 + 间隙

InnoDB 默认算法

MDL

元数据锁

DDL 自动加,长事务是天敌

死锁

循环等待

固定访问顺序 + 短事务

总结

MySQL 锁机制的设计目标很明确:在保证数据一致性的前提下,尽可能提高并发度。

核心逻辑链:

  1. 表锁粗、行锁细​ → 行锁必须走索引
  2. S 和 X 互斥​ → 读写分离靠 MVCC 快照读绕过
  3. 意向锁​ → 让表锁不用逐行检查就能判断能不能加
  4. 临键锁 = 记录锁 + 间隙锁​ → RR 级别防幻读,代价是锁范围更大
  5. 死锁​ → 固定访问顺序 + 短事务 + 重试

线上锁问题的排查路径:

接口超时 → SHOW PROCESSLIST 看 State
→ 是 Waiting for table metadata lock?→ 查长事务 / DDL
→ 是 Waiting for lock?→ 查 data_lock_waits 定位阻塞源
→ 死锁?→ SHOW ENGINE INNODB STATUS 看详情
→ 根因:缺索引 / 事务太长 / 访问顺序不固定

理解锁机制不是为了背概念,而是为了在写每一行 SQL 时,脑子里能预判它会加什么锁、和谁互斥、会不会阻塞别人。这种"锁意识"是区分初级和资深后端开发的关键分水岭。

以上就是MySQL锁机制全解析:共享锁、排他锁、意向锁、行锁、表锁的详细内容,更多关于MySQL锁机制全解析的资料请关注脚本之家其它相关文章!

相关文章

  • MySql 备忘录

    MySql 备忘录

    在MySQL中如果不为NOT NULL字段赋值(等同于赋NULL值)也就是说,MySQL中NOT NULL并不是一个约束条件了
    2012-03-03
  • MySQL 中的行锁(Record Lock) 和 间隙锁(Gap Lock)详解

    MySQL 中的行锁(Record Lock) 和 间隙锁(Gap Lock)详解

    InnoDB中的行锁(RecordLock)和间隙锁(GapLock)是用于保证事务隔离性的两种锁机制,本文给大家介绍MySQL中的行锁(Record Lock) 和间隙锁(Gap Lock),感兴趣的朋友跟随小编一起看看吧
    2025-11-11
  • Mysql添加字段或索引导致锁表的处理方式

    Mysql添加字段或索引导致锁表的处理方式

    我们后端开发人员在实现业务修改时,经常需要更新数据库表结构,如增加字段、修改字段长度等等,下面这篇文章主要给大家介绍了关于Mysql添加字段或索引导致锁表的处理方式,需要的朋友可以参考下
    2023-05-05
  • MySQL的执行原理之 MySQL的查询重写规则详解

    MySQL的执行原理之 MySQL的查询重写规则详解

    MySQL 性能调优是一个复杂且多维度的过程,下面从数据库设计、查询优化、配置参数调整、硬件优化几个方面为你介绍相关的调优方法,本文给大家介绍MySQL的执行原理之 MySQL的查询重写规则,感兴趣的朋友一起看看吧
    2025-04-04
  • MySQL和MariaDB性能对比测试哪个更快

    MySQL和MariaDB性能对比测试哪个更快

    想知道MySQL和MariaDB谁更快吗?本文通过详细的插入和查询测试,对比了MySQL 8.0.19和MariaDB 10.4.12的性能差异,并回顾了MySQL的发展史以及MariaDB的诞生背景,帮你判断是否该迁移到MariaDB
    2026-10-10
  • MySQL索引的原理与性能优化设计教程(图文代码)

    MySQL索引的原理与性能优化设计教程(图文代码)

    本文详细介绍了索引的分类、功能和实现方式,包括主键索引、唯一索引、常规索引、全文索引以及聚簇索引和非聚簇索引,文章还探讨了索引设计的最佳实践,包括主键选择、索引覆盖等优化策略,为数据库性能优化提供了理论基础
    2026-01-01
  • MySQL9.1.0实现最基础主从复制的步骤

    MySQL9.1.0实现最基础主从复制的步骤

    本文主要介绍了使用Docker实现MySQL的主从复制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-02-02
  • 往MySQL中存储图片的方法

    往MySQL中存储图片的方法

    这篇文章主要介绍了往MySQL中存储图片的方法,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • MySQL批量删除海量数据的几种方法总结

    MySQL批量删除海量数据的几种方法总结

    在数据库的日常维护中,我们经常遇到需要删除大量数据的场景,例如,删除过期日志、清理历史数据等,但如果一次性删除大量数据,可能会导致锁表、事务日志暴增、影响数据库性能等问题,本文将介绍几种高效批量删除 MySQL 海量数据的方法,需要的朋友可以参考下
    2024-11-11
  • SELECT INTO 和 INSERT INTO SELECT 两种表复制语句详解(SQL数据库和Oracle数据库的区别)

    SELECT INTO 和 INSERT INTO SELECT 两种表复制语句详解(SQL数据库和Oracle数据库的

    我们经常会遇到需要表复制的情况,如将一个table1的数据的部分字段复制到table2中,或者将整个table1复制到table2中,这时候我们就要使用SELECT INTO 和 INSERT INTO SELECT 表复制语句了
    2019-03-03

最新评论