PostgreSQL 的锁机制分为多个层级,从应用层的 Advisory Lock 到行级、表级,再到内核内部的 LWLock,每一层都有其适用场景。

锁的层级总览

层级 类型 由谁控制 可见性
表级锁 8 种模式 SQL 语句或 LOCK TABLE pg_locks
行级锁 4 种模式 SELECT FOR ... pg_locks (元组锁)
页级锁 共享/排他 Buffer Manager 内部 不可见
Advisory Lock 会话级/事务级 应用程序 pg_locks
Predicate Lock 谓词锁 串行化隔离级别 (SSI) pg_locks
LWLock / Spinlock 内核共享内存保护 内核内部 不可见

表级锁

PostgreSQL 表级锁有 8 种模式,从弱到强排列。冲突意味着两个锁不能被同时持有。

8 种模式

模式 缩写 典型操作 说明
ACCESS SHARE AS SELECT 最弱,只与 ACCESS EXCLUSIVE 冲突
ROW SHARE RS SELECT FOR UPDATE/SHARE 与 EXCLUSIVE、ACCESS EXCLUSIVE 冲突
ROW EXCLUSIVE RE INSERT、UPDATE、DELETE 与 SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE 冲突
SHARE UPDATE EXCLUSIVE SUE VACUUM、ANALYZE、CREATE INDEX CONCURRENTLY 与自身及更强模式冲突
SHARE S CREATE INDEX(非 CONCURRENTLY) 阻止并发写
SHARE ROW EXCLUSIVE SRE CREATE TRIGGER、部分 ALTER TABLE 比 SHARE 更强,与自身冲突
EXCLUSIVE E REFRESH MATERIALIZED VIEW CONCURRENTLY 只允许并发 ACCESS SHARE(即普通 SELECT)
ACCESS EXCLUSIVE AE DROP TABLE、TRUNCATE、ALTER TABLE、LOCK TABLE 最强,与所有模式冲突,阻塞所有操作包括 SELECT

冲突矩阵

              AS  RS  RE  SUE  S  SRE  E  AE
ACCESS SHARE   -   -   -   -   -   -   -  ✗
ROW SHARE      -   -   -   -   -   -   ✗  ✗
ROW EXCLUSIVE  -   -   -   -   ✗   ✗   ✗  ✗
SHARE UPD EXC  -   -   -   ✗   ✗   ✗   ✗  ✗
SHARE          -   -   ✗   ✗   -   ✗   ✗  ✗
SHARE ROW EXC  -   -   ✗   ✗   ✗   ✗   ✗  ✗
EXCLUSIVE      -   ✗   ✗   ✗   ✗   ✗   ✗  ✗
ACCESS EXCL    ✗   ✗   ✗   ✗   ✗   ✗   ✗  ✗

显式表锁

-- 显式加表锁,默认 ACCESS EXCLUSIVE
LOCK TABLE orders;

-- 指定模式
LOCK TABLE orders IN SHARE MODE;
LOCK TABLE orders IN ROW EXCLUSIVE MODE;

-- 非阻塞,获取不到立即失败
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE NOWAIT;

常见场景:批量数据迁移时先 LOCK TABLE ... IN EXCLUSIVE MODE,防止迁移过程中有新写入。


行级锁

行级锁在 SELECT ... FOR ... 语句执行时获取,事务结束时自动释放。

4 种模式

语法 强度 说明
FOR KEY SHARE 最弱 允许其他事务修改非主键列,阻止 FOR UPDATE
FOR SHARE 弱 阻止其他事务修改行,允许多个事务同时持有共享锁
FOR NO KEY UPDATE 强 修改行但不影响主键/唯一键时使用
FOR UPDATE 最强 排他锁,阻止其他事务的任何写和锁

冲突关系

FOR KEY SHARE FOR SHARE FOR NO KEY UPDATE FOR UPDATE
FOR KEY SHARE - - - ✗
FOR SHARE - - ✗ ✗
FOR NO KEY UPDATE - ✗ ✗ ✗
FOR UPDATE ✗ ✗ ✗ ✗

阻塞行为选项

SELECT FOR UPDATE 有三种等待策略:

