Skip to content

MySQL锁机制:表锁、页锁与行锁

在 MySQL 数据库的世界里,锁机制是保证数据一致性和并发性能的基石。
当我们谈论高并发、事务隔离时,本质上都是在讨论锁的博弈。

对于开发者而言,理解表锁、页锁和行锁的区别,以及 InnoDB 引擎下的加锁规则,是避免线上“慢查询”和“死锁”事故的必修课。


锁粒度的三足鼎立

MySQL 根据锁定数据范围的大小,将锁分为:

  • 表级锁(Table Lock)
  • 页级锁(Page Lock)
  • 行级锁(Row Lock)

这三种锁在并发性能、系统开销和死锁风险上呈现出截然不同的特征。

特性维度表锁(Table Lock)页锁(Page Lock)行锁(Row Lock)
锁定粒度整张表数据页(约 16KB)单行记录
并发性能最低(写操作阻塞所有读写)中等最高(允许多人操作不同行)
系统开销最小(加锁快,内存占用少)中等最大(需维护大量锁对象)
死锁风险有(概率较低)
代表引擎MyISAMBDB(旧版)InnoDB

1. 表锁

粒度最粗,属于“一锁锁全家”。

虽然并发性能最差,但因为实现简单、开销极小,非常适合以下场景:

  • 全表扫描
  • 批量更新
  • 数据迁移
  • 大批量导入

2. 页锁

页锁介于表锁和行锁之间,是一种折中方案。

InnoDB 会将数据划分为若干页(通常每页 16KB),页锁就是锁定这 16KB 内的所有数据。
它的并发度优于表锁,但容易出现“假冲突”:

  • 两个事务修改的是不同行
  • 但如果这些行恰好位于同一页中
  • 依然可能发生阻塞

3. 行锁

行锁是 InnoDB 引擎的核心能力之一。

它把锁粒度精确到单行,极大提升了并发处理能力。在以下场景中尤为重要:

  • 电商秒杀
  • 银行转账
  • 订单处理
  • 高并发账户操作

不同行的数据可被不同事务并发处理,彼此互不干扰。


如何正确加锁:实战操作指南

理解了理论,关键在于如何在代码中正确使用。不同类型的锁,对应不同的触发方式。

表锁的手动干预

虽然 InnoDB 默认使用行锁,但在某些特定场景下,比如大批量数据导入,我们可能会主动加表锁,以避免产生大量行锁日志。

示例:表锁操作

sql
-- 加读锁:当前会话可读,其他会话不可写
LOCK TABLES user READ;

-- 加写锁:当前会话可读写,其他会话被阻塞
LOCK TABLES user WRITE;

-- 释放锁
UNLOCK TABLES;

行锁的自动与显式控制

行锁的加锁方式更灵活,主要分为两类:

1. 自动加锁

当执行标准 DML 语句时,InnoDB 会自动对涉及的行加排他锁,例如:

  • UPDATE
  • DELETE

注意:这里有一个非常关键的前提,SQL 必须命中索引

sql
-- 自动加行锁(假设 id 是主键)
UPDATE user SET name = 'New Name' WHERE id = 1;

2. 显式加锁

如果在事务中需要先读取数据,再决定后续逻辑,可以使用显式锁。

  • SELECT ... FOR UPDATE:加排他锁
  • SELECT ... LOCK IN SHARE MODE:加共享锁
sql
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) 隔离级别下执行:

sql
SELECT * FROM user WHERE id > 10 FOR UPDATE;

它不仅会锁住:

  • 所有 id > 10 的现有记录

还会锁住:

  • 满足条件范围内的索引间隙

这样其他事务就不能在这个范围内插入新记录,从而避免幻读。


总结

掌握 MySQL 锁机制,核心不只是记住概念,而是理解它们在实际业务中的影响:

  • 表锁:开销小,但并发差
  • 页锁:折中方案,但可能产生假冲突
  • 行锁:并发高,但依赖索引,死锁风险也更高

在 InnoDB 中,真正决定锁粒度的关键,往往不是你“以为”用了行锁,而是:

  • SQL 是否命中索引
  • 事务是否足够短
  • 加锁范围是否可控

只有理解这些底层规则,才能在高并发场景下写出真正稳定、高效、安全的数据库代码。

Last updated: