之前已经学习过表级锁和行级锁,但PostgreSQL还有一种锁叫咨询锁(advisory lock)。
咨询锁和使用场景
PostgreSQL提供了应用程序创建锁的方法,以控制并发访问表中的数据。咨询锁的一般用途,我在网上查过,有这么几个场景可以考虑。
在微服务架构中,应用程序在进行 API调用时需要与多个服务关联交易。 后台程序的并发控制。举例来说,我们希望计算数据并向一些领导发送report,但我们必须保证后台开发人员不会同时开始计算。 多结点任务调度器可以使用咨询锁来协调任务分配给工作程序,或者是指定到不同的PostgreSQL分片中。 秒杀场景,这个案例主要来自于德哥。与 for update no wait相比,在秒杀场景中咨询锁的效率更高。
咨询锁的类型
咨询锁主要分为两类,一类是专用咨询锁,另一类是共享咨询锁。下面的图片来自:https://vladmihalcea.com/how-do-postgresql-advisory-locks-work/
「专用咨询锁」:如图所示,目前有两个用户 Alice和 Bob, Alice申请了专用咨询锁,当 Bob想要使用共享锁获得它时,就会被阻塞。在Alice释放之前, Bob无法获取共享锁。这就是说,专用咨询锁是独占的,在独占期间任何人都无法访问,那怕是读也不行。因此我们可以通过咨询锁实现读的锁定。

「共享咨询锁」,如图所示,目前有3个用户 Alice、 Bob和 Carol,Alice最先申请了一个共享咨询锁, Carol可以继续申请相同的共享咨询锁。如果 Bob现在想申请一个专用的咨询锁,那么就需要等待 Alice和 Bob将该共享咨询锁释放,否则它会一直阻塞。

咨询锁的范围
咨询锁有两个范围,会话级和事务级。会话级锁不与任何数据库事务绑定,一旦获得锁,应用程序开发人员需要明确地释放它。一个事务级的咨询锁绑定到当前正在执行的事务上,因此这些锁定会在事务结束时被释放,无论是提交还是回滚。
官方文档的关于咨询锁的函数非常多,如下所示。
「Table 9-73. Advisory Lock Functions」
Name | Return Type | Description |
---|---|---|
pg_advisory_lock(key bigint) | void | Obtain exclusive session level advisory lock |
pg_advisory_lock(key1 int, key2 int) | void | Obtain exclusive session level advisory lock |
pg_advisory_lock_shared(key bigint) | void | Obtain shared session level advisory lock |
pg_advisory_lock_shared(key1 int, key2 int) | void | Obtain shared session level advisory lock |
pg_advisory_unlock(key bigint) | boolean | Release an exclusive session level advisory lock |
pg_advisory_unlock(key1 int, key2 int) | boolean | Release an exclusive session level advisory lock |
pg_advisory_unlock_all() | void | Release all session level advisory locks held by the current session |
pg_advisory_unlock_shared(key bigint) | boolean | Release a shared session level advisory lock |
pg_advisory_unlock_shared(key1 int, key2 int) | boolean | Release a shared session level advisory lock |
pg_advisory_xact_lock(key bigint) | void | Obtain exclusive transaction level advisory lock |
pg_advisory_xact_lock(key1 int, key2 int) | void | Obtain exclusive transaction level advisory lock |
pg_advisory_xact_lock_shared(key bigint) | void | Obtain shared transaction level advisory lock |
pg_advisory_xact_lock_shared(key1 int, key2 int) | void | Obtain shared transaction level advisory lock |
pg_try_advisory_lock(key bigint) | boolean | Obtain exclusive session level advisory lock if available |
pg_try_advisory_lock(key1 int, key2 int) | boolean | Obtain exclusive session level advisory lock if available |
pg_try_advisory_lock_shared(key bigint) | boolean | Obtain shared session level advisory lock if available |
pg_try_advisory_lock_shared(key1 int, key2 int) | boolean | Obtain shared session level advisory lock if available |
pg_try_advisory_xact_lock(key bigint) | boolean | Obtain exclusive transaction level advisory lock if available |
pg_try_advisory_xact_lock(key1 int, key2 int) | boolean | Obtain exclusive transaction level advisory lock if available |
pg_try_advisory_xact_lock_shared(key bigint) | boolean | Obtain shared transaction level advisory lock if available |
pg_try_advisory_xact_lock_shared(key1 int, key2 int) | boolean | Obtain shared transaction level advisory lock if available |
实际上仔细看,锁定模式主要有三种, Acquire、Release和 Try。获得锁,释放锁,尝试锁定。每个函数有两种形式,一种是单参数版本,另一种是双参数版本。单参数值为bigint类型,多参数值为两个 int类型。
咨询锁示例
「那么问题就来了,咨询锁是否锁定了数字呢??」
是啊,它确实锁定了一个数字。让我们看一个案例。
--会话1
postgres=# BEGIN; SELECT pg_try_advisory_xact_lock(1);
BEGIN
pg_try_advisory_xact_lock
---------------------------
t
(1 row)
Session 1执行函数,试图获得锁定,发现可以锁定,返回 true,并锁定该数字。现在,您可以在pg_locks视图中看到相关锁。
postgres=# SELECT clock_timestamp(),pg_locks.locktype,pg_locks.relation,pg_locks.pid, pg_locks.mode, pg_locks.granted from pg_locks where locktype='advisory';
clock_timestamp | locktype | relation | pid | mode | granted
-------------------------------+----------+----------+------+---------------+---------
2021-01-26 14:47:22.251143+00 | advisory | | 2706 | ExclusiveLock | t
这时Session 2也想尝试锁定该数字,发现锁定不了,立即返回 flase。实际上,这一作用与我们的 for update no wait相似,只是它的效率高很多。所以适合当作秒杀来替换for update no wait操作。
postgres=# BEGIN; SELECT pg_try_advisory_xact_lock(1);
BEGIN
pg_try_advisory_xact_lock
---------------------------
f
(1 row)
假如Session 2要强行锁定,就会一直卡住。
postgres=# BEGIN; SELECT pg_advisory_xact_lock(1);
BEGIN
此时,可以在pg_locks视图时看到两个ExclusiveLock锁定的记录,其中一个已获得锁定,而另一个正在等待。
postgres=# SELECT clock_timestamp(),pg_locks.locktype,pg_locks.relation,pg_locks.pid, pg_locks.mode, pg_locks.granted from pg_locks where locktype='advisory';
clock_timestamp | locktype | relation | pid | mode | granted
-------------------------------+----------+----------+------+---------------+---------
2021-01-26 14:53:59.739354+00 | advisory | | 2708 | ExclusiveLock | f
2021-01-26 14:53:59.739359+00 | advisory | | 2712 | ExclusiveLock | t
后记
其实咨询锁的用处很大,但一般开发人员不知道如何使用它,所以还是需要我们 DBA进行大力宣传,至少我觉得可以用把for update no wait替换掉。
参考链接
1.https://www.postgresql.org/docs/9.5/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS-TABLE
2.https://vladmihalcea.com/how-do-postgresql-advisory-locks-work/