Skip to content

顾问锁概述(Advisory Locks Overview) ​

普通锁是数据库替你加的:你写 UPDATE,PostgreSQL 自动对行加排他锁;你写 ALTER TABLE,它自动加 ACCESS EXCLUSIVE。而顾问锁(Advisory Lock)反过来——键值由你定、含义由你定、加锁释放的时机也由你定,数据库只负责一件事:保证同一个键在任意时刻只能被一个会话(或一个事务)独占。

它把 PostgreSQL 变成了一个「免费的分布式锁服务」。本页讲清顾问锁是什么、它和表锁的本质区别、两种封锁范围、两套互不重叠的键空间、为什么它不参与 MVCC,以及如何用 pg_locks 把它看得一清二楚。

前置知识

阅读本节前,建议先了解:表级锁(ACCESS SHARE / EXCLUSIVE)、行级锁(FOR UPDATE / FOR SHARE)

基础概念 ​

顾问锁:语义由应用定义,互斥由数据库保证 ​

顾问锁(Advisory Lock) 是 PostgreSQL 提供的一种应用自定义含义的锁。官方对它的定义非常直白:

PostgreSQL provides a means for creating locks that have application-defined meanings. These are called advisory locks, because the system does not enforce their use — it is up to the application to use them correctly.

关键在最后一句:数据库不强制你怎么用。它不知道 pg_advisory_lock(42) 里的 42 代表「订单 42」还是「2026 年 9 月的对账任务」,也不知道你拿到锁之后该干什么。它只承诺一件事:

text
同一个锁号(同一键空间内),同一时刻只有一个持有者。

所以顾问锁的本质是:「一个由数据库托管、带自动释放语义的互斥量(Mutex)」。它和表数据没有任何关系,加锁不会阻止任何人读写任何表。

为什么需要它:MVCC 覆盖不到的角落 ​

MVCC(多版本并发控制)解决的是「读写不互相阻塞」,行锁解决的是「并发改同一行」。但有一类需求它们都管不了:

需求MVCC/行锁为什么不行
全库同时只允许一个定时任务在跑没有对应的「行」可以锁
同一租户的订单必须串行处理订单可能还不存在,锁不住未来的行
缓存击穿时只放一个请求去查库要锁的是一个字符串 key,不是表也不是行
迁移脚本全局互斥迁移跨越多张表,锁表会误伤

这些场景的共同点是:要保护的「资源」在数据库里没有对应的物理对象。顾问锁就是为这类需求准备的——你给它一个数字,它给你一个互斥保证。

还有一种「反向」用法:某些本来用普通锁的地方,用顾问锁反而更轻。比如「队列消费要保证同一消息只被一个 worker 处理」,如果新插入的行还没提交,SELECT ... FOR UPDATE SKIP LOCKED 看起来能用,但一旦涉及跨事务的业务串行,顾问锁更直接。

与表锁的本质区别 ​

这是最容易混淆的地方,必须一次说清。顾问锁和表锁共用同一个锁管理器(Lock Manager),但它们的冲突域完全不同:

维度表锁 / 行锁顾问锁
锁对象真实存在的表、索引、行应用自定义的数字键
谁决定加锁数据库自动(隐式)或 LOCK 语句只能由应用显式调用函数
加锁方式LOCK TABLE、SELECT FOR UPDATE、DML 隐式pg_advisory_lock() 等函数
自动释放事务结束必然释放分会话级/事务级两种
语义保护数据库内部一致性保护应用层互斥
冲突范围表锁与表锁冲突顾问锁与顾问锁冲突,与表锁永不冲突

最反直觉的一点:持有一把顾问锁,完全不会阻止任何人 SELECT、INSERT、UPDATE、DROP 那些被你「逻辑保护」的表。

sql
-- 会话 A
SELECT pg_advisory_lock(9527);

-- 会话 B:完全不受影响,即使 9527 被解读为「保护 orders 表」
UPDATE orders SET status = 'paid' WHERE id = 100;
-- UPDATE 1

顾问锁保护的是应用层的约定,不是数据库层的对象。你是否真的按约定行事,数据库不管。

两种封锁范围:会话级与事务级 ​

官方原文:There are two ways to acquire an advisory lock in PostgreSQL: at session level or at transaction level.