选项 行为
默认(无选项) 阻塞等待,直到持有锁的事务提交或回滚
NOWAIT 无法立即获取锁时抛出错误,不等待
SKIP LOCKED 跳过已被锁定的行,继续处理其余行
-- 默认:阻塞等待,持有锁的事务提交或回滚后才能继续
-- 如果对方事务长时间不结束,当前查询会一直挂起
-- 可通过 SET lock_timeout = '5s' 设置最长等待时间,超时后报错
SELECT * FROM orders WHERE id = 1 FOR UPDATE;

-- NOWAIT:获取不到立即报错(ERROR: could not obtain lock on row in relation "orders")
SELECT * FROM orders WHERE id = 1 FOR UPDATE NOWAIT;

-- SKIP LOCKED:跳过已被锁定的行(任务队列场景常用)
SELECT * FROM tasks WHERE status = 'pending' FOR UPDATE SKIP LOCKED LIMIT 10;

lock_timeout 适合不想无限等待又不想立即失败的场景:

SET lock_timeout = '3s';
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 等待超过 3 秒仍未获取到锁,抛出 ERROR: canceling statement due to lock timeout

SKIP LOCKED 实现任务队列

SKIP LOCKED 是实现无竞争任务队列的标准做法,多个 worker 并发消费时互不阻塞:

-- Worker 抢任务:跳过其他 worker 已锁定的行
BEGIN;
SELECT id, payload
FROM tasks
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1;

-- 处理完成后更新状态
UPDATE tasks SET status = 'done' WHERE id = $1;
COMMIT;

Advisory Lock 咨询锁

什么是 Advisory Lock

Advisory Lock (咨询锁) 是 PostgreSQL 提供的一种应用层协调机制。与行锁、表锁保护的是数据库内部的数据对象不同, Advisory Lock 的含义完全由应用自己定义 —— 数据库只负责加锁/解锁的原子性和互斥性, 至于这把锁代表什么、保护什么资源, 完全取决于应用程序的约定。

Advisory Lock 使用一个 64 位整数 (或两个 32 位整数) 作为锁的 key, 存储在共享内存中, 不写入 WAL, 不占用磁盘空间。

核心 API

Session 级别 (会话级)

-- 获取排他锁 (阻塞, 直到获取成功)
SELECT pg_advisory_lock(key bigint);

-- 尝试获取排他锁 (非阻塞, 成功返回 true, 失败返回 false)
SELECT pg_try_advisory_lock(key bigint);

-- 获取共享锁 (多个会话可同时持有共享锁)
SELECT pg_advisory_lock_shared(key bigint);

-- 尝试获取共享锁 (非阻塞)
SELECT pg_try_advisory_lock_shared(key bigint);

-- 释放排他锁
SELECT pg_advisory_unlock(key bigint);

-- 释放共享锁
SELECT pg_advisory_unlock_shared(key bigint);

-- 释放当前会话所有 Advisory Lock
SELECT pg_advisory_unlock_all();

Transaction 级别 (事务级)

-- 获取事务级排他锁 (事务结束时自动释放, 无法手动解锁)
SELECT pg_advisory_xact_lock(key bigint);

-- 尝试获取事务级排他锁
SELECT pg_try_advisory_xact_lock(key bigint);

两个 int4 参数版本

当业务 key 由两个维度组成时 (如 resource_type + resource_id), 可以用两个 int4:

-- 等效于 pg_advisory_lock((int8(class) << 32) | int8(id))
SELECT pg_advisory_lock(class int4, id int4);
SELECT pg_try_advisory_lock(class int4, id int4);

Session 级 vs Transaction 级的区别

维度 Session 级 Transaction 级
释放时机 手动调用 pg_advisory_unlock 或连接断开 事务提交或回滚时自动释放
可重入 ✅ 同一 Session 可多次加锁, 需同等次数解锁 ✅ 事务内多次加锁只需一次 (事务结束统一释放)
可手动解锁 ✅ ❌ 只能等事务结束
连接池兼容性 ⚠️ 有风险 (见下文) ✅ 使用 AUTOCOMMIT 时自然归还

Advisory Lock 的重要特性

1. 存储在共享内存, 不写 WAL

Advisory Lock 不记录到 WAL (Write-Ahead Log), 因此:

  • 不会复制到 Standby, 主从切换后锁状态消失
  • 服务器崩溃重启后锁状态消失
  • 性能极高, 无磁盘 I/O

这与行级锁表方案形成对比: 行级锁写入 WAL, 可以通过同步复制保证 Failover 后锁一致性。

2. 可重入性

Session 级 Advisory Lock 是可重入的: 同一会话可以对同一个 key 多次调用 pg_advisory_lock, 内部维护一个计数器, 必须调用相同次数的 pg_advisory_unlock 才能真正释放。

SELECT pg_advisory_lock(100);   -- count = 1
SELECT pg_advisory_lock(100);   -- count = 2
SELECT pg_advisory_unlock(100); -- count = 1, 锁仍持有
SELECT pg_advisory_unlock(100); -- count = 0, 锁释放

3. 连接断开自动释放

持有 Session 级 Advisory Lock 的连接断开后 (正常关闭或异常崩溃), PostgreSQL 会自动释放该会话持有的所有 Advisory Lock。这天然解决了死锁问题 —— 持有锁的进程崩溃后锁不会永久阻塞其他进程。

使用连接池的注意事项

使用 PgBouncer 等连接池时, Session 级 Advisory Lock 存在风险:

应用 A: 从连接池借到连接 conn-1
应用 A: pg_advisory_lock(100) → 加锁成功
应用 A: 完成业务, 将 conn-1 归还连接池  ← 忘记 unlock!

应用 B: 从连接池借到 conn-1
应用 B: 以为是干净连接, 实际上 conn-1 仍持有 key=100 的锁
应用 B: 对其他 key 加锁, 操作共享资源 → 产生意外的锁竞争

解决方案:

  1. 始终在 finally 块中解锁, 确保归还连接前释放锁
  2. 优先使用 Transaction 级 pg_advisory_xact_lock, 事务结束时自动释放, 配合 AUTOCOMMIT 使用时尤其安全
  3. 在 pg_advisory_lock 前先调用 pg_advisory_unlock_all() 作为保险措施 (不推荐, 副作用大)

典型使用场景

场景一: 分布式定时任务防重

多个实例同时运行定时任务时, 用 Advisory Lock 保证只有一个实例执行:

-- 在应用代码中 (伪代码)
connection.execute("SELECT pg_try_advisory_lock(hashtext('daily_report_job'))");
if result == true:
    run_daily_report()
    connection.execute("SELECT pg_advisory_unlock(hashtext('daily_report_job'))")
else:
    log("Another instance is running the job, skip")

场景二: 防止并发处理同一业务对象

-- 对 order_id=12345 加锁, 防止并发处理
SELECT pg_try_advisory_xact_lock(12345);
-- 返回 true: 获取成功, 处理订单
-- 返回 false: 已有其他事务在处理, 跳过或等待

-- 事务提交时自动释放, 无需手动 unlock
COMMIT;

场景三: 与 SELECT FOR UPDATE 配合

先用 Advisory Lock 防止并发, 再用 SELECT FOR UPDATE 锁定行:

BEGIN;
SELECT pg_advisory_xact_lock(order_id);
SELECT * FROM orders WHERE id = order_id FOR UPDATE;
-- 处理业务逻辑
COMMIT;

Advisory Lock vs 其他方案对比

方案 性能 复制到 Standby 崩溃后自动释放 适用场景
Advisory Lock (Session) ⭐⭐⭐⭐⭐ ❌ ✅ (连接断开) 任务调度, 轻量协调
Advisory Lock (Xact) ⭐⭐⭐⭐⭐ ❌ ✅ (事务结束) 短事务内互斥
行级锁表 ⭐⭐⭐ ✅ (同步复制时) ✅ (TTL + 清理) 强一致性, 跨服务
Redis SET NX ⭐⭐⭐⭐⭐ ❌ (异步复制) ✅ (TTL) 高并发, 允许极低概率失败
ZooKeeper / etcd ⭐⭐⭐ ✅ (Raft) ✅ 强一致性要求

主从切换与数据库重启对 Advisory Lock 的影响

