什么是数据库锁?行锁、表锁、意向锁一次讲清

6 阅读9分钟

什么是数据库锁?行锁、表锁、意向锁一次讲清


一、先解决一个灵魂问题:为什么需要锁?

想象一个银行转账场景:

账户A余额:10000元
账户B余额:10000元

线程1A 转账 2000B
线程2:同时查询 A 的余额

如果没有锁,可能发生:

线程1:读取 A=10000
线程2:读取 A=10000  ← 此时线程1还没扣款
线程1:A = 10000 - 2000 = 8000
线程1:B = 10000 + 2000 = 12000

线程2读到的 A=10000 是脏数据——它不知道线程1正在修改。

锁的本质就是:在一段时间内,对某个数据的访问权限进行控制,保证数据一致性。


二、锁的粒度:从大到小排排坐

数据库锁
├── 表级锁(Table Lock)    -- 锁整张表
├── 页级锁(Page Lock)     -- 锁一页数据(InnoDB内部)
└── 行级锁(Row Lock)     -- 锁一行数据

粒度越小,并发越高,但锁的开销越大。

📌 MySQL 的 InnoDB 引擎,默认是行级锁,但也会自动升级为表锁。


三、表锁:简单粗暴,但杀伤力大

3.1 什么是表锁?

一次锁住整张表,其他线程对这张表的任何操作都得等。

sql
-- 显式加表锁
LOCK TABLES user READ;      -- 读锁:别人只能读,不能写
-- 你的查询操作...
UNLOCK TABLES;

LOCK TABLES user WRITE;     -- 写锁:别人读写都不行
-- 你的修改操作...
UNLOCK TABLES;

3.2 表锁的两种模式

锁类型别名兼容性
读锁共享锁(S锁)多个读锁可以共存
写锁排他锁(X锁)写锁独占,谁都不能进

兼容性矩阵

S锁(读)X锁(写)
S锁✅ 兼容❌ 互斥
X锁❌ 互斥❌ 互斥

简单记:读读不冲突,读写冲突,写写冲突。

3.3 谁会加表锁?

MyISAM 引擎:默认就是表锁,不管你查一行还是查一万行,整张表都锁住。

sql
-- MyISAM 下,这两句都会锁表
SELECT * FROM user WHERE id = 1;
UPDATE user SET name = '张三' WHERE id = 1;

InnoDB 引擎:一般不加表锁,但以下情况会自动升级

触发条件原因
更新没有索引的列InnoDB 没法定位到行,只能锁表
语句影响的行数超过阈值(约5000行)行锁太多,不如直接锁表
显式加锁 LOCK TABLES你自己要求的

3.4 表锁的优缺点

优点缺点
加锁快,开销小并发能力极差
不会出现死锁(一次锁一张表)一锁锁全表,其他人全等
适合读多写少的场景写操作会被严重阻塞

四、行锁:精细控制,但有讲究

4.1 什么是行锁?

只锁住被操作的那一行,其他行不受影响。

sql
-- InnoDB 默认行为
UPDATE user SET name = '张三' WHERE id = 1;
-- 只锁 id=1 这一行,其他线程还能改 id=2, id=3...

4.2 行锁的两种实现方式

InnoDB 的行锁,底层其实是通过索引实现的:

方式说明举例
记录锁(Record Lock)锁住索引记录本身WHERE id = 1,锁住 id=1 的索引项
间隙锁(Gap Lock)锁住索引之间的间隙,防止插入WHERE id = 1,锁住 (0,1) 和 (1,2) 的间隙
临键锁(Next-Key Lock)记录锁 + 间隙锁,锁住一个左开右闭区间锁住 (0, 1]

📌 很多人以为行锁就是锁一行,其实 InnoDB 默认用的是临键锁,锁的是一个范围。这就是为什么有时候你明明只改了一行,却发现别人也插不进数据。

4.3 举个例子感受一下

假设表中有 id:1, 3, 5, 7

sql
-- 事务A
BEGIN;
SELECT * FROM user WHERE id = 3 FOR UPDATE;  -- 临键锁:锁住 (1, 3]

-- 事务B(此时执行)
INSERT INTO user (id) VALUES (2);  -- ❌ 阻塞!间隙 (1,3) 被锁了
INSERT INTO user (id) VALUES (4);  -- ✅ 成功!间隙 (3,5) 没被锁

这就是间隙锁的作用:防止幻读。

4.4 行锁的加锁时机

操作加锁时机
SELECT ... FOR UPDATE读取时加锁
SELECT ... LOCK IN SHARE MODE读取时加共享锁
UPDATE / DELETE找到记录时加锁
INSERT插入时加间隙锁(防止幻读)
普通 SELECT❌ 不加锁(MVCC 机制保证一致性)

📌 普通查询不加锁,靠的是 MVCC(多版本并发控制) ——读的是快照,写的是新版本,读写互不阻塞。这是 InnoDB 并发能力强的核心原因之一。

4.5 行锁的死锁问题

行锁虽然并发高,但因为锁的粒度细,死锁概率反而更高

sql
-- 事务A
BEGIN;
UPDATE user SET name = 'A' WHERE id = 1;  -- 锁住 id=1
UPDATE user SET name = 'B' WHERE id = 2;  -- 等 id=2 的锁

-- 事务B(同时执行)
BEGIN;
UPDATE user SET name = 'C' WHERE id = 2;  -- 锁住 id=2
UPDATE user SET name = 'D' WHERE id = 1;  -- 等 id=1 的锁 → 死锁!

死锁解决:InnoDB 会自动检测死锁,回滚代价较小的那个事务。

📌 避免死锁的技巧:

  • 多个表操作时,按固定顺序加锁
  • 大事务拆小事务
  • 尽量用索引查询,减少锁的范围