范围获取函数释放时机
会话级(Session Level)pg_advisory_lock()、pg_try_advisory_lock() 等显式 pg_advisory_unlock(),或会话结束
事务级(Transaction Level)pg_advisory_xact_lock()、pg_try_advisory_xact_lock() 等当前事务 COMMIT/ROLLBACK 时自动释放

两者的关键差异在于是否遵守事务语义:

text
会话级:不遵守事务语义。
        BEGIN; SELECT pg_advisory_lock(1); ROLLBACK;
        → 锁没有被回滚,仍然被持有!必须手动 unlock。

事务级:遵守事务语义。
        BEGIN; SELECT pg_advisory_xact_lock(1); ROLLBACK;
        → 锁随事务一起被释放(回滚既撤销数据也撤销锁)。

官方对会话级的描述同样明确:a lock acquired during a transaction that is later rolled back will still be held following the rollback, and likewise an unlock is effective even if the calling transaction fails later.

这条规则是无数「锁泄漏」事故的根源。两个范围各自有独立页面展开,本页只做区分。

两套互不重叠的键空间 ​

顾问锁的键(Key)有两种形态,对应两套完全独立、互不干扰的键空间(Key Space):

函数形态键的构成键空间
pg_advisory_lock(key bigint)单个 64 位有符号整数64 位键空间
pg_advisory_lock(key1 int, key2 int)两个 32 位有符号整数32 位双键空间

官方原话:which can be identified either by a single 64-bit key value or two 32-bit key values (note that these two key spaces do not overlap)。

「不重叠」意味着:

sql
SELECT pg_advisory_lock(1);        -- 占用 64 位键空间的 1
SELECT pg_advisory_lock(1, 1);     -- 占用双键空间的 (1,1)
-- 这两把锁没有任何关系,可以同时被同一会话或不同会话持有

那 pg_advisory_lock(1) 和 pg_advisory_lock(1, 0) 呢?也不冲突——因为 (1, 0) 走的是另一套键空间。不要以为可以把 64 位键拆成两半当作两个 32 位键用,它们进的是不同的哈希表。

双键形态的典型用途是「命名空间 + 资源 ID」:

sql
-- 第一键:业务域编号;第二键:资源 ID
SELECT pg_advisory_lock(1, 42);    -- 域 1 = 订单处理,资源 42 = 订单 42
SELECT pg_advisory_lock(2, 42);    -- 域 2 = 报表生成,资源 42 = 报表 42
-- 上面两把锁不冲突,因为第一键不同

顾问锁不参与 MVCC ​

顾问锁的内存结构存放在锁管理器(Lock Manager)里,而不是任何数据页中。由此推出几条重要性质:

  1. 不产生任何数据表的可见性变化——加锁不加行、不加版本、不写用户表。
  2. 不进 WAL 的常规数据流——会话级锁的持有状态不写 WAL(崩溃后自然全丢,因为会话也没了)。
  3. 崩溃即全部消失——数据库进程重启,所有顾问锁自动清空。这不是缺陷,而是设计:锁的持有者是会话,会话没了锁自然没了。
  4. 不阻塞 DDL 之外的东西——即使你「逻辑上」用顾问锁保护一张表,DROP TABLE 也不会等它。
text
会话级顾问锁的可靠边界:
  ✓ 进程崩溃 → 锁随会话消失(不会泄漏)
  ✗ 应用忘记 unlock 但连接仍活着 → 锁一直占着(这才是真正的风险)

pg_locks 中的顾问锁长什么样 ​

顾问锁在 pg_locks 视图里完全可见,靠 locktype = 'advisory' 精确筛出:

sql
SELECT locktype, database, classid, objid, objsubid, mode, granted, pid
FROM pg_locks
WHERE locktype = 'advisory';
字段对顾问锁的含义
locktype恒为 'advisory'
database顾问锁是每个数据库本地的,此列有实际意义(不同库的同号锁互不冲突)
modeExclusiveLock(独占)或 ShareLock(共享)
granted是否已授予;false 表示正在等待
pid持有该锁的进程;prepared 事务持有的锁此处为 NULL

关键:键在两个字段里的编码方式。

