PostgreSQL 锁
内容:
- 锁的种类(行锁 / 表锁 / 事务锁 / 元数据锁)
- 锁是如何产生的
- 业务中常见的锁问题
- 如何排查
- 如何避免阻塞
- 最佳实践
- PostgreSQL 17 版本适用
非常适合 OLTP、高并发、业务系统开发与运维。
PostgreSQL 锁分四大类:
| 类型 | 示例 | 说明 |
|---|---|---|
| 行级锁 Row-Level Locks | SELECT … FOR UPDATE | 最常用于业务,影响单行 |
| 表级锁 Table-Level Locks | LOCK TABLE … | DDL、并发写入时出现 |
| 事务锁 Transaction Locks | 对象版本检查 | MVCC 实现 |
| 元数据锁 Catalog Locks | 修改表结构时 | 不阻塞普通查询 |
行锁不会阻塞读(PostgreSQL 最大优势之一)。
PostgreSQL MVCC + 行锁 = 高并发强一致性。
锁定行以便更新(常用于扣库存、修改金额)
SELECT * FROM orders WHERE id = 100 FOR UPDATE;
其他事务:
- ❌ 不能更新此行
- ❌ 不能删除此行
- ✅ 可以读(但读到旧版本)
比 FOR UPDATE 的冲突范围更小。
共享锁,只允许读取,用于强一致查询。
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
保护外键约束(防止父表被删)。
SELECT * FROM jobs
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1;
特点:
- 不等待锁
- 忽略被锁的行
- 超适合 任务队列消费者、订单处理、抢单系统
最常出现导致阻塞的锁类型。
表锁等级(由强到弱)
| 锁模式 | 常用语句 | 影响 |
|---|---|---|
| ACCESS EXCLUSIVE | ALTER TABLE、DROP | 阻塞所有读写 |
| EXCLUSIVE | 罕见 | 阻塞写 |
| SHARE ROW EXCLUSIVE | CREATE INDEX CONCURRENTLY | 阻塞写,允许读 |
| ROW SHARE / ROW EXCLUSIVE | 普通 UPDATE/DELETE | 允许并发读 |
例:ALTER TABLE 会阻塞所有操作:
ALTER TABLE users ADD COLUMN age INT;
执行时:
- 所有写阻塞
- 所有读阻塞
- 事务积压风险(Deadlock Storm)
解决方法:使用 LOCK TIMEOUT
SET lock_timeout = '5s';
ALTER TABLE users ADD COLUMN age INT;
PostgreSQL 自动检测死锁,并回滚其中一个事务。
死锁典型示例:
事务 A:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
事务 B:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2;
A 接着:
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
B 接着:
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
👉 互锁,PostgreSQL 会自动中止其中一个
业务最佳实践避免死锁:
- 按统一顺序访问行(非常重要)
- 事务尽量短
- 避免在事务中执行慢查询或外部请求
- 加锁顺序统一:比如 always lock smaller ID first
查看锁:
SELECT * FROM pg_locks;
查看锁 + 对应 SQL:
SELECT
a.pid,
a.state,
a.query,
l.locktype,
l.mode,
l.granted
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
ORDER BY a.pid;
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.query AS blocked_query,
blocking_locks.pid AS blocking_pid,
blocking_activity.query AS blocking_query
FROM pg_locks blocked_locks
JOIN pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_locks blocking_locks ON
blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
JOIN pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
SELECT pg_terminate_backend(12345);
| 场景 | 推荐用法 | 说明 |
|---|---|---|
| 抢订单、抢红包 | FOR UPDATE SKIP LOCKED | 并发高时最稳 |
| 扣库存 | FOR UPDATE | 强一致 |
| 消费队列 | FOR UPDATE SKIP LOCKED | 常用 |
| 获取某行最新状态 | FOR SHARE | 阻止被别人更新 |
| 审核流程 | FOR NO KEY UPDATE | 不需要锁主键 |
| 外键保护 | FOR KEY SHARE | 提高并发 |
| 并发插入 | upsert(ON CONFLICT) | 避免唯一键冲突 |
| 批量更新 | 分批 LIMIT + SKIP LOCKED | 防止长锁 |
不要在事务里做:
- API 请求
- 网络请求
- 慢查询
- 人工等待
例如:
先锁小 ID,再锁大 ID
UPDATE users SET status = 1 WHERE email = 'x';
如果 email 没索引
👉 锁表扫描期间会堵住所有写操作!
OLTP 业务仍然可能锁冲突。