MySQL锁机制:表锁、页锁与行锁
在 MySQL 数据库的世界里,锁机制是保证数据一致性和并发性能的基石。
当我们谈论高并发、事务隔离时,本质上都是在讨论锁的博弈。
对于开发者而言,理解表锁、页锁和行锁的区别,以及 InnoDB 引擎下的加锁规则,是避免线上“慢查询”和“死锁”事故的必修课。
锁粒度的三足鼎立
MySQL 根据锁定数据范围的大小,将锁分为:
- 表级锁(Table Lock)
- 页级锁(Page Lock)
- 行级锁(Row Lock)
这三种锁在并发性能、系统开销和死锁风险上呈现出截然不同的特征。
| 特性维度 | 表锁(Table Lock) | 页锁(Page Lock) | 行锁(Row Lock) |
|---|---|---|---|
| 锁定粒度 | 整张表 | 数据页(约 16KB) | 单行记录 |
| 并发性能 | 最低(写操作阻塞所有读写) | 中等 | 最高(允许多人操作不同行) |
| 系统开销 | 最小(加锁快,内存占用少) | 中等 | 最大(需维护大量锁对象) |
| 死锁风险 | 无 | 有(概率较低) | 高 |
| 代表引擎 | MyISAM | BDB(旧版) | InnoDB |
1. 表锁
粒度最粗,属于“一锁锁全家”。
虽然并发性能最差,但因为实现简单、开销极小,非常适合以下场景:
- 全表扫描
- 批量更新
- 数据迁移
- 大批量导入
2. 页锁
页锁介于表锁和行锁之间,是一种折中方案。
InnoDB 会将数据划分为若干页(通常每页 16KB),页锁就是锁定这 16KB 内的所有数据。
它的并发度优于表锁,但容易出现“假冲突”:
- 两个事务修改的是不同行
- 但如果这些行恰好位于同一页中
- 依然可能发生阻塞
3. 行锁
行锁是 InnoDB 引擎的核心能力之一。
它把锁粒度精确到单行,极大提升了并发处理能力。在以下场景中尤为重要:
- 电商秒杀
- 银行转账
- 订单处理
- 高并发账户操作
不同行的数据可被不同事务并发处理,彼此互不干扰。
如何正确加锁:实战操作指南
理解了理论,关键在于如何在代码中正确使用。不同类型的锁,对应不同的触发方式。
表锁的手动干预
虽然 InnoDB 默认使用行锁,但在某些特定场景下,比如大批量数据导入,我们可能会主动加表锁,以避免产生大量行锁日志。
示例:表锁操作
-- 加读锁:当前会话可读,其他会话不可写
LOCK TABLES user READ;
-- 加写锁:当前会话可读写,其他会话被阻塞
LOCK TABLES user WRITE;
-- 释放锁
UNLOCK TABLES;行锁的自动与显式控制
行锁的加锁方式更灵活,主要分为两类:
1. 自动加锁
当执行标准 DML 语句时,InnoDB 会自动对涉及的行加排他锁,例如:
UPDATEDELETE
注意:这里有一个非常关键的前提,SQL 必须命中索引。
-- 自动加行锁(假设 id 是主键)
UPDATE user SET name = 'New Name' WHERE id = 1;2. 显式加锁
如果在事务中需要先读取数据,再决定后续逻辑,可以使用显式锁。
SELECT ... FOR UPDATE:加排他锁SELECT ... LOCK IN SHARE MODE:加共享锁
START TRANSACTION;
-- 显式加排他锁,锁定 id = 1 的行
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 执行更新逻辑
UPDATE user SET money = money - 100 WHERE id = 1;
COMMIT;避坑指南:行锁变“表锁”的惨案
很多开发者在使用 InnoDB 时会遇到一个诡异的问题:
- 明明是行锁
- 为什么锁竞争依然严重
- 甚至表现得像整张表都被锁住了
答案通常藏在 索引 里。
核心原理
InnoDB 的行锁,本质上是通过给 索引项 加锁来实现的。
这意味着:
- 只有当 SQL 通过索引条件过滤数据时
- InnoDB 才能精确锁定目标行
如果 SQL 没有使用索引,例如:
WHERE条件字段没有索引- 对索引列做了函数运算
- 条件写法导致索引失效
那么 InnoDB 就只能执行 全表扫描。
在全表扫描过程中,它会把扫描到的每一行都加锁。
虽然从实现上仍然是“行锁”,但由于锁住了整张表的所有记录,最终效果就和“表锁”几乎没有区别。
最佳实践
为了避免锁粒度失控,建议遵循以下原则:
- 使用
EXPLAIN检查执行计划,确保WHERE条件命中索引 - 避免在索引列上做计算、函数操作或隐式类型转换
- 避免使用会导致索引失效的写法
- 批量更新时,优先先查出主键 ID,再按主键更新
- 尽量让事务更短,减少锁持有时间
深入 InnoDB 内部:意向锁与间隙锁
除了基础的表锁和行锁,InnoDB 还有两个高级机制非常值得了解:
- 意向锁(Intention Lock)
- 间隙锁(Gap Lock)
意向锁
意向锁是为了解决 表锁和行锁之间的协调问题。
假设:
- 事务 A 已经锁住了表中的某一行
- 事务 B 现在想对整张表加表锁
如果没有意向锁,事务 B 就需要逐行检查是否存在行锁,代价极高。
有了意向锁之后:
- 事务 A 在给某行加锁前
- 会先在表上加一个“意向锁”
- 事务 B 只需检查表级意向锁
- 就能快速判断是否可以加表锁
这样就避免了逐行扫描。
间隙锁
间隙锁是 InnoDB 为了解决 幻读 问题而设计的。
它锁住的不只是已有记录,还包括记录之间的“间隙”。
例如,在 可重复读(REPEATABLE READ) 隔离级别下执行:
SELECT * FROM user WHERE id > 10 FOR UPDATE;它不仅会锁住:
- 所有
id > 10的现有记录
还会锁住:
- 满足条件范围内的索引间隙
这样其他事务就不能在这个范围内插入新记录,从而避免幻读。
总结
掌握 MySQL 锁机制,核心不只是记住概念,而是理解它们在实际业务中的影响:
- 表锁:开销小,但并发差
- 页锁:折中方案,但可能产生假冲突
- 行锁:并发高,但依赖索引,死锁风险也更高
在 InnoDB 中,真正决定锁粒度的关键,往往不是你“以为”用了行锁,而是:
- SQL 是否命中索引
- 事务是否足够短
- 加锁范围是否可控
只有理解这些底层规则,才能在高并发场景下写出真正稳定、高效、安全的数据库代码。