text
单 bigint 键:objsubid = 1
  classid = 键的高 32 位(无符号)
  objid   = 键的低 32 位(无符号)

双 int 键:objsubid = 2
  classid = 第一个 int 键
  objid   = 第二个 int 键

还原单个 64 位键的表达式(官方 pg_locks 文档给出):

sql
SELECT (classid::bigint << 32) | objid::bigint AS advisory_key
FROM pg_locks
WHERE locktype = 'advisory' AND objsubid = 1;

注意 classid 与 objid 都是无符号展示的 32 位值,做位移前必须先 ::bigint,否则高位会溢出成负数。

锁号命名约定:从字符串到键 ​

顾问锁的键必须是数字,但业务上的「资源」往往是字符串(如 order:9527、user@example.com)。常见做法是用哈希函数把字符串压成一个数字。

PostgreSQL 内置两个哈希函数:

函数返回类型说明
hashtext(text)integer(32 位)经典哈希,结果可正可负
hashtextextended(text, bigint)bigint(64 位)更宽的哈希,第二参数是种子(seed)

用它们生成锁号:

sql
-- 塞进单 bigint 键空间
SELECT pg_advisory_lock(hashtextextended('order:9527', 0));

-- 塞进双 int 键空间(取模压到 32 位)
SELECT pg_advisory_lock(1, hashtext('order:9527'));

-- 直接用文本哈希做键
SELECT pg_advisory_lock(hashtext('process-user-payments'));

哈希会碰撞

哈希必然存在碰撞:不同的字符串可能得到相同的哈希值,从而错误地互相阻塞。对正确性要求高的场景(如资金操作),不要把「人类可读字符串」直接当锁键,而应用业务主键的整数 ID(如 order_id),碰撞概率才可控。

max_locks_per_transaction 与锁槽位 ​

顾问锁和普通锁共用同一套锁槽位(Lock Slot),其规模由服务器参数 max_locks_per_transaction 决定:

text
可用锁槽位总数 ≈ max_locks_per_transaction × (max_connections + max_prepared_transactions + 后台进程数)

max_locks_per_transaction 默认值是 64。这意味着单个事务能同时持有的锁数量是有上限的——如果你在一个大事务里一次性申请成千上万把顾问锁,可能撞上:

text
ERROR:  out of shared memory
HINT:  You might need to increase max_locks_per_transaction.

这不是顾问锁特有的限制,而是整个锁管理器的容量限制。规划批量加锁任务时要把它算进去。

顾问锁 vs 表锁 vs 外部分布式锁 ​

选型时对照这张表最省事:

维度顾问锁表锁/行锁Redis / ZooKeeper 锁
一致性来源PostgreSQL 锁管理器同左外部服务
额外依赖无(已有数据库即可)无需要部署与运维
自动释放会话结束/事务结束事务结束依赖 TTL 或租约
跨库/跨集群不跨(每库本地)不跨跨
持有者身份数据库会话数据库事务外部客户端
故障时连接断开即释放事务中断即释放需等 TTL 过期
性能内存操作,很快内存操作,很快一次网络往返

一句话总结:顾问锁适合「同一 PostgreSQL 集群内部」的互斥;跨集群、跨存储系统,它无能为力。

语法与用法 ​

函数清单 ​

顾问锁没有 SQL 语句关键字,全部通过函数调用。共 12 个函数,按「范围 × 独占/共享 × 阻塞/非阻塞」组合:

text
会话级(Session Level)
  pg_advisory_lock(key bigint)                  → void    独占,阻塞
  pg_advisory_lock(key1 int, key2 int)          → void    独占,阻塞
  pg_try_advisory_lock(key bigint)              → boolean 独占,立即返回
  pg_try_advisory_lock(key1 int, key2 int)      → boolean 独占,立即返回
  pg_advisory_lock_shared(key bigint)           → void    共享,阻塞
  pg_advisory_lock_shared(key1 int, key2 int)   → void    共享,阻塞
  pg_try_advisory_lock_shared(key bigint)       → boolean 共享,立即返回
  pg_try_advisory_lock_shared(key1 int, key2 int) → boolean 共享,立即返回

