顾问锁概述(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 月的对账任务」,也不知道你拿到锁之后该干什么。它只承诺一件事:
同一个锁号(同一键空间内),同一时刻只有一个持有者。所以顾问锁的本质是:「一个由数据库托管、带自动释放语义的互斥量(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 那些被你「逻辑保护」的表。
-- 会话 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 时自动释放 |
两者的关键差异在于是否遵守事务语义:
会话级:不遵守事务语义。
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)。
「不重叠」意味着:
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」:
-- 第一键:业务域编号;第二键:资源 ID
SELECT pg_advisory_lock(1, 42); -- 域 1 = 订单处理,资源 42 = 订单 42
SELECT pg_advisory_lock(2, 42); -- 域 2 = 报表生成,资源 42 = 报表 42
-- 上面两把锁不冲突,因为第一键不同顾问锁不参与 MVCC
顾问锁的内存结构存放在锁管理器(Lock Manager)里,而不是任何数据页中。由此推出几条重要性质:
- 不产生任何数据表的可见性变化——加锁不加行、不加版本、不写用户表。
- 不进 WAL 的常规数据流——会话级锁的持有状态不写 WAL(崩溃后自然全丢,因为会话也没了)。
- 崩溃即全部消失——数据库进程重启,所有顾问锁自动清空。这不是缺陷,而是设计:锁的持有者是会话,会话没了锁自然没了。
- 不阻塞 DDL 之外的东西——即使你「逻辑上」用顾问锁保护一张表,
DROP TABLE也不会等它。
会话级顾问锁的可靠边界:
✓ 进程崩溃 → 锁随会话消失(不会泄漏)
✗ 应用忘记 unlock 但连接仍活着 → 锁一直占着(这才是真正的风险)pg_locks 中的顾问锁长什么样
顾问锁在 pg_locks 视图里完全可见,靠 locktype = 'advisory' 精确筛出:
SELECT locktype, database, classid, objid, objsubid, mode, granted, pid
FROM pg_locks
WHERE locktype = 'advisory';| 字段 | 对顾问锁的含义 |
|---|---|
locktype | 恒为 'advisory' |
database | 顾问锁是每个数据库本地的,此列有实际意义(不同库的同号锁互不冲突) |
mode | ExclusiveLock(独占)或 ShareLock(共享) |
granted | 是否已授予;false 表示正在等待 |
pid | 持有该锁的进程;prepared 事务持有的锁此处为 NULL |
关键:键在两个字段里的编码方式。
单 bigint 键:objsubid = 1
classid = 键的高 32 位(无符号)
objid = 键的低 32 位(无符号)
双 int 键:objsubid = 2
classid = 第一个 int 键
objid = 第二个 int 键还原单个 64 位键的表达式(官方 pg_locks 文档给出):
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) |
用它们生成锁号:
-- 塞进单 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 决定:
可用锁槽位总数 ≈ max_locks_per_transaction × (max_connections + max_prepared_transactions + 后台进程数)max_locks_per_transaction 默认值是 64。这意味着单个事务能同时持有的锁数量是有上限的——如果你在一个大事务里一次性申请成千上万把顾问锁,可能撞上:
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 个函数,按「范围 × 独占/共享 × 阻塞/非阻塞」组合:
会话级(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最简示例
-- 1. 申请一把会话级独占锁
SELECT pg_advisory_lock(9527); pg_advisory_lock
------------------
(1 row)-- 2. 同一会话再次申请同一把锁(可重入,计数 +1)
SELECT pg_advisory_lock(9527); pg_advisory_lock
------------------
(1 row)-- 3. 必须解锁同样的次数才能彻底释放
SELECT pg_advisory_unlock(9527);
SELECT pg_advisory_unlock(9527); pg_advisory_unlock
--------------------
t
(1 row)
pg_advisory_unlock
--------------------
t
(1 row)-- 4. 非阻塞版本:拿不到立刻返回 false,不等待
SELECT pg_try_advisory_lock(9527); pg_try_advisory_lock
----------------------
t
(1 row)查询当前所有顾问锁
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; 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 种模式不同,顾问锁只有两档:
| 模式 | 获取方式 | 冲突规则 |
|---|---|---|
ExclusiveLock | pg_advisory_lock*(默认) | 与任何其他持有者冲突 |
ShareLock | pg_advisory_lock_shared* | 与其他共享锁兼容,与独占锁冲突 |
-- 会话 A:拿共享锁
SELECT pg_advisory_lock_shared(100);
-- 会话 B:也能拿到同一 key 的共享锁(不冲突)
SELECT pg_advisory_lock_shared(100);-- 会话 C:申请独占锁,会被阻塞
SELECT pg_advisory_lock(100);
-- 一直等待,直到 A 和 B 都释放共享/独占的典型用途是「读写锁」:多个读者并发读缓存,写者独占重建缓存。
可重入:加锁是计数,不是布尔
同一会话可以重复申请同一把顾问锁,不会自我死锁。内部维护一个计数器:
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(不报错) pg_advisory_unlock
--------------------
f
(1 row)pg_advisory_unlock 在没持有该锁时返回 false 而非报错——这一点常被用来做「我到底有没有持有」的探测,但更稳妥的方式是查 pg_locks。
阻塞时受 lock_timeout 约束
顾问锁的阻塞等待会计入 lock_timeout(默认 0,即无限等待):
SET lock_timeout = '2s';
SELECT pg_advisory_lock(42);ERROR: canceling statement due to lock timeout这比无限阻塞好得多。但会话级顾问锁有一个已知竞态:超时取消与锁授予可能同时发生,导致「语句报超时错误,但锁其实已经被本会话持有」。社区讨论中 Tom Lane 明确指出这一点,并建议报错后通过 pg_locks 复核状态:
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 等)复用连接,于是出现这样的灾难链条:
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:
BEGIN;
SELECT pg_advisory_xact_lock(42);
SELECT pg_advisory_unlock(42); -- 返回 f,无效
COMMIT; -- 这时才真正释放 pg_advisory_unlock
--------------------
f
(1 row)正确姿势是:拿到锁 → 干活 → COMMIT(或 ROLLBACK),释放全自动。
顾问锁不跨数据库
pg_locks 里顾问锁的 database 列有实际值,说明它的作用域是单个数据库:
-- 在 db_a 里
SELECT pg_advisory_lock(42);
-- 在 db_b 里(另一个连接)
SELECT pg_try_advisory_lock(42);
-- t ← 毫不冲突如果你的应用在同一实例上有多个数据库,并期望「全局唯一」,这个行为会直接破坏你的假设。要跨库互斥,只能选一个「代表库」统一在里面加锁。
哈希函数生成锁号的取舍
-- 方案一:整数业务键,直接当锁号(推荐,无碰撞)
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 定位「谁把锁占了」
最常见的排查需求:
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; 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
每申请一次(含共享锁)都占一行:
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; count
-------
2
(1 row)排查容量问题(out of shared memory)时,这个计数很有用——说明是谁把锁槽位吃光了。
实战示例
场景:确认某个键当前是否被占用(不产生副作用)
-- 用一个「探测键」去试,拿到立刻还
SELECT pg_try_advisory_lock(9527) AS got_it; got_it
--------
f
(1 row)如果返回 t,说明此刻没人占;但你拿到之后必须立刻释放,否则你自己就成了占用者:
SELECT pg_advisory_unlock(9527);更干净的做法是直接查 pg_locks,不扰动锁状态:
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;场景:观察阻塞与等待
开三个终端,按顺序执行:
-- 终端 1
SELECT pg_advisory_lock(8801);
-- 保持不释放
-- 终端 2
SET lock_timeout = '5s';
SELECT pg_advisory_lock(8801);ERROR: canceling statement due to lock timeout同时在终端 3 观察等待者:
SELECT pid, mode, granted, waitstart
FROM pg_locks
WHERE locktype = 'advisory' AND NOT granted; pid | mode | granted | waitstart
-------+---------------+---------+----------------------------
18740 | ExclusiveLock | f | 2026-09-28 10:12:33.417+08
(1 row)waitstart 告诉你它等了多久。这是区分「卡住」与「正常等待」的关键字段。
场景:安全地释放一个会话的全部顾问锁
当某个连接疑似泄漏了多把锁,又不想杀掉连接(可能承载其他业务)时:
-- 先看清楚这个连接持有哪些顾问锁
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;-- 然后在该连接自己的会话里(而不是从别处)执行
SELECT pg_advisory_unlock_all(); pg_advisory_unlock_all
------------------------
(1 row)pg_advisory_unlock_all 只作用于当前会话
你无法从会话 B 释放会话 A 的顾问锁——除非把 A 杀掉(pg_terminate_backend)。这也是顾问锁「按会话归属」的直接体现。
场景:确认锁号是否落在正确的键空间
-- 用双键形态申请
SELECT pg_advisory_lock(1, 9527);
-- 查它的编码:objsubid 应为 2
SELECT classid, objid, objsubid, mode
FROM pg_locks
WHERE locktype = 'advisory' AND pid = pg_backend_pid(); classid | objid | objsubid | mode
---------+-------+----------+---------------
1 | 9527 | 2 | ExclusiveLock
(1 row)对照验证:如果同一时刻另有一个会话持有 pg_advisory_lock(9527)(单 bigint),它会是 classid = 0, objid = 9527, objsubid = 1,两把锁互不阻塞。
SELECT pg_advisory_unlock(1, 9527);场景:一次性看清键空间与模式的分布
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; 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 系列的完整函数族、可重入计数、会话生命周期与连接池下的泄漏防范。