结论先行: Advisory Lock 在主从切换和数据库重启后会完全消失。

为什么会消失

Advisory Lock 存储在 PostgreSQL 的共享内存 (Shared Memory) 中, 不写入 WAL (Write-Ahead Log)。这意味着:

  • 进程内存的数据在进程终止后即消失
  • 没有 WAL 就无法复制到 Standby
  • 没有 WAL 就无法在崩溃恢复时重放

场景一: 数据库重启

应用 A 持有 Advisory Lock(100) ← 存在共享内存中

数据库重启 (计划内 or 崩溃)
    ↓
共享内存清空
    ↓
Advisory Lock(100) 消失

数据库重启完成后:
- 应用 A 的连接已断开 (连接断开时锁本来就会自动释放)
- 即使应用 A 重新连接, 也需要重新申请锁
- 其他应用可以正常申请 Lock(100)

影响: 数据库重启后锁状态清空, 行为是符合预期的。因为持有锁的连接在重启时已强制断开, 锁随之释放, 不存在「锁记录残留但持有者已消失」的僵尸状态。

场景二: 主从切换 (Failover)

应用 A 连接 Primary, 持有 Advisory Lock(100)
Primary 宕机
    ↓
Standby 晋升为新 Primary
    ↓
新 Primary 共享内存中没有 Advisory Lock(100)
    ↓
应用 B 连接新 Primary, 申请 Advisory Lock(100) → 成功! ⚠️

同时:
应用 A 的连接已断开 (Primary 宕机)
应用 A 重连新 Primary, 发现自己没有锁了

影响: Failover 窗口期内存在互斥性被短暂破坏的风险 —— 原持有锁的客户端还没意识到 Primary 已切换, 而新 Primary 上已经可以申请到同一把锁。

与行级锁表的对比

事件 Advisory Lock 行级锁表 (异步复制) 行级锁表 (同步复制)
数据库重启 锁消失 (正常) 锁记录保留 锁记录保留
主从切换 锁消失 ❌ 可能丢锁 ✅ 锁记录保留
对互斥性的影响 短暂窗口期风险 短暂窗口期风险 安全

实际应对方式

方案一: 业务幂等兜底 (最重要)

无论用何种分布式锁方案, 幂等性都是最后一道防线。Failover 期间即使两个客户端同时获得锁, 幂等检查也能保证业务结果正确:

-- 获得锁后, 先检查业务状态
SELECT pg_try_advisory_xact_lock(order_id);
IF acquired THEN
    -- 检查幂等: 是否已经处理过?
    IF NOT EXISTS (SELECT 1 FROM orders WHERE id = order_id AND status = 'processed') THEN
        -- 处理业务
        UPDATE orders SET status = 'processed' WHERE id = order_id;
    END IF;
END IF;

方案二: 客户端感知重连

应用层在重连后重新申请锁, 并检查业务状态是否需要重新处理。

方案三: 对强一致性要求高时, 改用行级锁表 + 同步复制

如果业务绝对不能容忍 Failover 期间的短暂互斥失效, 应改用行级锁表方案并开启同步复制:

# postgresql.conf
synchronous_commit = remote_apply
synchronous_standby_names = 'standby1'

总结

场景 Advisory Lock 行为 是否需要担心
计划内重启 锁随连接断开消失, 正常 不需要, 持有锁的连接会重新连接并申请
崩溃重启 锁消失, 正常 不需要, 配合业务幂等即可
主从切换 锁消失, 存在短暂窗口期 需要幂等兜底
Standby 上申请锁 Standby 只读, 无法申请 不适用

Advisory Lock 不适合需要跨 Failover 保持严格互斥的场景。 对于这类需求, 应使用行级锁表 + 同步复制, 或 etcd/ZooKeeper。大多数场景下, Advisory Lock + 业务幂等是足够且高效的组合。

查看当前 Advisory Lock 状态

-- 查看当前所有 Advisory Lock
SELECT
    pid,
    locktype,
    classid,
    objid,
    mode,
    granted
FROM pg_locks
WHERE locktype = 'advisory';

