首页 / MySQL 入门教程 / 锁机制

MySQL 入门教程

锁机制

本教程共 46 篇 · 第 39 篇 · 更新于 2026-07-30 · 约 14 分钟阅读

MySQLMySQL 入门教程行锁间隙锁临键锁死锁InnoDB

39. 锁机制

本节目标:搞清楚 InnoDB 中表锁和行锁的区别,理解共享锁/排他锁、记录锁/间隙锁/临键锁的作用,掌握死锁的产生原因和排查方法,了解乐观锁与悲观锁的思想。

39.1 为什么需要锁

多个事务同时修改同一份数据,如果不加控制就会互相覆盖、数据错乱。锁就是用来协调并发访问、保证数据一致性的机制。

打个比方:锁就像公共厕所的门锁。有人进去了把门锁上(加锁),其他人就得在外面等(阻塞),等里面的人出来(释放锁)才能进。

39.2 表锁 vs 行锁

按锁定粒度,MySQL 的锁分为两大类:

特性表锁(Table Lock)行锁(Row Lock)
锁定范围整张表单行或多行
并发度
开销
引擎支持MyISAM、InnoDB仅 InnoDB

表锁

MyISAM 引擎只支持表锁。LOCK TABLES 可以手动加表锁:

-- 加读锁
LOCK TABLES users READ;

-- 加写锁
LOCK TABLES users WRITE;

-- 释放
UNLOCK TABLES;

InnoDB 也支持表锁,但一般不用。InnoDB 的优势在于行锁。

Note

InnoDB 在执行某些 DDL 操作(如 ALTER TABLE)时会加表锁。普通 DML 操作默认走行锁。

行锁

InnoDB 的行锁是基于索引实现的。这意味着:只有通过索引条件检索数据时,才使用行锁;否则会退化为表锁。

-- 有索引的情况:行锁
-- 假设 id 是主键
BEGIN;
UPDATE users SET age = 30 WHERE id = 1;  -- 只锁 id=1 这一行
COMMIT;
-- 没有索引的情况:退化为表锁
-- 假设 age 列没有索引
BEGIN;
UPDATE users SET age = 30 WHERE age = 25;  -- 锁住整张表!
COMMIT;
Warning

这是个很隐蔽的坑。WHERE 条件没有走索引的 UPDATE/DELETE,InnoDB 会对整张表加锁,把并发完全堵死。确保更新和删除操作的 WHERE 条件走索引。

39.3 共享锁与排他锁

按锁的兼容性,分为两种:

锁类型别名加锁方式兼容性
共享锁(S Lock)读锁SELECT ... LOCK IN SHARE MODE与共享锁兼容
排他锁(X Lock)写锁SELECT ... FOR UPDATE与任何锁不兼容

共享锁(Shared Lock)

多个事务可以同时持有同一行的共享锁,但持有期间不能被修改。

-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- 加共享锁

-- 事务 B(另一个会话)
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- OK,共享锁兼容
UPDATE users SET age = 30 WHERE id = 1;  -- 阻塞!等事务 A 释放锁
COMMIT;

排他锁(Exclusive Lock)

只有持有排他锁的事务能修改数据,其他事务既不能读(当前读)也不能写。

-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 加排他锁

-- 事务 B
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 阻塞!等事务 A 释放
COMMIT;
Tip

FOR UPDATE 是实际开发中最常用的加锁方式。比如扣库存场景:先 SELECT ... FOR UPDATE 锁住库存行,再 UPDATE 扣减,保证不会超卖。

锁兼容矩阵

共享锁(S)排他锁(X)
共享锁(S)兼容不兼容
排他锁(X)不兼容不兼容

普通 SELECT(快照读)不加任何锁,不受影响。

39.4 意向锁(Intention Lock)

InnoDB 还有意向锁,是表级别的锁,用来表示”我打算在这张表的某些行上加锁”。

