数据库基础体系 · 第 97/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
PostgreSQL 锁与 Serializable SSI:谓词冲突、死锁和咨询锁
一、先区分三类“并发控制”
讨论 PostgreSQL 的锁、Serializable SSI 和咨询锁时,最容易混淆的是:它们都与并发有关,但解决的问题并不相同。
1. MVCC:决定“读到什么”
PostgreSQL 使用 MVCC。普通 SELECT 通常不会等待正在修改数据的事务,而是根据当前事务的快照判断某个行版本是否可见。
例如:
BEGIN;
SELECT balance
FROM accounts
WHERE id = 1;
这个查询主要依赖:
- 当前事务的快照;
- 行版本的事务可见性信息;
xmin、xmax等元数据;- 已提交、未提交和事务快照的关系。
MVCC 解决的是:
当前事务应该看到哪个版本?
它并不自动保证多个查询组合起来具有串行化语义。
2. 锁:限制某些并发操作
锁解决的是:
某个资源当前是否允许另一个事务继续执行某种操作?
例如:
- 表锁协调
ALTER TABLE、VACUUM、INSERT等操作; - 行锁协调对同一行的更新和锁定;
- 咨询锁协调应用自定义的业务资源。
锁可能让事务等待,也可能导致死锁。
3. Serializable SSI:检测不安全的读写依赖
SERIALIZABLE 隔离级别的目标是:
所有成功提交的事务,其结果应等价于某种串行执行顺序。
PostgreSQL 通过 Serializable Snapshot Isolation,简称 SSI,实现这一保证。SSI 通常不会把所有读操作都变成阻塞锁,而是记录读写依赖;如果发现当前并发执行不可能等价于串行执行,就让某个事务失败,返回序列化失败。
因此:
- 普通锁主要通过等待来协调;
- SSI 主要通过检测冲突并回滚事务来协调;
- 咨询锁是应用自己定义的锁,不会自动改变 MVCC 或 SSI 的规则。
二、PostgreSQL 锁的基本模型
2.1 锁保护的不是“数据库”这么一个整体
PostgreSQL 中存在多个层次的锁资源,常见的包括:
- 数据库对象锁,例如表、索引、模式;
- 行锁,例如某一行被
UPDATE或SELECT FOR UPDATE锁定; - 事务 ID、虚拟事务 ID 相关的等待;
- 咨询锁;
- Serializable 使用的谓词锁。
表锁并不意味着整个表永远只能由一个事务访问。关键在于不同锁模式之间是否冲突。
例如,普通的 INSERT、UPDATE、DELETE 会取得表级的 RowExclusiveLock。这种锁与其他普通数据修改通常可以并存,但会与某些需要更强表级排他性的操作冲突,例如部分 DDL。
可以用下面的语句观察表级锁:
SELECT
pid,
relation::regclass AS relation_name,
mode,
granted,
waitstart
FROM pg_locks
WHERE relation IS NOT NULL
ORDER BY relation_name, pid, granted;
其中:
granted = true表示锁已经获得;granted = false表示正在等待;relation是内部对象标识,转换成regclass后通常可以显示表或索引名称。
2.2 行锁不是 MVCC 可见性
考虑两个事务同时更新同一行:
UPDATE accounts
SET balance = balance - 10
WHERE id = 1;
第一个事务已经修改但尚未提交时,第二个事务通常会等待第一个事务对该行的锁。第一个事务提交后,第二个事务会重新检查目标行是否仍然满足条件,并根据当前规则继续或放弃更新。
这个过程中的“等待”是锁行为,而不是普通 SELECT 的可见性判断。
另一方面:
SELECT balance
FROM accounts
WHERE id = 1;
普通读取一般不会因为另一个事务正在更新该行而等待。它可能读取旧版本,前提是该版本对当前快照可见。
因此,以下两句话不能互换:
- “我能读到这行”;
- “我锁住了这行”。
如果读取结果将决定后续更新,通常需要明确选择合适的隔离级别、行锁或业务约束。
三、谓词冲突:冲突的不是一行,而是一个条件
3.1 什么是谓词
谓词是查询条件。例如:
SELECT *
FROM orders
WHERE customer_id = 10
AND status = 'pending';
这里的谓词可以抽象为:
查询读取的不是某个预先指定的行,而是满足 P(row) 的所有行。
这会产生一个重要问题:事务读取时结果为空,但另一个事务随后插入一行,使这行满足同一个条件。
这就是常说的幻读场景。
3.2 一个空集读取的例子
假设表中当前没有 sku = 'A' 的商品。
事务 T1:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*)
FROM inventory
WHERE sku = 'A';
-- 返回 0
事务 T2:
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO inventory(sku, quantity)
VALUES ('A', 100);
COMMIT;
如果 T1 随后基于“没有 sku = 'A'”这一事实进行决策,那么 T2 的插入就可能破坏 T1 所依据的假设。
这里 T1 并没有读取某一行,因此不能只用“锁住已读行”解释这个冲突。冲突针对的是:
满足
sku = 'A'这一条件的集合,或者说这个条件对应的搜索空间。
3.3 PostgreSQL 的谓词锁是 SIREAD
在 Serializable 模式下,PostgreSQL 使用谓词锁记录读取过的范围或谓词相关资源。这类锁通常称为 SIREAD 锁。
它有几个关键特征:
-
SIREAD 通常不阻塞写入。
一个事务读取某个范围后,另一个事务仍然可以写入该范围。 -
写入会与已记录的读取产生谓词冲突。
SSI 根据这些冲突判断是否形成不安全的依赖结构。 -
它不是普通的排他锁。
不能把 SIREAD 理解成“这个范围被读锁锁住了,其他事务不能插入”。 -
它可以记录在不同粒度。
可能涉及元组、页面或关系。索引访问路径也会影响谓词锁的表示。
可以观察谓词锁:
SELECT *
FROM pg_predicate_locks;
也可以从 pg_locks 中查看 Serializable 相关锁:
SELECT *
FROM pg_locks
WHERE mode = 'SIReadLock';
具体看到的锁粒度和数量与查询计划、索引、事务状态以及谓词锁汇总有关。
3.4 为什么谓词锁可能扩大范围
数据库不一定为每一个逻辑谓词保存无限精细的区间信息。为了控制内存使用,谓词锁可能从更细的元组粒度汇总到页面或关系粒度。
例如,逻辑上事务只读取了少数行,但内部最终可能记录为某个数据页甚至整张表上的谓词锁。这样做会增加误报的可能性:
- 两个事务实际上访问的逻辑范围不重叠;
- 但由于锁粒度被提升,系统认为它们可能冲突;
- 最终某个 Serializable 事务收到序列化失败。
这不表示结果不正确。SSI 的目标是避免漏报不安全并发,必要时可以保守地中止事务。
相关配置参数包括:
max_pred_locks_per_transactionmax_pred_locks_per_relationmax_pred_locks_per_page
它们影响谓词锁的内存管理和汇总行为。调大参数不能消除逻辑上的冲突,只可能减少某些由锁粒度提升带来的额外冲突,同时会增加共享内存等资源压力。
四、SSI 如何从读写依赖推导出序列化失败
4.1 读写依赖的形式化表示
设事务 读取了某个对象或谓词范围,而事务 写入了可能影响该读取的内容。如果这种读取发生在 的写入之前,并且 的快照没有看到 的写入,则可形成一个读写依赖:
直觉是:
如果要把这两个事务排成串行顺序,T_i 必须排在 T_j 前面,否则 T_i 读到的结果无法解释。
这只是一个方向上的约束,并不等于立即发生错误。
4.2 危险结构
SSI 关注的核心结构是两个连续的读写依赖:
如果 和 在某个关键时间点仍然并发,那么这条依赖链可能形成无法满足的串行顺序,称为危险结构。
从序列化角度看:
- 第一条边要求 排在 前面;
- 第二条边要求 排在 前面;
- 如果另外的冲突或提交关系又要求 排在 前面;
- 就形成环,任何串行顺序都无法满足。
SSI 不需要等待所有事务完成后再检查,而是在事务运行和提交过程中维护这些依赖,并在必要时中止事务。
4.3 两事务写偏差
建立一个约束:至少有一名医生值班。初始状态如下:
| doctor | on_call |
|---|---|
| Alice | true |
| Bob | true |
事务 T1:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*)
FROM doctors
WHERE on_call = true;
-- 看到 2
UPDATE doctors
SET on_call = false
WHERE doctor = 'Alice';
COMMIT;
事务 T2 几乎同时执行:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*)
FROM doctors
WHERE on_call = true;
-- 也看到 2
UPDATE doctors
SET on_call = false
WHERE doctor = 'Bob';
COMMIT;
在普通快照隔离下,两个事务可能都看到对方尚未提交,于是都认为“另一名医生仍然值班”,最后得到:
| doctor | on_call |
|---|---|
| Alice | false |
| Bob | false |
约束被破坏。
SSI 可以观察到:
- T1 读取了 Bob 仍值班这一事实,T2 写入了 Bob;
- T2 读取了 Alice 仍值班这一事实,T1 写入了 Alice。
于是存在相互交叉的读写依赖:
这构成无法串行化的依赖环。系统会让其中一个事务失败,常见错误是:
ERROR: could not serialize access due to read/write dependencies among transactions
客户端通常应看到 SQLSTATE:
40001
需要注意,具体哪个事务失败取决于执行时序和 SSI 的状态,不应把某个事务固定视为“必然失败者”。
4.4 为什么 Serializable 事务必须重试
序列化失败不是普通 SQL 语法错误,也不是只重试失败的那一条语句就够了。
错误发生时,当前事务的快照和此前读取的结果已经不能继续用于保证串行化。正确的处理方式是:
- 回滚整个事务;
- 重新建立事务;
- 重新执行完整的业务逻辑;
- 必要时加入有限次数的退避重试。
伪代码如下:
for attempt in 1..N:
begin transaction at serializable
execute all reads and writes
try commit
if SQLSTATE = 40001:
rollback
sleep with backoff
continue
if success:
return
otherwise:
report error
应用必须确保事务内的副作用可以安全重试。比如已经发送到外部系统的消息、已经调用的支付接口,不能仅依赖数据库事务回滚来撤销。这属于跨系统一致性问题。
五、Serializable 不等于“所有读都等待”
这是一个常见误解。
在 PostgreSQL 中:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT *
FROM inventory
WHERE sku = 'A';
并不意味着:
- 其他事务不能更新
inventory; - 其他事务不能插入满足条件的行;
- 当前查询会像显式锁一样等待所有潜在写入。
更准确的理解是:
- 读取建立 SIREAD 谓词依赖;
- 写入与这些读取产生冲突记录;
- 如果冲突组合会破坏串行化,事务提交或执行期间可能失败。
因此,Serializable 常见的失败表现不是“永远阻塞”,而是:
could not serialize access due to read/write dependencies among transactions
而 SELECT FOR UPDATE 是另一种机制:
SELECT *
FROM inventory
WHERE sku = 'A'
FOR UPDATE;
它会锁定已经找到的行,但对于“当前没有任何行”的空集,不能简单地理解为锁住了未来所有可能插入的行。要保护一个不存在的谓词范围,应依赖 Serializable 的谓词冲突检测,或者使用明确的唯一约束、键表、应用锁等设计。
六、死锁:等待图中的环
6.1 死锁的必要条件
死锁不是“锁很多”这么简单。典型死锁需要形成循环等待:
其中箭头表示:
前一个事务正在等待后一个事务持有的资源。
例如:
事务 T1:
BEGIN;
UPDATE accounts
SET balance = balance - 10
WHERE id = 1;
-- 暂停,不提交
事务 T2:
BEGIN;
UPDATE accounts
SET balance = balance - 20
WHERE id = 2;
-- 暂停,不提交
然后 T1 再执行:
UPDATE accounts
SET balance = balance + 10
WHERE id = 2;
T1 等待 T2 释放 id = 2 的行锁。
此时 T2 执行:
UPDATE accounts
SET balance = balance + 20
WHERE id = 1;
T2 等待 T1 释放 id = 1 的行锁,于是形成:
T1 waits for T2
T2 waits for T1
PostgreSQL 的死锁检测器会发现这个等待图中的环,并中止其中一个事务,通常返回:
ERROR: deadlock detected
对应 SQLSTATE:
40P01
6.2 为什么统一加锁顺序有效
如果所有事务都按递增的账户 ID 获取资源:
UPDATE accounts ... WHERE id = 1;
UPDATE accounts ... WHERE id = 2;
那么不应出现一个事务先拿 1 再等 2,另一个事务先拿 2 再等 1 的结构。
这并不是“锁越少越好”,而是:
对同一组资源建立全局一致的加锁顺序,破坏循环等待条件。
应用代码中应特别注意:
- 批量更新是否按稳定顺序处理;
- 多张表的访问顺序是否一致;
- 行锁和咨询锁的顺序是否一致;
- 是否在事务中调用可能反过来访问数据库的函数或服务。
6.3 死锁与锁等待超时不同
下面几个错误需要区分:
| SQLSTATE | 含义 |
|---|---|
40P01 |
检测到死锁 |
55P03 |
lock_timeout 到期 |
40001 |
序列化失败,或其他可重试的序列化异常 |
lock_timeout 只表示等待某个锁超过限制,不一定存在环。
deadlock_timeout 则是 PostgreSQL 在等待一段时间后检查死锁的触发阈值。它不是死锁的最大允许时间,也不是“锁最多只能持有这么久”。检查过于频繁会增加开销,设置过大则会让真正的死锁错误延迟更久。
可以在会话中设置:
SET lock_timeout = '3s';
SET deadlock_timeout = '1s';
生产环境是否调整这些参数,要结合事务时长、正常锁等待和故障恢复策略验证,不能用极小的超时掩盖设计上的锁顺序问题。
七、如何诊断线上锁等待
7.1 找出等待者和阻塞者
可以使用 pg_stat_activity 与 pg_blocking_pids:
SELECT
w.pid AS waiting_pid,
w.usename AS waiting_user,
w.query AS waiting_query,
w.wait_event_type,
w.wait_event,
b.pid AS blocking_pid,
b.usename AS blocking_user,
b.query AS blocking_query,
b.state AS blocking_state
FROM pg_stat_activity AS w
LEFT JOIN LATERAL unnest(pg_blocking_pids(w.pid)) AS x(pid)
ON true
LEFT JOIN pg_stat_activity AS b
ON b.pid = x.pid
WHERE w.wait_event_type = 'Lock';
解释:
waiting_pid是正在等待的后端进程;blocking_pid是当前阻塞它的进程;wait_event_type = 'Lock'表示正在等待锁类事件;- 阻塞者可能处于
idle in transaction,这通常意味着事务开启后没有及时提交或回滚。
再结合 pg_locks:
SELECT
pid,
locktype,
relation::regclass AS relation_name,
page,
tuple,
virtualxid,
transactionid,
mode,
granted,
waitstart
FROM pg_locks
WHERE pid IN (
SELECT pid
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
)
ORDER BY pid, granted, locktype;
pg_locks 中:
granted = false是等待中的锁请求;granted = true是已经持有的锁;relation、page、tuple是否有值取决于锁类型;- 不应只看表级锁就断定是“整张表被锁住”,因为阻塞也可能来自行锁、事务 ID 等资源。
7.2 死锁日志
如果发生死锁,服务器日志通常会记录:
- 哪个进程等待哪个锁;
- 哪个进程持有什么锁;
- 参与死锁的 SQL;
- 最终被回滚的事务。
诊断时应把日志中的 PID、事务时间线和应用请求 ID 对起来。仅看到“deadlock detected”而不保留相关 SQL,很难判断真正的资源访问顺序。
八、咨询锁:应用定义的互斥协议
8.1 咨询锁是什么
咨询锁,也叫 advisory lock,是 PostgreSQL 提供给应用使用的锁命名空间。
数据库本身不知道数字 42 代表:
- 某个用户;
- 某个订单;
- 某个租户;
- 某个定时任务;
- 还是一批文件。
应用自行约定一个键,并使用这个键协调并发任务。
它不直接绑定:
- 表;
- 行;
- 索引;
- MVCC 行版本;
- 外部系统资源。
例如,使用事务级咨询锁保护同一租户的业务操作:
BEGIN;
SELECT pg_advisory_xact_lock(1001);
-- 执行 tenant_id = 1001 的一组业务操作
COMMIT;
另一个事务也请求键 1001 时,会等待该事务结束。
8.2 事务级与会话级咨询锁
常见函数有两组。
事务级:
SELECT pg_advisory_xact_lock(1001);
SELECT pg_try_advisory_xact_lock(1001);
会话级:
SELECT pg_advisory_lock(1001);
SELECT pg_try_advisory_lock(1001);
SELECT pg_advisory_unlock(1001);
区别如下:
| 类型 | 释放时机 | 回滚是否释放 |
|---|---|---|
| 事务级 | 事务结束时自动释放 | 回滚会释放 |
| 会话级 | 显式解锁或数据库会话结束 | 回滚不会释放 |
pg_try_* 不等待,立即返回布尔值。例如:
BEGIN;
SELECT pg_try_advisory_xact_lock(1001) AS acquired;
如果返回 false,当前事务没有获得锁,可以选择跳过、排队到应用层,或返回“资源忙”。
事务级咨询锁一般更适合保护数据库事务内的业务操作,因为事务提交或回滚时会自动释放。会话级咨询锁则更容易受到连接池影响:应用逻辑结束了,但连接没有断开,锁仍然存在。
8.3 咨询锁不会自动保护行
下面两个操作之间没有自动关系:
SELECT pg_advisory_xact_lock(1001);
以及:
UPDATE tenants
SET ...
WHERE tenant_id = 1001;
只有遵守同一协议的代码才会被咨询锁协调。另一个没有获取咨询锁、直接更新表的事务仍然可以执行。
因此咨询锁适合:
- 数据库中没有天然行可锁定的业务资源;
- 需要协调定时任务或同一租户的串行处理;
- 需要把多个表或多个步骤视为一个应用级资源。
它不适合替代:
- 唯一约束;
- 外键;
- 行锁;
- 数据库的实际一致性约束。
如果资源本来就是一行,通常更直接的方案是使用唯一约束或:
SELECT *
FROM job
WHERE id = 1
FOR UPDATE;
8.4 咨询锁也可能死锁
咨询锁并不是死锁免疫的。事务 T1 和 T2 可以这样形成循环:
T1 持有 advisory key 1,等待 advisory key 2
T2 持有 advisory key 2,等待 advisory key 1
更复杂的情况是混合资源:
T1 持有行锁,等待咨询锁
T2 持有咨询锁,等待行锁
因此应用必须把咨询锁纳入整体等待图,统一考虑:
- 咨询锁之间的顺序;
- 咨询锁与行锁之间的顺序;
- 咨询锁与表锁之间的顺序。
咨询锁可以在 pg_locks 中观察:
SELECT
pid,
locktype,
classid,
objid,
objsubid,
mode,
granted,
waitstart
FROM pg_locks
WHERE locktype = 'advisory';
8.5 咨询锁键的设计
咨询锁有单个 bigint 键,也有两个 integer 组成的键。应用应明确命名空间,避免不同模块意外使用同一个数字。
例如可以约定:
高位:资源类型
低位:资源 ID
或者使用两个整数:
SELECT pg_advisory_xact_lock(7, 1001);
这里 7 可以代表资源类型,1001 代表资源 ID。
如果应用使用字符串哈希成整数,要考虑哈希碰撞。碰撞不会破坏数据库内部的锁语义,但会把两个本不相关的业务资源错误地串行化。
九、谓词冲突、普通锁和咨询锁的关系
下面这个对比有助于建立边界:
| 机制 | 保护对象 | 是否通常阻塞 | 是否由 MVCC 自动产生 | 失败方式 |
|---|---|---|---|---|
| 普通表锁 | 数据库对象 | 可能阻塞 | 部分数据库操作自动使用 | 锁等待、死锁、超时 |
| 行锁 | 已定位的行 | 可能阻塞 | UPDATE 等操作会使用 |
锁等待、死锁、超时 |
| SIREAD 谓词锁 | 读取过的谓词范围 | 通常不阻塞写入 | Serializable 事务使用 | 序列化失败 |
| 咨询锁 | 应用定义的键 | 取决于函数 | 不自动产生 | 锁等待、死锁、超时 |
特别要注意:
- SIREAD 不是应用可以随意获取和释放的普通咨询锁;
- 咨询锁不会让普通
SELECT变成可串行化读取; - 行锁只保护已找到的行,不天然保护一个空谓词范围;
- Serializable 也不会消除普通锁之间的死锁。
一个 Serializable 事务完全可能同时经历:
- 因为
UPDATE等待行锁; - 因为查询建立 SIREAD 依赖;
- 因为冲突结构最终收到
40001; - 或者因为和另一个事务的锁顺序相反而收到
40P01。
这些是不同的事件,应用处理策略也应分别记录和重试。
十、生产设计中的几个边界
10.1 数据库约束优先于“先查再决定”
如果业务规则可以表达为唯一约束、外键或排他约束,应优先使用数据库约束。
例如,不要仅依赖:
SELECT count(*)
FROM user_roles
WHERE user_id = 1
AND role = 'owner';
然后在应用层判断是否可以插入。并发事务都可能看到“没有”,最后产生重复数据。
更可靠的方案通常是:
CREATE UNIQUE INDEX user_one_owner
ON user_roles(user_id)
WHERE role = 'owner';
约束负责保证事实,SSI 或咨询锁负责协调更复杂的事务流程。
10.2 使用 Serializable 仍需要处理重试
Serializable 的语义保证只针对成功提交的事务。它不保证每个事务都一定成功,也不保证没有等待。
应用应区分:
40001:重新执行整个事务;40P01:修正锁顺序,并可对整个事务重试;55P03:判断是正常高峰等待还是异常阻塞;- 其他错误:不能一概当作可安全重试。
重试事务时,随机数、当前时间、外部调用和消息发送等副作用必须重新设计,否则数据库层面虽然重试成功,业务层面可能重复执行。
10.3 长事务会放大这些问题
长事务会:
- 持有普通锁更久;
- 让其他事务等待更久;
- 保留更早的 MVCC 快照;
- 增加 SSI 需要维护的依赖;
- 延迟旧版本回收和 Vacuum 的效果。
因此锁诊断不能只看单条 SQL。需要同时关注:
SELECT
pid,
state,
xact_start,
query_start,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
idle in transaction 但长期不结束的会话,尤其容易成为锁等待和旧版本保留的根源。
十一、一个完整的测试方式
下面的测试需要同一个 PostgreSQL 数据库中的两个 psql 会话。先初始化数据:
DROP TABLE IF EXISTS doctors;
CREATE TABLE doctors (
doctor text PRIMARY KEY,
on_call boolean NOT NULL
);
INSERT INTO doctors(doctor, on_call)
VALUES
('Alice', true),
('Bob', true);
会话 A
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*)
FROM doctors
WHERE on_call = true;
-- 结果为 2
UPDATE doctors
SET on_call = false
WHERE doctor = 'Alice';
会话 B
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*)
FROM doctors
WHERE on_call = true;
-- 结果也可能为 2
UPDATE doctors
SET on_call = false
WHERE doctor = 'Bob';
随后分别提交:
COMMIT;
其中一个事务可能成功,另一个事务可能收到 40001。如果两个事务都成功提交,应检查实际执行时序和事务边界;在标准的完整写偏差场景中,Serializable 必须阻止最终不可串行化的结果,而不是保证两个事务都成功。
若改成普通默认隔离级别,并且没有其他约束或显式协调,则两个事务都成功、两名医生都离岗的风险会出现。
十二、最终的判断框架
遇到并发问题时,可以依次询问:
- 这是“读到了哪个版本”的问题,还是“谁必须等待谁”的问题?
- 冲突针对的是已经存在的行,还是一个查询谓词覆盖的范围?
- 当前使用的是普通锁、SIREAD 谓词依赖,还是咨询锁?
- 事务是在等待,还是已经因为
40001被中止? - 如果是等待,等待图中是否存在环?
- 如果是咨询锁,所有访问同一业务资源的代码是否遵守同一协议?
- 如果事务失败,应用是否回滚并重试了完整事务?
- 业务规则是否可以由唯一约束等数据库约束直接表达?
理解这些边界后,PostgreSQL 的“锁住了”“读到了”“可串行化了”和“业务资源被协调了”就不再是同一个概念:普通锁控制等待,MVCC控制可见性,SSI 检测谓词级读写依赖,咨询锁则把应用自定义的资源纳入数据库的等待与死锁体系。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:PostgreSQL WAL 与 Checkpoint:提交、崩溃恢复、归档和写入性能
- 下一篇:PostgreSQL 分区表:规划、裁剪、索引、维护和在线迁移
- 延伸:PostgreSQL MVCC 与 Vacuum:Tuple 可见性、膨胀、冻结和诊断
- 延伸:MVCC、锁与死锁:可见性、锁粒度、等待图和线上诊断
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论