事务级(Transaction Level)
  pg_advisory_xact_lock(key bigint)               → void    独占,阻塞
  pg_advisory_xact_lock(key1 int, key2 int)       → void    独占,阻塞
  pg_try_advisory_xact_lock(key bigint)           → boolean 独占,立即返回
  pg_try_advisory_xact_lock(key1 int, key2 int)   → boolean 独占,立即返回
  pg_advisory_xact_lock_shared(key bigint)        → void    共享,阻塞
  pg_advisory_xact_lock_shared(key1 int, key2 int) → void   共享,阻塞
  pg_try_advisory_xact_lock_shared(key bigint)    → boolean 共享,立即返回
  pg_try_advisory_xact_lock_shared(key1 int, key2 int) → boolean 共享,立即返回

解锁(仅会话级需要手动解锁)
  pg_advisory_unlock(key bigint)                → boolean
  pg_advisory_unlock(key1 int, key2 int)        → boolean
  pg_advisory_unlock_shared(key bigint)         → boolean
  pg_advisory_unlock_shared(key1 int, key2 int) → boolean
  pg_advisory_unlock_all()                      → void

最简示例 ​

sql
-- 1. 申请一把会话级独占锁
SELECT pg_advisory_lock(9527);
text
 pg_advisory_lock
------------------

(1 row)
sql
-- 2. 同一会话再次申请同一把锁(可重入,计数 +1)
SELECT pg_advisory_lock(9527);
text
 pg_advisory_lock
------------------

(1 row)
sql
-- 3. 必须解锁同样的次数才能彻底释放
SELECT pg_advisory_unlock(9527);
SELECT pg_advisory_unlock(9527);
text
 pg_advisory_unlock
--------------------
 t
(1 row)

 pg_advisory_unlock
--------------------
 t
(1 row)
sql
-- 4. 非阻塞版本:拿不到立刻返回 false,不等待
SELECT pg_try_advisory_lock(9527);
text
 pg_try_advisory_lock
----------------------
 t
(1 row)

查询当前所有顾问锁 ​

sql
SELECT
    pid,
    classid,
    objid,
    objsubid,
    mode,
    granted,
    CASE objsubid
        WHEN 1 THEN ((classid::bigint << 32) | objid::bigint)::text
        WHEN 2 THEN format('(%s, %s)', classid, objid)
    END AS key_readable
FROM pg_locks
WHERE locktype = 'advisory'
ORDER BY pid, key_readable;
text
  pid  | classid | objid | objsubid |     mode      | granted | key_readable
-------+---------+-------+----------+---------------+---------+--------------
 18422 |       0 |  9527 |        1 | ExclusiveLock | t       | 9527
 18422 |       0 |  9527 |        1 | ExclusiveLock | t       | 9527
 18740 |       0 | 16385 |        2 | ExclusiveLock | f       | (1, 16385)
(3 rows)

注意第 2 行:同一把锁被同一会话申请两次,会在 pg_locks 里出现两行(计数反映在行数上)。第 3 行 granted = f 表示某个会话正在等待 (1, 16385)。

详细说明 ​

顾问锁的锁模式只有两种 ​

和表锁有 8 种模式不同,顾问锁只有两档:

模式获取方式冲突规则
ExclusiveLockpg_advisory_lock*(默认)与任何其他持有者冲突
ShareLockpg_advisory_lock_shared*与其他共享锁兼容,与独占锁冲突
sql
-- 会话 A:拿共享锁
SELECT pg_advisory_lock_shared(100);

-- 会话 B:也能拿到同一 key 的共享锁(不冲突)
SELECT pg_advisory_lock_shared(100);
text
-- 会话 C:申请独占锁,会被阻塞
SELECT pg_advisory_lock(100);
-- 一直等待,直到 A 和 B 都释放

共享/独占的典型用途是「读写锁」:多个读者并发读缓存,写者独占重建缓存。

可重入:加锁是计数,不是布尔 ​

同一会话可以重复申请同一把顾问锁,不会自我死锁。内部维护一个计数器:

sql
SELECT pg_advisory_lock(42);
SELECT pg_advisory_lock(42);
SELECT pg_advisory_lock(42);
-- 计数 = 3

