03 - 按模式分:共享锁与排他锁
上一章讲了锁的「粒度」(锁多大范围)。这一章讲第二个维度:模式——这把锁是「读锁」还是「写锁」,以及它们之间能不能共存。
一句话先记住:读可以和读共存(大家一起看没问题),但写必须独占(改的时候别人不能碰)。
3.1 两种基本锁模式
| 锁模式 | 别名 | 用途 | 一句话 |
|---|---|---|---|
| 共享锁(Shared / S 锁) | 读锁 | 读数据时用 | 「我在读,你也能读,但都别改」 |
| 排他锁(Exclusive / X 锁) | 写锁 | 改数据时用 | 「我在改,谁都别碰」 |
3.2 核心规则:锁的兼容性
两把锁能不能同时加在同一份数据上,看这张兼容矩阵——这是本章最重要的内容:
请求 共享锁(读) 请求 排他锁(写)
持有 共享锁(读) ✅ 兼容 ❌ 冲突(要等)
持有 排他锁(写) ❌ 冲突(要等) ❌ 冲突(要等)
翻译成人话:
读 + 读 → ✅ 可以同时(大家一起看数据,谁也不改,没问题)
读 + 写 → ❌ 不行(有人在读,你想改?等他读完)
写 + 写 → ❌ 不行(俩人都想改?排队来)
写 + 读 → ❌ 不行(有人在改,你想读?等他改完)
一句话记忆:读读相容,读写互斥,写写互斥。 只有「读+读」能并行,只要沾上「写」,就得排队。
3.3 为什么这样设计
读 + 读 可以共存:
两个人同时看同一个数据,不会互相影响,数据也不会变
→ 没必要互相阻塞,放行!
一旦有写:
数据要变了。如果这时允许别人读,就可能读到「改了一半」的脏数据;
如果允许别人也写,两个写就会互相覆盖(丢失更新)
→ 必须独占,其他人等着
这套规则,正是为了防住第 01 章讲的「脏读」「丢失更新」等问题。
3.4 在 PostgreSQL 里怎么用
PostgreSQL 的行级锁提供了对应的手动加锁语法(都在事务里用):
-- 排他锁(写锁):读出来就锁住,准备改它,别人不能改也不能加锁
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- ... 计算、修改 ...
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 共享锁(读锁):锁住这行,别人能读,但不能改它
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- 我要基于这行做判断,期间不许别人改它,但允许别人也读
COMMIT;
| 语法 | 加什么锁 | 效果 |
|---|---|---|
SELECT ... FOR UPDATE | 行级排他锁 | 别人不能改、不能加锁,直到你提交 |
SELECT ... FOR SHARE | 行级共享锁 | 别人能读,但不能改这行 |
重点:PostgreSQL 里普通的
SELECT不加任何行锁(靠 MVCC 读快照)。只有你显式写FOR UPDATE/FOR SHARE,才会加行锁。这跟「读一定要加读锁」的传统认知不同。
3.5 意向锁(了解即可)
在支持「表锁 + 行锁」并存的数据库里(如 MySQL InnoDB),还有一种意向锁,作用是当「协调员」:
问题:事务A 锁了表里某一行(行锁)。
这时事务B 想锁「整张表」(表锁),它怎么快速知道「表里有没有行被锁着」?
难道要一行行去检查吗?太慢了。
解决:事务A 加行锁前,先在「表」上打一个"意向锁"标记,
表示"我在这表里锁了某些行"。
事务B 想加表锁时,一看表上有意向锁,立刻就知道「有人锁了行」,不用逐行检查。
意向锁是数据库自动管理的,你不用手动操作,理解它的「协调」作用即可。
PostgreSQL 的锁体系与 InnoDB 不同,意向锁更多是 InnoDB 的概念。这里作为通用知识了解。
本章小结
| 你要记住 | 要点 |
|---|---|
| 两种模式 | 共享锁(读锁)、排他锁(写锁) |
| 核心规则 | 读读相容,读写互斥,写写互斥 |
| 设计原因 | 一旦有写就可能脏读/覆盖,必须独占 |
| PG 语法 | FOR UPDATE(排他)、FOR SHARE(共享) |
| PG 特点 | 普通 SELECT 不加锁,要显式声明才加行锁 |
下一章预告:前两章讲的都是数据库真的「加锁排队」(悲观锁)。但还有一种「不加锁、靠版本号检测冲突」的思路——乐观锁。这是实战中极其重要的一对概念。
上一章 ← 02 - 按粒度分:表锁与行锁 | 下一章 → 04 - 乐观锁与悲观锁