Skip to main content
Documents
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

PostgreSQL 锁

内容:

  • 锁的种类(行锁 / 表锁 / 事务锁 / 元数据锁)
  • 锁是如何产生的
  • 业务中常见的锁问题
  • 如何排查
  • 如何避免阻塞
  • 最佳实践
  • PostgreSQL 17 版本适用

非常适合 OLTP、高并发、业务系统开发与运维。

1. PostgreSQL 锁的分类总览(核心知识)

PostgreSQL 锁分四大类:

类型 示例 说明
行级锁 Row-Level Locks SELECT … FOR UPDATE 最常用于业务,影响单行
表级锁 Table-Level Locks LOCK TABLE … DDL、并发写入时出现
事务锁 Transaction Locks 对象版本检查 MVCC 实现
元数据锁 Catalog Locks 修改表结构时 不阻塞普通查询

行锁不会阻塞读(PostgreSQL 最大优势之一)。

2. PostgreSQL 行级锁(业务中使用最多)

PostgreSQL MVCC + 行锁 = 高并发强一致性。

2.1 FOR UPDATE

锁定行以便更新(常用于扣库存、修改金额)

SELECT * FROM orders WHERE id = 100 FOR UPDATE;

其他事务:

  • ❌ 不能更新此行
  • ❌ 不能删除此行
  • ✅ 可以读(但读到旧版本)

2.2 FOR NO KEY UPDATE

比 FOR UPDATE 的冲突范围更小。

2.3 FOR SHARE

共享锁,只允许读取,用于强一致查询。

SELECT * FROM accounts WHERE id = 1 FOR SHARE;

2.4 FOR KEY SHARE

保护外键约束(防止父表被删)。

2.5 SKIP LOCKED(高并发队列神器)

SELECT * FROM jobs
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1;

特点:

  • 不等待锁
  • 忽略被锁的行
  • 超适合 任务队列消费者订单处理抢单系统

3. 表级锁(DDL 或批量修改触发)

最常出现导致阻塞的锁类型。

表锁等级(由强到弱)

锁模式 常用语句 影响
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;

4. 死锁 Deadlock(业务最常见问题)

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 会自动中止其中一个

业务最佳实践避免死锁:

  1. 按统一顺序访问行(非常重要)
  2. 事务尽量短
  3. 避免在事务中执行慢查询或外部请求
  4. 加锁顺序统一:比如 always lock smaller ID first

5. 如何查看当前锁情况(运维最常用)

查看锁:

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;

6. 查出阻塞链(生产级排查大招)

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;

7. 强制取消阻塞的会话

SELECT pg_terminate_backend(12345);

8. 典型业务锁场景总结

场景 推荐用法 说明
抢订单、抢红包 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 防止长锁

9. 优化锁的最佳实践(非常关键)

1. 事务越短越好

不要在事务里做:

  • API 请求
  • 网络请求
  • 慢查询
  • 人工等待

2. 按固定顺序访问行(最有效防死锁)

例如:

先锁小 ID,再锁大 ID

3. WHERE 带索引,否则锁整表 ⚠️

UPDATE users SET status = 1 WHERE email = 'x';

如果 email 没索引

👉 锁表扫描期间会堵住所有写操作!

4. 读写分离 NOT ENOUGH

OLTP 业务仍然可能锁冲突。

5. 使用 SKIP LOCKED 提高队列并发