-- 结合进程信息
SELECT
    l.pid,
    a.usename,
    a.application_name,
    l.classid,
    l.objid,
    l.mode,
    l.granted
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.locktype = 'advisory';

Predicate Lock 谓词锁

谓词锁是 PostgreSQL 在串行化隔离级别 (SERIALIZABLE) 下自动使用的锁,普通 READ COMMITTED 下不涉及。

目的

防止序列化异常(包括幻读)。普通的行锁和表锁只能防止对已存在行的并发修改,无法防止「一个事务读取了某个范围,另一个事务在该范围内插入了新行」这类幻读。谓词锁通过追踪读取谓词(即 WHERE 条件所覆盖的范围)来检测潜在冲突。

工作方式

-- 开启串行化隔离
BEGIN ISOLATION LEVEL SERIALIZABLE;

-- 读取操作会隐式在读取范围上加谓词锁
SELECT SUM(amount) FROM accounts WHERE user_id = 1;

-- 如果另一个并发事务在 user_id=1 的范围内插入了新行
-- PostgreSQL 会检测到冲突,并在提交时回滚其中一个事务
COMMIT;

当检测到序列化冲突时,PostgreSQL 会抛出:

ERROR:  could not serialize access due to concurrent update
SQLSTATE: 40001

应用需要捕获 40001 错误并重试事务。

与行级锁的区别

行级锁 谓词锁
保护对象 已存在的行 查询谓词覆盖的范围
防幻读 ❌ ✅
隔离级别 任意 仅 SERIALIZABLE
冲突处理 阻塞等待 事务回滚(需重试)

查看锁状态

pg_locks 视图

-- 查看所有锁
SELECT
    pid,
    locktype,
    relation::regclass AS table_name,
    mode,
    granted,
    transactionid,
    classid,
    objid
FROM pg_locks
ORDER BY pid;

locktype 常见值:

值 含义
relation 表级锁
tuple 行级锁(元组)
advisory Advisory Lock
transactionid 事务 ID 锁
virtualxid 虚拟事务 ID 锁

查看锁持有时长

-- 查看锁:持有时长、模式、所属进程,按持有时间倒序
SELECT locker.pid,
       pc.relname,
       locker.mode,
       locker_act.application_name,
       least(query_start, xact_start)                        start_time,
       locker_act.state,
       CASE
           WHEN granted = 'f' THEN 'wait_lock'
           WHEN granted = 't' THEN 'get_lock'
       END                                                    lock_status,
       current_timestamp - least(query_start, xact_start) AS runtime,
       locker_act.query
FROM pg_locks locker
JOIN pg_stat_activity locker_act ON locker.pid = locker_act.pid
JOIN pg_class pc ON locker.relation = pc.oid
WHERE NOT locker.pid = pg_backend_pid()
  AND pc.reltype <> 0
ORDER BY runtime DESC;

查看锁等待

-- 查看正在等待锁的会话
SELECT
    blocked.pid          AS blocked_pid,
    blocked_act.query    AS blocked_query,
    blocking.pid         AS blocking_pid,
    blocking_act.query   AS blocking_query,
    blocked_locks.mode   AS waiting_for_mode
FROM pg_locks blocked_locks
JOIN pg_stat_activity blocked_act ON blocked_act.pid = blocked_locks.pid
JOIN pg_locks blocking_locks
    ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    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.pid != blocked_locks.pid
JOIN pg_stat_activity blocking_act ON blocking_act.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

查看 Advisory Lock

SELECT
    l.pid,
    a.usename,
    a.application_name,
    l.classid,
    l.objid,
    l.mode,
    l.granted
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.locktype = 'advisory';

各锁类型对比

类型 粒度 自动释放 持久化 跨 Failover 适用场景
表级锁 表 事务结束 ✅ WAL ✅ DDL、批量操作
行级锁 行 事务结束 ✅ WAL ✅ 并发更新同一行
Advisory Lock (会话) 自定义 连接断开 ❌ 内存 ❌ 任务调度、轻量协调
Advisory Lock (事务) 自定义 事务结束 ❌ 内存 ❌ 短事务内互斥
Predicate Lock 谓词范围 事务结束 ❌ 内存 ❌ SERIALIZABLE 防幻读