PostgreSQL 锁机制和死锁如何理解?如何排查锁等待?
简化版
PostgreSQL 有表级锁、行级锁和事务相关锁。普通更新会加行锁,DDL 往往需要更强的表锁;死锁通常来自多个事务以不同顺序持有资源。排查时看 pg_stat_activity、pg_locks 和阻塞链,治理重点是缩短事务、统一加锁顺序、避免长事务和高峰 DDL。
详细版
锁的目标是保证并发正确性。PostgreSQL 的 MVCC 让读写很多时候互不阻塞,但写写冲突、DDL、外键检查、显式锁仍会产生等待。
排查思路:
- 查看当前活动 SQL 和等待事件。
- 用
pg_locks找锁类型和持有者。 - 识别阻塞者和被阻塞者。
- 优先处理长事务和空闲事务。
- 对死锁,数据库会自动检测并中止其中一个事务。
治理方法包括控制事务长度、固定资源访问顺序、为外键列建索引、把 DDL 放低峰期执行。
完整版教学
一、MVCC 减少读写阻塞,但不等于没有锁
PostgreSQL 通过 MVCC 保存行版本,让普通 select 通常不阻塞 update,update 也不阻塞历史快照读取。这是它并发能力的重要来源。
但这不代表 PostgreSQL 没有锁。两个事务同时修改同一行,仍然会产生行级锁等待;DDL 修改表结构时,也可能等待表上其他事务结束。
事务 A: update account set balance = balance - 100 where id = 1;
事务 B: update account set balance = balance + 50 where id = 1;
事务 B 必须等待事务 A 提交或回滚
记忆钩子:MVCC 主要减少读写冲突,写写冲突和结构变更仍然靠锁协调。
二、表级锁常见于 DDL 和显式 LOCK
PostgreSQL 有多种表级锁模式,不同 SQL 会申请不同强度的锁。普通查询也会有轻量的访问锁,DDL 通常需要更强的锁。
比如 ALTER TABLE 可能需要阻塞写入甚至读取,具体取决于操作类型。生产环境在大表上执行 DDL 前,要评估锁级别和执行时间。
| 操作 | 常见风险 |
|---|---|
select | 通常不阻塞写 |
update/delete | 锁定被修改行 |
alter table | 可能等待或阻塞业务 SQL |
create index | 普通创建可能阻塞写,需考虑 concurrently |
面试时不用死背所有锁模式,但要知道 DDL 的锁风险比普通 DML 大。
三、行级锁来自对同一行的修改
行级锁常见于 update、delete、select for update。如果多个事务按不同顺序修改多行,就容易出现等待甚至死锁。
事务 A: 锁住订单 1 -> 等订单 2
事务 B: 锁住订单 2 -> 等订单 1
形成死锁
数据库会检测死锁,并中止其中一个事务,让另一个继续。应用需要捕获错误并决定是否重试。
四、死锁的本质是资源顺序不一致
死锁不是 PostgreSQL 独有问题。它一般满足互斥、持有并等待、不可抢占、循环等待。工程上最常见的原因是事务访问资源顺序不一致。
解决办法是统一加锁顺序。比如批量更新账户转账时,总是先锁小 ID,再锁大 ID。
错误:A 转 B 锁 A 再锁 B;B 转 A 锁 B 再锁 A
正确:无论谁转谁,都先锁 min(account_id),再锁 max(account_id)
这个原则比单纯调参数更重要。
五、pg_stat_activity 和 pg_locks 是排查入口
锁等待排查通常从活动视图开始,看谁在等、等了多久、SQL 是什么。
select pid, state, wait_event_type, wait_event, query
from pg_stat_activity
where wait_event_type is not null;
进一步可以结合 pg_locks 查看锁对象和持有情况。很多线上排障脚本会构造阻塞链,把 blocker 和 waiter 列出来。
如果发现阻塞者是 idle in transaction,通常说明应用开启事务后没有及时提交或回滚,这是高优先级问题。
六、外键和索引也会影响锁等待
外键检查可能带来额外锁和查询。如果子表外键列没有索引,父表删除或更新时,数据库可能需要扫描子表检查引用关系,导致锁等待时间变长。
比如:
orders.user_id -> users.id
如果 orders.user_id 没索引,删除 users 某行时检查成本很高
所以有外键的列通常要建索引,尤其是子表数据量大时。即使不用数据库外键,应用层关联字段也要考虑查询和删除成本。
七、常见误区与追问
- 误区:PostgreSQL 有 MVCC,所以不会锁等待。 MVCC 减少读写阻塞,但写写冲突、DDL 和显式锁仍会等待。
- 误区:死锁只能靠数据库参数解决。 关键是统一资源访问顺序、缩短事务和减少锁持有时间。
- 误区:锁等待时只看慢 SQL。 阻塞者可能已经空闲在事务中,要看阻塞链。
- 追问:怎么查谁阻塞了谁? 结合
pg_stat_activity、pg_locks或pg_blocking_pids(pid)。 - 追问:DDL 为什么危险? 大表 DDL 可能申请强锁,等待期间会阻塞业务读写或被业务事务阻塞。
- 追问:死锁后应用要做什么? 捕获死锁错误,对幂等操作做有限重试,并记录告警。
八、面试中可以这样落地
排查锁等待可以先执行:
select pid,
pg_blocking_pids(pid) as blockers,
wait_event_type,
wait_event,
query
from pg_stat_activity
where cardinality(pg_blocking_pids(pid)) > 0;
然后定位 blocker 的 SQL、事务开始时间和状态。如果是长事务,优先让业务提交、回滚或人工终止;如果是 DDL 锁等待,就调整执行窗口或使用更低锁风险的方式。
九、加强记忆
PostgreSQL 锁题记住“MVCC 不等于无锁,写写冲突会等,DDL 风险更大,死锁来自顺序不一致”。排查用 pg_stat_activity、pg_locks、pg_blocking_pids,治理靠短事务、统一加锁顺序、低峰 DDL 和必要索引。