基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

PostgreSQL中的咨询锁(Advisory Locks)

知识点图片

在 PostgreSQL 中,咨询锁(Advisory Locks) 是一种非常强大且独特的功能。它是一种由应用程序控制其含义的锁,数据库本身并不关心你用这个锁来保护什么数据或逻辑。

简而言之:普通的行锁/表锁是为了保护数据库内部数据的完整性;而咨询锁是为了帮助应用程序做并发控制(例如实现分布式锁)


1. 咨询锁的核心特点

  • 应用层定义含义:数据库只提供“锁”的机制(获取、阻塞、释放),至于这个锁代表“不能同时发送邮件”还是“不能同时处理同一个订单”,完全由你的代码决定。
  • 基于数字 ID:咨询锁的标识符不是表名或行 ID,而是一个 64 位的整数,或者一对 32 位的整数
  • 不影响 MVCC 和表锁:咨询锁不会阻塞普通的 SELECTUPDATEINSERT,也不会导致表膨胀。它们存在于共享内存中,速度非常快。
  • 无死锁检测限制:虽然咨询锁也会参与 PostgreSQL 的死锁检测,但它们不会与普通的数据库对象锁产生交叉干扰。

2. 咨询锁的分类

咨询锁按照生命周期获取方式分为多个维度:

A. 按生命周期分类

  1. 会话级(Session-level)
    • 锁属于当前数据库连接(Session)。
    • 获取后,直到显式调用释放函数,或者断开数据库连接时才会释放。
    • 哪怕事务回滚(Rollback),会话级锁也不会被释放。
  2. 事务级(Transaction-level)
    • 锁属于当前事务。
    • 不需要(也没有)显式释放函数。当事务提交(COMMIT)或回滚(ROLLBACK)时,锁会自动释放。
    • 非常适合与数据库事务绑定的业务逻辑。

B. 按行为分类

  1. 阻塞型(Blocking):如果锁被别人占用了,当前请求会一直等待(阻塞),直到锁被释放。
  2. 非阻塞/尝试型(Try):如果锁被占用了,立刻返回 false;如果获取成功,返回 true。不会等待。

C. 按排他性分类

  1. 排他锁(Exclusive):同一时刻只能有一个人持有。
  2. 共享锁(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. 总结与注意事项

优点

  1. 零运维成本:不需要额外引入 Redis/Zookeeper 组件。
  2. 安全可靠:如果客户端崩溃或网络断开,PG 会自动回收 Session,锁也会自动释放,不会产生死锁导致永久锁死(类似于 Redis 的自动过期,但更完美,因为它是基于 TCP 连接状态的)。
  3. 性能极高:纯内存操作,不落盘。

缺点/注意

  1. 哈希冲突:如果用 hashtext() 把字符串转数字,理论上极小概率会发生哈希碰撞,导致两个不同的业务抢了同一把锁。
  2. PgBouncer 连接池兼容性:如果你使用了 PgBouncer 的事务模式(Transaction mode)绝对不要使用会话级咨询锁pg_advisory_lock),因为你的前后两个 SQL 可能被分配到不同的后端连接上。在这种架构下,只能使用事务级咨询锁pg_advisory_xact_lock)。
00:00
00:00