两种意向锁:

  • 意向共享锁(IS):打算在某些行上加共享锁;
  • 意向排他锁(IX):打算在某些行上加排他锁。

意向锁是 InnoDB 自动加的,不需要手动操作。它的作用是:当某个事务想加表锁时,快速判断表里有没有行锁,不用逐行检查。

事务 A 要给 id=1 加行级排他锁
  -> 先在表上加意向排他锁(IX)
  -> 再在行上加排他锁(X)

事务 B 要给整张表加表锁
  -> 检查表上有没有 IX 锁
  -> 有 IX 锁 -> 说明有行被锁了 -> 等待
Note

意向锁之间互相兼容,意向锁和行锁也兼容(不同行)。意向锁只和表锁冲突。这是 InnoDB 内部优化,开发者一般不用直接关心。

39.5 行锁的三种算法

InnoDB 的行锁有三种具体实现,这是理解锁机制的关键。

记录锁(Record Lock)

锁定索引上的一条记录。最简单的行锁。

-- 精确等值查询命中记录
-- 假设 id=1 存在
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 只锁 id=1 这条记录

间隙锁(Gap Lock)

锁定索引记录之间的”间隙”,防止其他事务在间隙中插入新记录。只在 REPEATABLE READ 及以上隔离级别生效。

假设 users 表 id 有:1, 5, 10
间隙为:(-∞, 1), (1, 5), (5, 10), (10, +∞)
-- 等值查询不命中记录时,加间隙锁
SELECT * FROM users WHERE id = 7 FOR UPDATE;
-- 锁住间隙 (5, 10),防止其他事务插入 id=6,7,8,9
Note

间隙锁的唯一目的是防止幻读。它阻止其他事务在间隙中插入新数据。间隙锁之间不冲突(多个事务可以同时持有同一间隙的间隙锁),但间隙锁会阻止 INSERT。

临键锁(Next-key Lock)

记录锁 + 间隙锁的组合,锁定一个左开右闭区间。这是 InnoDB 在 REPEATABLE READ 下默认的行锁算法。

假设 users 表 id 有:1, 5, 10
临键锁区间为:(-∞, 1], (1, 5], (5, 10], (10, +∞)
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;
-- 锁住 (1, 5] 和 (5, 10],也就是锁住 id=5 和 id=10 的记录,
-- 以及它们之间的间隙,防止插入 id=6,7,8,9

三种行锁对比

锁类型锁定范围防止什么触发条件
记录锁单条记录修改/删除冲突等值查询命中记录
间隙锁记录之间的间隙插入冲突等值查询不命中记录
临键锁记录 + 前面的间隙修改 + 插入冲突范围查询
Tip

临键锁是 InnoDB 防幻读的关键。在 REPEATABLE READ 下,范围查询会用临键锁锁住查询范围内的所有记录和间隙,其他事务无法在这个范围内插入新行,从而防止幻读。

临键锁退化为记录锁

当等值查询命中唯一索引(如主键)时,临键锁会退化为记录锁,不需要锁间隙(因为唯一值不可能重复插入)。

-- id 是主键,等值命中
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 只加记录锁,锁 id=5,不加间隙锁

39.6 死锁

死锁(Deadlock) 是两个或多个事务互相等待对方释放锁,形成循环等待,谁也动不了。

事务 A:锁了 id=1,等 id=2 的锁
事务 B:锁了 id=2,等 id=1 的锁
-> A 等 B,B 等 A,死锁

死锁演示

-- 事务 A
BEGIN;
UPDATE users SET age = 25 WHERE id = 1;  -- 锁 id=1

-- 事务 B(另一个会话)
BEGIN;
UPDATE users SET age = 30 WHERE id = 2;  -- 锁 id=2

-- 事务 A
UPDATE users SET age = 35 WHERE id = 2;  -- 等 B 释放 id=2 的锁