SELECT pg_advisory_unlock(42);  -- 计数 = 2,返回 t
SELECT pg_advisory_unlock(42);  -- 计数 = 1,返回 t
SELECT pg_advisory_unlock(42);  -- 计数 = 0,真正释放,返回 t
SELECT pg_advisory_unlock(42);  -- 已经没有了,返回 f(不报错)
text
 pg_advisory_unlock
--------------------
 f
(1 row)

pg_advisory_unlock 在没持有该锁时返回 false 而非报错——这一点常被用来做「我到底有没有持有」的探测,但更稳妥的方式是查 pg_locks。

阻塞时受 lock_timeout 约束 ​

顾问锁的阻塞等待会计入 lock_timeout(默认 0,即无限等待):

sql
SET lock_timeout = '2s';
SELECT pg_advisory_lock(42);
text
ERROR:  canceling statement due to lock timeout

这比无限阻塞好得多。但会话级顾问锁有一个已知竞态:超时取消与锁授予可能同时发生,导致「语句报超时错误,但锁其实已经被本会话持有」。社区讨论中 Tom Lane 明确指出这一点,并建议报错后通过 pg_locks 复核状态:

sql
SET lock_timeout = '2s';
SELECT pg_advisory_lock(42);
-- 若报 lock timeout,务必检查是否实际已持有
SELECT count(*) FROM pg_locks
WHERE locktype = 'advisory' AND pid = pg_backend_pid()
  AND objsubid = 1 AND ((classid::bigint << 32) | objid::bigint) = 42;

事务级顾问锁不存在这个隐患——因为报错会中止事务,锁随事务一起消失。

会话级锁在连接池下极易泄漏 ​

会话级顾问锁的释放条件是「显式 unlock」或「连接断开」。而连接池(PgBouncer、HikariCP、Druid 等)复用连接,于是出现这样的灾难链条:

text
1. 请求 A 借到连接 C,执行 SELECT pg_advisory_lock(42)
2. 请求 A 的代码路径抛异常,unlock 那行没执行到
3. 连接 C 被归还到池中,没有断开
4. 请求 B 借到连接 C,执行 pg_advisory_lock(42) → 竟然成功(同会话可重入!)
5. 请求 B 以为拿到了互斥,其实请求 A 的逻辑根本没走完
6. 锁计数永远 ≥ 1,其他所有会话永远拿不到 42

在连接池环境里,优先用事务级顾问锁(pg_advisory_xact_lock),因为它绑在事务上,事务一结束必定释放,不依赖代码是否记得 unlock。

事务级锁不可手动解锁 ​

事务级顾问锁没有对应的 unlock 函数——pg_advisory_unlock 只认会话级锁。对事务级锁调用它,会返回 false:

sql
BEGIN;
SELECT pg_advisory_xact_lock(42);
SELECT pg_advisory_unlock(42);   -- 返回 f,无效
COMMIT;                          -- 这时才真正释放
text
 pg_advisory_unlock
--------------------
 f
(1 row)

正确姿势是:拿到锁 → 干活 → COMMIT(或 ROLLBACK),释放全自动。

顾问锁不跨数据库 ​

pg_locks 里顾问锁的 database 列有实际值,说明它的作用域是单个数据库:

sql
-- 在 db_a 里
SELECT pg_advisory_lock(42);

-- 在 db_b 里(另一个连接)
SELECT pg_try_advisory_lock(42);
-- t  ← 毫不冲突

如果你的应用在同一实例上有多个数据库,并期望「全局唯一」,这个行为会直接破坏你的假设。要跨库互斥,只能选一个「代表库」统一在里面加锁。

哈希函数生成锁号的取舍 ​

sql
-- 方案一:整数业务键,直接当锁号(推荐,无碰撞)
SELECT pg_advisory_lock(order_id) FROM orders WHERE id = 9527;

-- 方案二:字符串哈希成 32 位,塞进双键空间的第一键
SELECT pg_advisory_lock(1001, hashtext('checkout:' || user_id::text));

-- 方案三:字符串哈希成 64 位
SELECT pg_advisory_lock(hashtextextended('migration:2026_09', 0));

hashtextextended 的 64 位空间远大于 32 位,碰撞概率低得多。但要注意:哈希值和数据库版本相关吗? 不需要担心——hashtext/hashtextextended 的算法是稳定实现的,同一文本在同一大版本内结果一致;但跨大版本升级后不保证完全一致,因此不要把哈希值持久化进业务表当外键,只当运行时锁号用。