五、意向锁:你可能没听过,但它一直在工作

5.1 为什么需要意向锁?

回到表锁和行锁的矛盾:

事务A:想给整张表加表锁
事务B:已经锁了表里的某几行

如果没有意向锁,事务A 加表锁前,必须逐行检查:
"id=1 锁了吗?id=2 锁了吗?id=3 锁了吗?..."
-- 100万行?查到天荒地老

意向锁就是一个"标记":告诉别人"这张表里面有人加了行锁,你想锁表就得等"。

5.2 意向锁的类型

锁类型含义谁加的
IS(意向共享锁)"我打算给某些行加共享锁"事务在加行级共享锁之前先加 IS
IX(意向排他锁)"我打算给某些行加排他锁"事务在加行级排他锁之前先加 IX

5.3 意向锁的兼容性

ISIX表级S锁表级X锁
IS
IX
表S
表X

关键点

  • 意向锁之间互相兼容(IS 和 IX 可以共存)
  • 表级共享锁和意向锁兼容(因为读锁不冲突)
  • 表级排他锁和任何意向锁都互斥(你想锁整张表,但里面已经有行锁了,不行)

5.4 加锁过程图解

事务A:UPDATE user SET name='张三' WHERE id=1;

加锁顺序:
1. 先给 id=1 的记录加 X 锁(行锁)
2. 再给整张 user 表加 IX 锁(意向锁)

事务B:LOCK TABLES user WRITE;  -- 想锁整张表

加锁顺序:
1. 试图给整张表加 X 锁
2. 检查到表上已经有 IX 锁 → 冲突,等待!

→ 事务B 不用逐行检查,看到 IX 就知道"表里有人在改"

5.5 一句话理解意向锁

意向锁 = 表级的"预告片":我要锁表里的某些行了,你想锁整张表就先等等。

它本身不阻塞任何操作,只是一个信号,让表锁和行锁之间的冲突检测从"逐行扫描"变成"一眼判断"。


六、三种锁对比总结

维度表锁行锁意向锁
锁的范围整张表一行或多行整张表(只是标记)
加锁开销极小
并发能力不影响并发
死锁风险几乎没有较高
谁用得多MyISAMInnoDBInnoDB(内部自动加)
典型场景读多写少,全表扫描高并发增删改行锁和表锁之间的协调
用户能直接控制吗✅ LOCK TABLES✅ SELECT ... FOR UPDATE❌ 自动加,不可手动控制

七、InnoDB 锁的完整体系(一张图搞懂)

InnoDB 锁体系
│
├── 按粒度分
│   ├── 表锁(LOCK TABLES,用户显式加)
│   ├── 行锁(Record Lock / Gap Lock / Next-Key Lock)
│   └── 意向锁(IS / IX,自动加)
│
├── 按模式分
│   ├── 共享锁(S锁):读锁,别人也能读,不能写
│   └── 排他锁(X锁):写锁,别人读写都不行
│
├── 按场景分
│   ├── DML 锁:UPDATE/DELETE/INSERT 自动加的行锁
│   ├── 查询锁:SELECT ... FOR UPDATE / LOCK IN SHARE MODE
│   └──  表级锁:LOCK TABLES 显式加
│
└── MVCC(不是锁,但实现了读写不阻塞)
    ├── 普通 SELECT 读快照,不加锁
    └── 写操作生成新版本,读旧版本

八、实战:你一定会遇到的锁问题

问题1:为什么我的 UPDATE 锁住了整张表?

sql
-- user 表的 name 列没有索引
UPDATE user SET name = '张三' WHERE name = '李四';

原因name 没索引,InnoDB 找不到具体哪些行要改,只能全表扫描 + 锁所有扫描到的行,等价于锁表。

解决:给 name 加索引。


问题2:为什么并发插入时出现死锁?

sql
-- 两个事务同时插入,且有唯一索引
-- 事务A:INSERT ... ON DUPLICATE KEY UPDATE id=1
-- 事务B:INSERT ... ON DUPLICATE KEY UPDATE id=2
-- 如果唯一键冲突,都会加间隙锁,可能死锁

原因:唯一索引的插入也会加间隙锁,两个事务互相等对方释放间隙锁。

解决

  • 捕获死锁异常,自动重试
  • 插入顺序保持一致

问题3:SELECT ... FOR UPDATE 到底锁了什么?

sql
-- 假设 id 是主键,有索引
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 锁住:id=1 的记录 + (0,1) 和 (1,2) 的间隙 → 临键锁

-- 假设 age 没有索引
SELECT * FROM user WHERE age = 25 FOR UPDATE;
-- 锁住:全表!因为没索引,只能全表扫描,锁所有行

📌 没有索引的 FOR UPDATE = 锁表,这是最常见的"行锁变表锁"的坑。


九、最后一张速查表

场景实际加的锁备注
SELECT ... FOR UPDATE(有索引)行级排他锁(临键锁)锁记录+间隙
SELECT ... LOCK IN SHARE MODE行级共享锁别人可以读,不能改
UPDATE/DELETE(有索引)行级排他锁找到记录就锁
UPDATE/DELETE(无索引)表锁全表扫描,锁所有行
INSERT间隙锁(防止幻读)插入前先锁住目标位置的间隙
LOCK TABLES ... WRITE表级排他锁显式锁表
事务开始加行锁前自动加意向锁(IX)协调表锁和行锁

十、一句话总结

表锁简单粗暴,行锁精细但有坑,意向锁是幕后协调员。

InnoDB 默认行锁,但没索引就变表锁;
有索引还得注意间隙锁和临键锁;
意向锁你看不见,但每次行锁都有它在帮忙。

理解锁,不是为了背概念,是为了写 SQL 时知道:这句话下去,会锁住什么,会阻塞谁。