PostgreSQL中的咨询锁(Advisory Locks)
在 PostgreSQL 中,咨询锁(Advisory Locks) 是一种非常强大且独特的功能。它是一种由应用程序控制其含义的锁,数据库本身并不关心你用这个锁来保护什么数据或逻辑。
简而言之:普通的行锁/表锁是为了保护数据库内部数据的完整性;而咨询锁是为了帮助应用程序做并发控制(例如实现分布式锁)。
1. 咨询锁的核心特点
- 应用层定义含义:数据库只提供“锁”的机制(获取、阻塞、释放),至于这个锁代表“不能同时发送邮件”还是“不能同时处理同一个订单”,完全由你的代码决定。
- 基于数字 ID:咨询锁的标识符不是表名或行 ID,而是一个 64 位的整数,或者一对 32 位的整数。
- 不影响 MVCC 和表锁:咨询锁不会阻塞普通的
SELECT、UPDATE、INSERT,也不会导致表膨胀。它们存在于共享内存中,速度非常快。 - 无死锁检测限制:虽然咨询锁也会参与 PostgreSQL 的死锁检测,但它们不会与普通的数据库对象锁产生交叉干扰。
2. 咨询锁的分类
咨询锁按照生命周期和获取方式分为多个维度:
A. 按生命周期分类
- 会话级(Session-level):
- 锁属于当前数据库连接(Session)。
- 获取后,直到显式调用释放函数,或者断开数据库连接时才会释放。
- 哪怕事务回滚(Rollback),会话级锁也不会被释放。
- 事务级(Transaction-level):
- 锁属于当前事务。
- 不需要(也没有)显式释放函数。当事务提交(COMMIT)或回滚(ROLLBACK)时,锁会自动释放。
- 非常适合与数据库事务绑定的业务逻辑。
B. 按行为分类
- 阻塞型(Blocking):如果锁被别人占用了,当前请求会一直等待(阻塞),直到锁被释放。
- 非阻塞/尝试型(Try):如果锁被占用了,立刻返回
false;如果获取成功,返回true。不会等待。
C. 按排他性分类
- 排他锁(Exclusive):同一时刻只能有一个人持有。
- 共享锁(Shared):多人可以同时持有共享锁,但共享锁与排他锁互斥。
3. 常用的 SQL 函数
| 功能分类 | 函数名 (64位ID示例) | 说明 |
|---|---|---|
| 会话级 - 阻塞 | pg_advisory_lock(id) |
获取排他锁,如果被占用则等待 |
| 会话级 - 尝试 | pg_try_advisory_lock(id) |
尝试获取,成功返回 true,失败返回 false |
| 会话级 - 释放 | pg_advisory_unlock(id) |
释放会话级排他锁 |
| 事务级 - 阻塞 | pg_advisory_xact_lock(id) |
获取事务级排他锁,随事务结束自动释放 |
| 事务级 - 尝试 | pg_try_advisory_xact_lock(id) |
尝试获取事务级排他锁,随事务结束自动释放 |
(注:以上函数均有 _shared 后缀的版本,用于获取共享锁,如 pg_advisory_lock_shared)
4. 常见应用场景
场景一:轻量级分布式锁(替代 Redis/Zookeeper)
如果你的系统已经使用了 PostgreSQL,且并发量不是千万级别的,完全不需要引入 Redis 仅仅为了做一个分布式锁。
- 做法:多个微服务实例连接同一个 PG 库,抢占同一个咨询锁。
- 示例:sql
-- 微服务A尝试获取锁 10086 SELECT pg_try_advisory_lock(10086); -- 返回 true,A 开始执行任务 -- 微服务B尝试获取,返回 false,B 放弃执行
场景二:防止定时任务重复执行(Cron Job 防重)
多台服务器部署了相同的定时任务代码,为了防止同一时间任务被执行多次。
- 做法:在任务开始前,执行
pg_try_advisory_lock(任务ID)。拿到 true 的服务器执行,拿到 false 的跳过。
场景三:避免并发导致的数据重复插入/脏数据
例如,用户快速连击按钮,导致并发发起两次请求。虽然可以用 UNIQUE 索引兜底,但在复杂业务中,可以使用事务级咨询锁排队处理。
- 做法:sql
BEGIN; -- 用用户的 ID 作为锁 ID,阻塞后续请求 SELECT pg_advisory_xact_lock(用户ID); -- 执行业务检查和数据插入... COMMIT; -- 事务结束,自动释放锁,下一个请求进入
5. 实用技巧:如何将字符串转化为锁 ID?
咨询锁的参数是整型(bigint 或两个 int),但业务中我们通常用字符串(如 'order_processing_123')。你可以使用 PG 自带的哈希函数将字符串转为 ID:
转为 64位 bigint (推荐):
sql
SELECT pg_advisory_lock(hashtextextended('order_123', 0));
-- 或者
SELECT pg_advisory_lock(hashtext('order_123')::bigint);
转为两个 32位 int:
有些开发者习惯用两个参数:第一个参数代表“业务模块ID”,第二个参数代表“具体记录ID”:
sql
-- 1001 代表订单模块,8888 代表订单号
SELECT pg_try_advisory_lock(1001, 8888);
6. 如何监控和查看咨询锁?
你可以通过查询 pg_locks 系统视图来查看当前系统中谁持有了咨询锁:
sql
SELECT
pid,
locktype,
objid AS lock_id,
mode,
granted
FROM pg_locks
WHERE locktype = 'advisory';
pid: 持有锁的数据库连接进程 ID。lock_id: 你传入的那个数字 ID。granted:true表示已获得,false表示正在等待。
7. 总结与注意事项
优点:
- 零运维成本:不需要额外引入 Redis/Zookeeper 组件。
- 安全可靠:如果客户端崩溃或网络断开,PG 会自动回收 Session,锁也会自动释放,不会产生死锁导致永久锁死(类似于 Redis 的自动过期,但更完美,因为它是基于 TCP 连接状态的)。
- 性能极高:纯内存操作,不落盘。
缺点/注意:
- 哈希冲突:如果用
hashtext()把字符串转数字,理论上极小概率会发生哈希碰撞,导致两个不同的业务抢了同一把锁。 - PgBouncer 连接池兼容性:如果你使用了 PgBouncer 的事务模式(Transaction mode),绝对不要使用会话级咨询锁(
pg_advisory_lock),因为你的前后两个 SQL 可能被分配到不同的后端连接上。在这种架构下,只能使用事务级咨询锁(pg_advisory_xact_lock)。