用 pg_locks 定位「谁把锁占了」 ​

最常见的排查需求:

sql
SELECT
    l.pid,
    a.usename,
    a.datname,
    a.client_addr,
    a.state,
    a.query_start,
    now() - a.query_start AS held_for,
    l.mode,
    CASE l.objsubid
        WHEN 1 THEN ((l.classid::bigint << 32) | l.objid::bigint)::text
        WHEN 2 THEN format('(%s, %s)', l.classid, l.objid)
    END AS key_readable,
    a.query
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
WHERE l.locktype = 'advisory'
ORDER BY held_for DESC;
text
  pid  | usename | datname | client_addr | state  |  held_for  |      key_readable
-------+---------+---------+-------------+--------+------------+------------------------
 18422 | app     | shop    | 10.0.3.17   | idle   | 02:41:19   | 9527
(1 row)

state = idle 加上很长的 held_for,就是典型的「锁泄漏」画像:会话开着、什么都不干、锁就是不还。

共享锁的行数也计入 pg_locks ​

每申请一次(含共享锁)都占一行:

sql
SELECT pg_advisory_lock_shared(500);
SELECT pg_advisory_lock_shared(500);

SELECT count(*) FROM pg_locks
WHERE locktype = 'advisory' AND objsubid = 1
  AND ((classid::bigint << 32) | objid::bigint) = 500;
text
 count
-------
     2
(1 row)

排查容量问题(out of shared memory)时,这个计数很有用——说明是谁把锁槽位吃光了。

实战示例 ​

场景:确认某个键当前是否被占用(不产生副作用) ​

sql
-- 用一个「探测键」去试,拿到立刻还
SELECT pg_try_advisory_lock(9527) AS got_it;
text
 got_it
--------
 f
(1 row)

如果返回 t,说明此刻没人占;但你拿到之后必须立刻释放,否则你自己就成了占用者:

sql
SELECT pg_advisory_unlock(9527);

更干净的做法是直接查 pg_locks,不扰动锁状态:

sql
SELECT EXISTS (
    SELECT 1 FROM pg_locks
    WHERE locktype = 'advisory' AND granted
      AND objsubid = 1
      AND ((classid::bigint << 32) | objid::bigint) = 9527
) AS is_locked;

场景:观察阻塞与等待 ​

开三个终端,按顺序执行:

sql
-- 终端 1
SELECT pg_advisory_lock(8801);
-- 保持不释放

-- 终端 2
SET lock_timeout = '5s';
SELECT pg_advisory_lock(8801);
text
ERROR:  canceling statement due to lock timeout

同时在终端 3 观察等待者:

sql
SELECT pid, mode, granted, waitstart
FROM pg_locks
WHERE locktype = 'advisory' AND NOT granted;
text
  pid  |     mode      | granted |          waitstart
-------+---------------+---------+----------------------------
 18740 | ExclusiveLock | f       | 2026-09-28 10:12:33.417+08
(1 row)

waitstart 告诉你它等了多久。这是区分「卡住」与「正常等待」的关键字段。

场景:安全地释放一个会话的全部顾问锁 ​

当某个连接疑似泄漏了多把锁,又不想杀掉连接(可能承载其他业务)时:

sql
-- 先看清楚这个连接持有哪些顾问锁
SELECT
    CASE objsubid
        WHEN 1 THEN ((classid::bigint << 32) | objid::bigint)::text
        WHEN 2 THEN format('(%s, %s)', classid, objid)
    END AS key_readable
FROM pg_locks
WHERE locktype = 'advisory' AND pid = 18422;
sql
-- 然后在该连接自己的会话里(而不是从别处)执行
SELECT pg_advisory_unlock_all();
text
 pg_advisory_unlock_all
------------------------

(1 row)

pg_advisory_unlock_all 只作用于当前会话

你无法从会话 B 释放会话 A 的顾问锁——除非把 A 杀掉(pg_terminate_backend)。这也是顾问锁「按会话归属」的直接体现。

场景:确认锁号是否落在正确的键空间 ​

