01 - 为什么需要锁
在讲各种锁之前,先搞清楚一个根本问题:数据库为什么要有锁? 答案很简单——因为很多人同时操作同一份数据时,不加控制就会出乱子。
一句话先记住:锁的本质,是用「排队等待」换「数据正确」。没有并发,就不需要锁。
1.1 并发带来的四个经典问题
当多个事务同时读写同一份数据,可能出现这四种问题。理解它们,就理解了锁存在的意义。
问题一:脏读(Dirty Read)—— 读到别人没提交的数据
事务A 事务B
│ │
│ 把余额从 100 改成 500 │
│ (还没提交) │
│ │ 读到余额 = 500 ← 脏读!
│ 回滚!余额变回 100 │
│ │ B 拿着错的 500 去干活了
B 读到了一个「根本不存在(被回滚了)」的值。
问题二:不可重复读(Non-repeatable Read)—— 同一行前后读的值不一样
事务A 事务B
│ 读余额 = 100 │
│ │ 把余额改成 500 并提交
│ 再读余额 = 500 ← 咦?变了! │
A 在同一个事务里,两次读同一行,结果不一致。
问题三:幻读(Phantom Read)—— 前后查询的行数不一样
事务A 事务B
│ 查"年龄>18的用户"= 10 条 │
│ │ 插入一条年龄 20 的用户并提交
│ 再查"年龄>18的用户"= 11 条 │ ← 多出一条,像幻觉一样
不可重复读针对「某一行的值变了」,幻读针对「行数变了(多/少了行)」。
问题四:丢失更新(Lost Update)—— 两个人改,一个人的改动被覆盖
事务A 事务B
│ 读库存 = 10 │ 读库存 = 10
│ 卖出 1 个,算出 9 │ 卖出 1 个,算出 9
│ 写回 9 │
│ │ 写回 9 ← A 的扣减被覆盖了!
│ │
结果:卖了 2 个,库存却只减了 1
这是最常见、最坑的并发问题,也是「乐观锁/悲观锁」重点要解决的(见第 04 章)。
1.2 锁:用「排队」保证正确
解决这些问题的核心手段之一就是锁:当一个事务在操作某份数据时,先「锁住」它,别人想动就得等。
没有锁(乱): 有锁(排队):
A ─┐ A ─┐
B ─┼─▶ 同时改 → 乱 B ─┘ A 先锁住,改完释放
C ─┘ ▼
B 拿到锁,再改
▼
C 拿到锁,再改
代价显而易见:排队 = 变慢。所以数据库的锁设计,一直在「正确」和「性能」之间做权衡——这也是后面各种锁的由来。
1.3 锁和「事务隔离级别」的关系
你在《PostgreSQL》教程第六章学过隔离级别。它和锁是什么关系?
- 隔离级别 = 你想要「多干净」的并发效果(脏读/不可重复读/幻读能不能发生)
- 锁 / MVCC = 数据库为了达到这个隔离级别,底层用的手段
隔离级别(目标) 底层实现(手段)
───────────── ─────────────
Read Committed ←── MVCC + 必要时加锁
Repeatable Read ←── MVCC 快照
Serializable ←── 更严格的锁 / 冲突检测
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed(PG 默认) | 不会 | 可能 | 可能 |
| Repeatable Read | 不会 | 不会 | 不会* |
| Serializable | 不会 | 不会 | 不会 |
*PostgreSQL 的 Repeatable Read 比 SQL 标准更严格,也防住了幻读。
关键认知:你平时设置隔离级别,其实就是在间接决定数据库「怎么加锁、加多严」。锁是隔离级别背后的引擎。
1.4 一个重要区分:锁是数据库自动加的,也可以手动加
- 自动加锁:你执行
UPDATE、DELETE时,数据库会自动对涉及的行加锁,你无需操心。 - 手动加锁:某些场景(如「先查后改」要防丢失更新),你需要主动加锁,比如
SELECT ... FOR UPDATE。
后面章节会反复用到这两种情况,先有个印象。
本章小结
| 你要记住 | 要点 |
|---|---|
| 锁的动机 | 解决并发读写同一数据带来的问题 |
| 四个经典问题 | 脏读、不可重复读、幻读、丢失更新 |
| 锁的本质 | 用「排队等待」换「数据正确」,代价是变慢 |
| 与隔离级别 | 隔离级别是目标,锁/MVCC 是实现手段 |
| 两种加锁 | 数据库自动加 + 你手动加(FOR UPDATE) |
下一章预告:锁可以锁一整张表,也可以只锁几行。粒度不同,性能和并发天差地别。我们先讲「按粒度分」。
下一章 → 02 - 按粒度分:表锁与行锁