锁(Lock)机制是并发控制的核心手段,用来在多个事务同时读写数据时保证一致性(Consistency)和隔离性(Isolation),避免脏读、不可重复读、幻读、丢失更新等问题。
下面按“做什么 → 锁什么 → 怎么锁 → 常见类型 → 典型问题”来梳理。
一、为什么需要锁
数据库支持多事务并发,如果不加控制会出现:
- 脏写:T1 改了,T2 又改,T1 回滚导致 T2 的修改消失
- 脏读:读到别的事务未提交的数据
- 不可重复读:同一事务内两次读同一行结果不同
- 幻读:同一查询条件,两次查出来的行数不同
锁的本质:让某些操作互斥或有序执行。
二、锁的基本维度
1. 按锁的粒度(Granularity)
- 行锁(Row Lock):锁单行,并发最高(MySQL InnoDB、Oracle 默认偏向)
- 页锁(Page Lock):锁一个数据页(SQL Server 早期常见)
- 表锁(Table Lock):锁整张表,开销小但并发差(MyISAM 常用)
- 全局锁:锁整个数据库(如
FLUSH TABLES WITH READ LOCK)
粒度越细 → 并发越好,但管理开销越大
2. 按锁的兼容性(模式)
共享锁(S 锁 / Read Lock)
- 允许多个事务同时读
- 阻塞写锁
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE; -- MySQL
排他锁(X 锁 / Write Lock)
- 只允许一个事务持有
- 阻塞其他读锁和写锁
SELECT * FROM t WHERE id=1 FOR UPDATE;
UPDATE t SET ... WHERE id=1;
意向锁(Intention Locks)
- 表级“声明”:我准备在表里锁某几行
- 提高“表锁 vs 行锁”冲突检测效率
- 如:IS(意向共享)、IX(意向排他)
三、InnoDB(MySQL)中的核心锁机制
1. 行锁实现方式
InnoDB 行锁是基于索引的:
- 走索引 → 锁索引记录(Record Lock)
- 没走索引 → 升级为近似表锁(锁很多行 / 全表扫描)
2. 三种行锁类型
- Record Lock:锁某一条索引记录
- Gap Lock:锁“记录之间的间隙”,防插入(解决幻读)
- Next-Key Lock = Record + Gap(默认 RR 级别防幻读)
例:
索引值:10, 20, 30
Next-Key Lock 可能锁住 (-∞,10], (10,20], (20,30], (30,+∞)
四、事务隔离级别与锁的关系
(以 SQL 标准 + InnoDB 为例)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁策略 |
|---|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 | 基本不加读锁 |
| Read Committed (RC) | 否 | 可能 | 可能 | 读不加锁(MVCC),写加 X 锁 |
| Repeatable Read (RR) | 否 | 否 | 否* | Next-Key Lock(InnoDB 默认) |
| Serializable | 否 | 否 | 否 | 读也加 S 锁,完全串行 |
MySQL InnoDB 在 RR 下通过 MVCC + Next-Key Lock 解决幻读
五、MVCC 与锁的关系(重点)
现代数据库(MySQL InnoDB、PostgreSQL)并不是“所有读都加锁”:
- 普通 SELECT(快照读):走 MVCC,读历史版本,不加锁
- SELECT … FOR UPDATE / LOCK IN SHARE MODE(当前读):加锁
- 写操作(UPDATE/DELETE):先当前读,再加 X 锁
所以:
锁主要负责“写-写”和“当前读-写”的互斥,MVCC 解决大多数“读-写”并发
六、常见锁问题
1. 死锁(Deadlock)
两个事务互相等待对方释放锁:
T1: 锁 A → 请求 B
T2: 锁 B → 请求 A
解决方式:
- 数据库自动检测并回滚代价小的事务
- 业务层:固定加锁顺序、缩短事务、降低隔离级别
2. 锁等待超时
ERROR 1205: Lock wait timeout exceeded
原因:长事务、热点行更新、索引缺失导致锁范围过大
3. 锁升级
- SQL Server:行锁 → 页锁 → 表锁(减少锁数量)
- MySQL InnoDB:不会显式“升级”,但无索引时等于锁全表
七、一句话总结
数据库锁机制 = 用不同粒度/模式的锁 + MVCC,在并发和一致性之间做权衡;
行锁保并发,X/S 锁保互斥,Gap/Next-Key 锁防幻读,死锁靠检测和设计规避。