乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

锁机制

导学

事务的隔离性靠什么实现?答案是锁。MySQL 5.7 的 InnoDB 提供了丰富的锁机制,理解锁的类型、粒度和兼容性,是排查死锁和性能问题的关键。

定义

锁(Lock):数据库用于控制并发访问的机制。锁防止多个事务同时修改同一数据,确保数据一致性。InnoDB 支持行级锁和表级锁,默认使用行级锁。

准备测试数据

CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2)
);

INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);

当前数据状态:

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部NULL

行级锁类型

共享锁(S Lock / Shared Lock)

共享锁允许多个事务同时读取同一行数据,但阻止其他事务获取排他锁。

示例:共享锁的并发效果

时间顺序会话 A会话 B
T1START TRANSACTION;
T2SELECT * FROM employees WHERE emp_id = 1 LOCK IN SHARE MODE;
T3START TRANSACTION;
T4SELECT * FROM employees WHERE emp_id = 1 LOCK IN SHARE MODE; -- 成功
T5UPDATE employees SET score = 150 WHERE emp_id = 1; -- 阻塞!
T6COMMIT;
T7UPDATE 继续执行

完整 SQL 演示:

-- ========== 会话 A ==========
START TRANSACTION;

-- 对 emp_id = 1 的行加共享锁
SELECT * FROM employees WHERE emp_id = 1 LOCK IN SHARE MODE;

当前数据状态:

emp_idemp_namedeptscore
1大翔技术部100

结果解读:

  • LOCK IN SHARE MODE 对 id = 1 的行加了共享锁(S 锁)
  • 此时其他事务也可以对该行加 S 锁(兼容)
-- ========== 会话 B ==========
START TRANSACTION;

-- 也可以加共享锁(S 锁与 S 锁兼容)
SELECT * FROM employees WHERE emp_id = 1 LOCK IN SHARE MODE;

会话 B 的查询结果:

emp_idemp_namedeptscore
1大翔技术部100
-- 会话 B 尝试更新(需要 X 锁,与 S 锁冲突)
UPDATE employees SET score = 150 WHERE emp_id = 1;
-- 结果:被阻塞,等待会话 A 释放 S 锁

结果解读:

  • S 锁和 X 锁不兼容,会话 B 的 UPDATE 被阻塞
  • 只有会话 A 提交(COMMIT)后,会话 B 的 UPDATE 才能继续
-- ========== 会话 A ==========
COMMIT;

-- 会话 B 的 UPDATE 现在可以执行了
-- ========== 会话 B ==========
COMMIT;

-- 验证最终数据
SELECT * FROM employees WHERE emp_id = 1;

操作后的数据状态:

emp_idemp_namedeptscore
1大翔技术部150

排他锁(X Lock / Exclusive Lock)

排他锁只允许一个事务持有,阻止其他事务获取任何锁(包括 S 锁和 X 锁)。

示例:排他锁的并发效果

时间顺序会话 A会话 B
T1START TRANSACTION;
T2SELECT * FROM employees WHERE emp_id = 2 FOR UPDATE;
T3START TRANSACTION;
T4SELECT * FROM employees WHERE emp_id = 2; -- 快照读,不加锁,成功
T5SELECT * FROM employees WHERE emp_id = 2 LOCK IN SHARE MODE; -- 阻塞!
T6COMMIT;
T7S 锁查询继续执行

完整 SQL 演示:

-- ========== 会话 A ==========
START TRANSACTION;

-- 对 emp_id = 2 的行加排他锁
SELECT * FROM employees WHERE emp_id = 2 FOR UPDATE;

当前数据状态:

emp_idemp_namedeptscore
2白歌技术部NULL
-- ========== 会话 B ==========
START TRANSACTION;

-- 普通 SELECT(快照读,不加锁,可以执行)
SELECT * FROM employees WHERE emp_id = 2;

会话 B 普通查询结果:

emp_idemp_namedeptscore
2白歌技术部NULL
-- 会话 B 尝试加共享锁(S 锁与 X 锁冲突,阻塞)
SELECT * FROM employees WHERE emp_id = 2 LOCK IN SHARE MODE;
-- 结果:被阻塞,等待会话 A 释放 X 锁

结果解读:

  • 普通 SELECT(快照读)不加锁,不受 X 锁影响(MVCC 机制)
  • SELECT ... LOCK IN SHARE MODE 需要 S 锁,与 X 锁冲突,被阻塞
  • SELECT ... FOR UPDATE 需要 X 锁,也与 X 锁冲突
-- 会话 A 提交
COMMIT;

-- 会话 B 的 S 锁查询现在可以执行
-- ========== 会话 B ==========
COMMIT;

锁兼容性矩阵

SX
S✅ 兼容❌ 冲突
X❌ 冲突❌ 冲突

意向锁(Intention Locks)

意向锁是表级锁,用于表明事务稍后要对表中的某些行加锁:

  • IS(Intention Shared):事务意图对某些行加 S 锁
  • IX(Intention Exclusive):事务意图对某些行加 X 锁

意向锁之间互相兼容,但与表级 S/X 锁冲突。它的作用是快速判断表级锁是否可用,而无需检查每一行。

示例:意向锁的作用

-- 会话 A 对某行加行级 X 锁
START TRANSACTION;
SELECT * FROM employees WHERE emp_id = 1 FOR UPDATE;
-- 此时表级会加 IX 锁,行级加 X 锁
-- 会话 B 尝试对全表加读锁(表级 S 锁)
LOCK TABLES employees READ;
-- 结果:被阻塞!因为表上已有 IX 锁,与表级 S 锁冲突

结果解读:

  • 会话 A 的 FOR UPDATE 在表级加了 IX 意向锁
  • 会话 B 的 LOCK TABLES ... READ 需要表级 S 锁
  • IX 与表级 S 锁冲突,所以会话 B 被阻塞
  • 意向锁的存在让 MySQL 无需检查每一行,快速判断表级锁是否可用
-- 会话 A 提交
COMMIT;

-- 会话 B 的 LOCK TABLES 现在可以执行
UNLOCK TABLES;

Gap Lock 和 Next-Key Lock

Gap Lock(间隙锁)

间隙锁锁定索引记录之间的"间隙",防止其他事务在间隙中插入数据。

示例:Gap Lock 演示

当前 employees 表主键值:1, 2

间隙包括:(-∞, 1), (1, 2), (2, +∞)

-- ========== 会话 A ==========
START TRANSACTION;

-- 锁定 emp_id > 2 的间隙(查询返回空集,但会加 Gap Lock)
SELECT * FROM employees WHERE emp_id > 2 FOR UPDATE;

查询结果:空集(因为没有 emp_id > 2 的记录)

结果解读:

  • 虽然查询返回空集,但 FOR UPDATE 会在 (2, +∞) 的间隙上加 Gap Lock
  • 这会阻止其他事务插入 emp_id = 3 的记录
-- ========== 会话 B ==========
START TRANSACTION;

-- 尝试在间隙中插入数据
INSERT INTO employees (emp_name, dept, score) VALUES ('孔蓝', '技术部', 90);
-- 结果:被阻塞!因为间隙 (2, +∞) 被 Gap Lock 锁定
-- ========== 会话 A ==========
COMMIT;

-- 会话 B 的 INSERT 现在可以执行
-- ========== 会话 B ==========
COMMIT;

-- 验证插入成功
SELECT * FROM employees WHERE emp_name = '孔蓝';

操作后的数据状态:

emp_idemp_namedeptscore
3孔蓝技术部90

注:演示后执行 DELETE FROM employees WHERE emp_name = '孔蓝'; 恢复为 2 条数据。

Next-Key Lock(临键锁)

Next-Key Lock 是行锁 + 间隙锁的组合,锁定"索引记录本身 + 记录前面的间隙"。

示例:Next-Key Lock 演示

-- ========== 会话 A ==========
START TRANSACTION;

-- 在 REPEATABLE READ 下,InnoDB 默认使用 Next-Key Lock
SELECT * FROM employees WHERE emp_id = 1 FOR UPDATE;

查询结果:

emp_idemp_namedeptscore
1大翔技术部100

结果解读:

  • Next-Key Lock 会锁定 id = 1 的行本身(行锁),以及前面的间隙 (-∞, 1](间隙锁)
  • 这会阻止其他事务:1) 修改 id = 1 的行;2) 在 (-∞, 1] 间隙中插入数据
-- ========== 会话 B ==========
START TRANSACTION;

-- 尝试修改 emp_id = 1 的行(被行锁阻塞)
UPDATE employees SET score = 150 WHERE emp_id = 1;
-- 结果:被阻塞

-- 尝试插入 emp_id = 0 的记录(被间隙锁阻塞)
INSERT INTO employees (emp_name, dept, score) VALUES ('赵鸣', '技术部', 80);
-- 结果:被阻塞(如果 emp_id=0 还不存在)
-- ========== 会话 A ==========
COMMIT;

-- 会话 B 的操作现在可以执行
-- ========== 会话 B ==========
COMMIT;

结果解读:

  • Next-Key Lock 是 InnoDB 在 REPEATABLE READ 下防止幻读的核心机制
  • 它同时保护已有记录和记录前的间隙

SQL 示例:死锁演示

场景:两个事务相互等待

操作过程

时间顺序会话 A会话 B
T1START TRANSACTION;
T2UPDATE employees SET score = 90 WHERE emp_id = 1;
T3START TRANSACTION;
T4UPDATE employees SET score = 80 WHERE emp_id = 2;
T5UPDATE employees SET score = 90 WHERE emp_id = 2; -- 阻塞
T6UPDATE employees SET score = 80 WHERE emp_id = 1; -- 死锁!
T7MySQL 检测到死锁,回滚其中一个

完整 SQL 演示:

-- ========== 会话 A ==========
START TRANSACTION;

-- 更新 emp_id = 1,加 X 锁
UPDATE employees SET score = 90 WHERE emp_id = 1;

-- 查看当前状态
SELECT * FROM employees WHERE emp_id IN (1, 2);

会话 A 中的中间状态:

emp_idemp_namedeptscore
1大翔技术部90
2白歌技术部NULL
-- ========== 会话 B ==========
START TRANSACTION;

-- 更新 emp_id = 2,加 X 锁
UPDATE employees SET score = 80 WHERE emp_id = 2;

-- 查看当前状态
SELECT * FROM employees WHERE emp_id IN (1, 2);

会话 B 中的中间状态:

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部80
-- ========== 会话 A ==========
-- 尝试更新 emp_id = 2(会话 B 已持有 X 锁,阻塞)
UPDATE employees SET score = 90 WHERE emp_id = 2;
-- 结果:等待会话 B 释放锁
-- ========== 会话 B ==========
-- 尝试更新 emp_id = 1(会话 A 已持有 X 锁,死锁!)
UPDATE employees SET score = 80 WHERE emp_id = 1;
-- 结果:ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

结果解读:

  • 会话 A 等待会话 B 释放 id = 2 的锁
  • 会话 B 等待会话 A 释放 id = 1 的锁
  • 形成循环等待,即死锁
  • MySQL 的 InnoDB 死锁检测器会自动检测到死锁,并选择代价较小的事务进行回滚
  • 被回滚的事务会收到 ERROR 1213,应用层应捕获此错误并重试
-- 被回滚的事务(假设是会话 B)需要重试
-- ========== 会话 B ==========
ROLLBACK;

-- 应用层应重新执行整个事务
START TRANSACTION;
UPDATE employees SET score = 80 WHERE emp_id = 2;
UPDATE employees SET score = 80 WHERE emp_id = 1;
COMMIT;

-- ========== 会话 A ==========
-- 会话 B 被回滚后,会话 A 的 UPDATE 继续执行
COMMIT;

-- 验证最终状态
SELECT * FROM employees WHERE emp_id IN (1, 2);

操作后的数据状态:

emp_idemp_namedeptscore
1大翔技术部90
2白歌技术部90

死锁排查

示例:查看死锁信息

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;
-- 在输出中查找 LATEST DETECTED DEADLOCK 部分

示例输出(节选):

------------------------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 100, OS thread handle 123456789, query id 500 localhost 127.0.0.1 root updating
UPDATE employees SET score = 90 WHERE emp_id = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `test`.`employees` trx id 12345 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 8 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 101, OS thread handle 123456790, query id 501 localhost 127.0.0.1 root updating
UPDATE employees SET score = 80 WHERE emp_id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `test`.`employees` trx id 12346 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `test`.`employees` trx id 12346 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (2)

结果解读:

  • TRANSACTION 12345:等待 id = 2 的行锁
  • TRANSACTION 12346:持有 id = 2 的行锁,等待 id = 1 的行锁
  • WE ROLL BACK TRANSACTION (2):MySQL 选择回滚事务 2(通常选择修改行数较少的事务)
-- 查看当前锁等待
SELECT * FROM information_schema.innodb_lock_waits;

-- 查看当前锁
SELECT * FROM information_schema.innodb_locks;

-- 查看当前事务
SELECT * FROM information_schema.innodb_trx;

减少死锁的建议

  1. 按固定顺序访问资源:所有事务都先操作 A 再操作 B
  2. 缩小事务范围:事务越短,锁持有时间越短
  3. 使用低隔离级别:READ COMMITTED 比 REPEATABLE READ 锁粒度更小
  4. 为查询条件列建索引:无索引时行锁可能升级为表锁

示例:按固定顺序避免死锁

-- 会话 A 和会话 B 都按 emp_id 升序更新,避免死锁

-- ========== 会话 A ==========
START TRANSACTION;
UPDATE employees SET score = 90 WHERE emp_id = 1;
UPDATE employees SET score = 90 WHERE emp_id = 2;
COMMIT;

-- ========== 会话 B ==========
START TRANSACTION;
UPDATE employees SET score = 80 WHERE emp_id = 1;  -- 等待 A 释放
UPDATE employees SET score = 80 WHERE emp_id = 2;  -- 等待 A 释放
COMMIT;

结果解读:

  • 两个事务都先更新 id = 1,再更新 id = 2
  • 会话 B 在更新 id = 1 时会等待会话 A 释放锁
  • 不会形成循环等待,因此不会死锁
  • 虽然会话 B 需要等待,但等待比死锁+回滚+重试更高效

常见误区

误区正解
InnoDB 没有表锁有。DDL 操作、无索引更新的行锁升级等情况会使用表锁。
SELECT 不加锁在 RR 下,普通 SELECT 是快照读(MVCC)不加锁;但 SELECT ... FOR UPDATE 和 SELECT ... LOCK IN SHARE MODE 会加锁。
死锁是 Bug死锁是正常现象。InnoDB 会自动检测并回滚其中一个事务,应用层只需捕获错误重试。

面试考点

Q:InnoDB 的行锁有哪些类型?

共享锁(S Lock)和排他锁(X Lock)。此外,为了支持多粒度锁定,还有意向共享锁(IS)和意向排他锁(IX)。在 RR 隔离级别下,还有 Gap Lock 和 Next-Key Lock。

Q:SELECT ... FOR UPDATE 和 SELECT ... LOCK IN SHARE MODE 的区别?

FOR UPDATE 加排他锁(X),其他事务不能读也不能写;LOCK IN SHARE MODE 加共享锁(S),其他事务可以读但不能写。

Q:如何避免死锁?

  1. 按固定顺序访问资源;2. 尽量缩短事务;3. 为查询条件列建索引避免锁升级;4. 应用层捕获死锁异常并重试。

小结

  • InnoDB 默认使用行级锁,包括共享锁(S)和排他锁(X)
  • 意向锁(IS/IX)用于表级锁的快速判断
  • Gap Lock 和 Next-Key Lock 是 RR 级别防止幻读的机制
  • 死锁是并发正常现象,MySQL 自动检测并回滚,应用层需重试

下一章引子:锁是悲观并发控制,而 MVCC 则是乐观并发控制的核心——它让读操作几乎不加锁,极大提升了并发性能。

上一页
事务隔离级别
下一页
MVCC