首页 / 知识库 / python服务端进阶 / 数据库锁详解

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 - 乐观锁与悲观锁