-- 事务 B
UPDATE users SET age = 40 WHERE id = 1;  -- 等 A 释放 id=1 的锁
-- 死锁!InnoDB 检测到后,回滚其中一个事务
-- ERROR 1213 (40001): Deadlock found when trying to get lock;
-- try restarting transaction

InnoDB 的死锁处理

InnoDB 有死锁检测机制:发现死锁后,自动选择一个”回滚代价最小”的事务进行回滚,让另一个事务继续执行。

-- 查看死锁日志
SHOW ENGINE INNODB STATUS\G
-- 找到 LATEST DETECTED DEADLOCK 部分

预防死锁

死锁不能完全避免,但可以减少发生概率:

1. 按固定顺序加锁

多张表或多行操作时,所有事务按相同顺序加锁,就不会形成循环等待。

-- 好的做法:所有事务都先锁 id 小的
-- 事务 A:先 id=1,再 id=2
-- 事务 B:先 id=1,再 id=2
-- 不会死锁

2. 大事务拆小

事务越长,持锁时间越长,死锁概率越高。把大事务拆成多个小事务。

3. 降低隔离级别

READ COMMITTED 没有间隙锁,锁的范围更小,死锁概率更低。

4. 加超时等待

-- 设置锁等待超时(秒),超时后报错而非一直等
SET innodb_lock_wait_timeout = 10;  -- 默认 50 秒
Warning

死锁被 InnoDB 自动回滚后,应用层应该捕获错误并重试。不要认为死锁是致命错误,在高并发系统中死锁是正常现象,重试通常就能成功。

39.7 乐观锁与悲观锁

这不是具体的锁实现,而是并发控制的两种思想

悲观锁(Pessimistic Lock)

假设冲突一定会发生,操作前先加锁。

-- 悲观锁实现扣库存
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;  -- 加排他锁
-- 应用层判断 stock > 0
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

特点:数据安全性高,但并发度低(其他事务要等)。

乐观锁(Optimistic Lock)

假设冲突很少发生,不加锁,更新时检查数据是否被改过。通常用版本号实现。

-- 乐观锁实现扣库存
-- products 表有 version 列

-- 第一步:读当前版本(不加锁)
SELECT stock, version FROM products WHERE id = 1;
-- 假设读到 stock=10, version=3

-- 第二步:更新时检查版本是否变化
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 3;
-- 如果 affected_rows = 1,说明没被别人改过,更新成功
-- 如果 affected_rows = 0,说明被别人改过了,重试

特点:并发度高,但冲突多时重试开销大。

对比悲观锁乐观锁
假设冲突频繁冲突稀少
实现FOR UPDATE版本号/CAS
并发度
适用场景写多读少读多写少
Tip

乐观锁适合读多写少的场景(如商品详情页),悲观锁适合写多冲突多的场景(如秒杀扣库存)。实际项目中两种经常混用。

39.8 查看锁信息

-- MySQL 8.0+ 查看当前锁
SELECT * FROM performance_schema.data_locks;

-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;

-- 查看事务
SELECT * FROM information_schema.INNODB_TRX;
Note

MySQL 8.0 之前用 SHOW ENGINE INNODB STATUS 查看锁信息,格式不太友好。8.0+ 的 performance_schema.data_locks 表更清晰。

39.9 小结

  • InnoDB 行锁基于索引实现,无索引退化为表锁;
  • 共享锁(读锁)兼容,排他锁(写锁)独占;
  • 意向锁是表级锁,用于快速判断表上是否有行锁;
  • 行锁三种算法:记录锁(锁单行)、间隙锁(锁间隙防插入)、临键锁(记录+间隙,防幻读);
  • 唯一索引等值命中时临键锁退化为记录锁;
  • 死锁由 InnoDB 自动检测和回滚,按固定顺序加锁可预防;
  • 悲观锁先加锁,乐观锁用版本号检测,适合不同并发场景。

下一节进入索引世界,学习 B+ 树、聚簇索引和各类索引的原理。