PostgreSQL 关系级锁 --- 总结与优化指南
一、核心概念速览
| 概念 | 说明 |
|---|---|
| 锁的本质 | 共享内存中的一个状态块,标识资源是否被占用 |
| 长锁 | 持续到事务结束,用于保护关系/行等数据库对象 |
| 短锁 | 持续几个 CPU 指令,用于保护内存数据结构 |
| 重锁 | 长锁的一种,位于共享内存,可通过 pg_locks 查看 |
二、重锁类型汇总
| locktype | 说明 |
|---|---|
virtualxid |
每个事务持有自身虚拟 ID 的独占锁 |
transactionid |
事务获取真实 XID 后额外持有的独占锁 |
relation |
关系级锁(表、索引等) |
tuple |
元组级锁 |
object |
非关系对象锁 |
extend |
表扩展锁 |
page |
部分索引类型使用的页级锁 |
advisory |
用户手动管理的咨询锁 |
三、关系级锁的 8 种模式(由弱到强)
| 模式 | 典型命令 | 说明 |
|---|---|---|
AccessShareLock |
SELECT |
最弱,仅与 AccessExclusive 冲突 |
RowShareLock |
SELECT FOR UPDATE/SHARE |
行共享 |
RowExclusiveLock |
INSERT / UPDATE / DELETE |
行独占,允许并发堆修改 |
ShareUpdateExclusiveLock |
VACUUM / CREATE INDEX CONCURRENTLY |
允许并发读写,阻止 DDL |
ShareLock |
CREATE INDEX |
允许并发读,阻止写 |
ShareRowExclusiveLock |
部分 ALTER TABLE |
阻止并发写和 ShareLock |
ExclusiveLock |
部分 ALTER TABLE |
仅允许 AccessShare 并发 |
AccessExclusiveLock |
DROP / TRUNCATE / VACUUM FULL / LOCK TABLE |
最强,与所有模式冲突 |
关键规律:前 4 种模式允许并发堆修改;后 4 种模式不允许。
四、等待队列机制
- 等待队列是公平的 FIFO 队列。
- 一旦队列中存在高强度锁(如
AccessExclusiveLock),后续所有请求(包括兼容的SELECT)都必须排队等待。 - 事务结束(提交或回滚)后,其持有的所有锁立即释放,队列中第一个进程被唤醒。
示例阻塞链:
UPDATE (RowExclusiveLock, 持有)
└─ CREATE INDEX (ShareLock, 等待 UPDATE)
└─ VACUUM FULL (AccessExclusiveLock, 等待 UPDATE + CREATE INDEX)
└─ SELECT (AccessShareLock, 等待 VACUUM FULL)
五、常用监控 SQL
查看当前所有锁
sql
SELECT pid, locktype, relation::regclass, mode, granted
FROM pg_locks
ORDER BY pid;
查看阻塞关系
sql
SELECT pid,
pg_blocking_pids(pid) AS blocked_by,
wait_event_type,
state,
left(query, 60) AS query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
查看锁等待链(依赖关系)
sql
SELECT blocked.pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));
六、优化建议
1. 缩短事务持锁时间
-
避免在事务中执行耗时操作(如外部 HTTP 调用、大量计算)。
-
尽量将非数据库操作移到事务外部。
-
使用
idle in transaction超时参数防止长时间持锁:sqlSET idle_in_transaction_session_timeout = '30s';
2. 优先使用非阻塞 DDL
- 用
CREATE INDEX CONCURRENTLY替代CREATE INDEX,避免长时间 ShareLock 阻塞写操作。 - 用
ALTER TABLE ... ADD COLUMN时,新列默认值为NULL或 volatile 函数时不会重写表(PG 11+)。 - 避免在业务高峰期执行
VACUUM FULL/TRUNCATE/DROP TABLE,这些操作需要AccessExclusiveLock。
3. 合理控制锁的数量
max_locks_per_transaction × max_connections决定锁池上限,超出会报错。- 批量操作大量表时(如分区表),注意锁数量,必要时调大
max_locks_per_transaction(需重启)。
4. 死锁预防
-
多个事务操作同一组资源时,保持一致的加锁顺序。
-
使用
SELECT ... FOR UPDATE时,尽量缩小锁定范围(加WHERE条件)。 -
设置合理的死锁检测超时(默认 1s):
sqlSET deadlock_timeout = '500ms'; -
死锁发生时,PostgreSQL 自动中止代价较小的事务,应用层需处理重试逻辑。
5. 使用咨询锁替代应用层分布式锁
-
对于业务层面的互斥需求,可使用 PostgreSQL 咨询锁,避免引入外部锁服务:
sql-- 获取会话级咨询锁 SELECT pg_advisory_lock(12345); -- 释放 SELECT pg_advisory_unlock(12345); -- 事务级(自动释放) SELECT pg_advisory_xact_lock(12345);
6. 监控与告警
- 定期检查
pg_stat_activity中wait_event_type = 'Lock'的会话。 - 对
idle in transaction状态超过阈值的连接设置自动终止。 - 结合
pg_blocking_pids()建立锁等待告警,及时发现阻塞链。
七、参数参考
| 参数 | 默认值 | 说明 |
|---|---|---|
max_locks_per_transaction |
64 | 每事务最大锁数(影响锁池大小,需重启) |
deadlock_timeout |
1s | 死锁检测等待时间 |
lock_timeout |
0(不限) | 等锁超时,超时后报错 |
idle_in_transaction_session_timeout |
0(不限) | 空闲事务超时自动断开 |
statement_timeout |
0(不限) | 单条语句执行超时 |
八、总结
PostgreSQL 的锁机制设计精细,通过多种锁模式和公平等待队列在并发性与数据一致性之间取得平衡。实际生产中的锁问题大多源于:
- 长事务持锁 → 设置超时参数,优化事务边界
- 高强度 DDL 阻塞业务 → 使用 CONCURRENTLY 变体,选择低峰期执行
- 死锁 → 统一加锁顺序,应用层重试
- 锁池耗尽 → 调整
max_locks_per_transaction,避免单事务操作过多对象
理解锁的兼容矩阵和等待队列行为,是排查和预防 PostgreSQL 性能问题的基础。