sql
-- 用双键形态申请
SELECT pg_advisory_lock(1, 9527);

-- 查它的编码:objsubid 应为 2
SELECT classid, objid, objsubid, mode
FROM pg_locks
WHERE locktype = 'advisory' AND pid = pg_backend_pid();
text
 classid | objid | objsubid |     mode
---------+-------+----------+---------------
       1 |  9527 |        2 | ExclusiveLock
(1 row)

对照验证:如果同一时刻另有一个会话持有 pg_advisory_lock(9527)(单 bigint),它会是 classid = 0, objid = 9527, objsubid = 1,两把锁互不阻塞。

sql
SELECT pg_advisory_unlock(1, 9527);

场景:一次性看清键空间与模式的分布 ​

sql
SELECT
    CASE objsubid WHEN 1 THEN 'bigint(64bit)' WHEN 2 THEN 'two-int(32bit)' END AS key_space,
    mode,
    count(*) AS holders,
    count(*) FILTER (WHERE NOT granted) AS waiters
FROM pg_locks
WHERE locktype = 'advisory'
GROUP BY 1, 2
ORDER BY 1, 2;
text
    key_space    |     mode      | holders | waiters
-----------------+---------------+---------+---------
 bigint(64bit)   | ExclusiveLock |       3 |       0
 two-int(32bit)  | ExclusiveLock |       1 |       1
 two-int(32bit)  | ShareLock     |       2 |       0
(3 rows)

注意事项 ​

  • 错误!把会话级顾问锁写进连接池代码却不加 try/finally。 一旦异常路径漏掉 pg_advisory_unlock,连接被池复用后锁会永久泄漏,其他会话全部阻塞。
  • 不推荐用字符串直接当锁键(pg_advisory_lock(hashtext(...)))保护资金类关键路径——哈希碰撞会导致无谓甚至有害的串行化。优先用整数业务 ID。
  • 错误!以为在库 A 加的顾问锁能挡住库 B。 顾问锁是每库本地的,跨库互斥需要额外设计。
  • 不推荐在长事务里批量申请成千上万把顾问锁,容易触发 out of shared memory;受 max_locks_per_transaction 制约。
  • 错误!依赖「事务回滚会释放会话级顾问锁」。 会话级锁不遵守事务语义,回滚后依然持有。
  • 不推荐把顾问锁当作数据一致性手段。它不保护任何表数据,只保证「同一键同时只有一个持有者」;真正的数据一致性仍需事务与约束。
  • 错误!用顾问锁去「保护」一张会被 DDL 操作的表。 顾问锁与表锁冲突域完全不同,DROP TABLE、ALTER TABLE 不会等你的顾问锁。
  • 不推荐在业务代码里假设解锁一定成功。pg_advisory_unlock 返回 false 意味着你没持有该锁(或键空间不匹配),应记录并告警。

最佳实践 ​

  • 默认优先事务级:能用 pg_advisory_xact_lock 就不用 pg_advisory_lock,让释放与时序绑定,避免泄漏。
  • 集中管理锁号:建立一张「锁号分配表」或代码常量表,把键空间(bigint 还是 two-int)、业务域、编号写清楚,杜绝不同模块撞号。
  • 双键形态做命名空间:用第一个 int 表示业务域,第二个 int 表示资源 ID,可读性与隔离性都更好。
  • 非阻塞优先:用 pg_try_advisory_lock + 显式失败处理,而不是无限阻塞等待;必须阻塞时配 lock_timeout。
  • 加锁后立即校验:在关键路径上,拿锁成功后再查一次 pg_locks 确认持有者是自己,抵御超时竞态。
  • 监控常驻:把 locktype='advisory' AND NOT granted 的等待数、以及单个会话持有顾问锁的数量做成监控指标。
  • 释放放在 finally 块:无论用哪种范围,代码结构上都要保证释放语句位于 finally(会话级)或被事务边界兜住(事务级)。
  • 命名规范:给锁键枚举加前缀注释,例如 -- 1001: 定时任务防重,让运维看 pg_locks 时能对号入座。

下一节

继续学习:会话级顾问锁——深入 pg_advisory_lock 系列的完整函数族、可重入计数、会话生命周期与连接池下的泄漏防范。

参考链接 ​