
PostgreSQL中咨询锁与行级锁、事务隔离级的相关应用
1. 前言
PostgreSQL有一种锁叫咨询锁(advisory lock)。它在实际应用中提供了表级锁和行级锁不一样的体验。提供了更细粒度更高效的控制。应用开发人员如果不能好好利用这一功能,就有些遗憾了。
下边是有关咨询锁的一些函数定义:(直接搬自官方文档)
| 函数名 | 返回值 | 基本描述 |
|---|---|---|
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类型。
2. 分析实作
我们先准备一张很简单的表:test_advisory。初始化如下:
create table test_advisory (id int, processed boolean, col3 varchar(32));
insert into test_advisory values (1, false, 'test1');
insert into test_advisory values (2, false, 'test2');
insert into test_advisory values (3, false, 'test3');
insert into test_advisory values (4, false, 'test4');
2.1 不可重复读
在RC (read committed)默认隔离级下,在同一事务下边,可能两次读取的结果是不一样的。即出现不可重复读的现象。
T1: BEGIN;
T2: BEGIN;
T1:
select * from test_advisory where id =3 ;
id | processed | col3
----+-----------+-------
3 | f | test3
(1 row)
T2: -- 做一次update操作,然后提交
update test_advisory set col3 = 'test3_updated' where id = 3;
UPDATE 1
commit;
COMMIT
T1: 同一事务内再次查询, 发现col3值发生了变化 。
select * from test_advisory where id =3 ;
id | processed | col3
----+-----------+---------------
3 | f | test3_updated
(1 row)
commit;
COMMIT
这就是RC隔离级下边出现的不可重复读现象。如果在你的应用逻辑中,基于第一次读的结果 ,再做一些逻辑处理,可能最后出现的结果就不是业务逻辑当中想要的。要想读取尽量保持一致,可以加上行锁。
2.2 SELECT … FOR UPDATE的行级锁
如前后按下述顺序执行:
T1:
begin; -- 1
select * from test_advisory where id = 3 for update; -- 2
id | processed | col3
----+-----------+-------
3 | f | test3
(1 row)
update test_advisory set col3 = 'test3_session2'; -- 4
UPDATE 4
commit; -- 5
T2:
BEGIN; -- 2
select * from test_advisory where id = 3 for update; -- 3
-- 此处会hang -- 6
id | processed | col3
----+-----------+----------------
3 | f | test3_session2
(1 row)
commit; -- 7
我们看到3处的select操作,并不能立即返回目标行的值。因为它被2里的操作锁定,要等它后续的事务结束才能返回。
2.3 利用咨询锁pg_advisory_xact_lock显式加锁
直接在语句中使用pg_advisory_xact_lock(key),代替 … for update行锁
T1:
BEGIN; -- 1
select pg_advisory_xact_lock(3); select * from test_advisory where id=3; -- 3
pg_advisory_xact_lock
-----------------------
(1 row)
id | processed | col3
----+-----------+--------
3 | f | test3
(1 row)
update test_advisory set col3 = 'test3'; -- 5
UPDATE 4
commit; -- 6
T2:
BEGIN; -- 2
select pg_advisory_xact_lock(3); select * from test_advisory where id=3; -- 4
-- (hang....)
pg_advisory_xact_lock -- 7
-----------------------
(1 row)
id | processed | col3
----+-----------+-------
3 | f | test3
(1 row)
commit; -- 8
COMMIT
我们能看到T2的整个会话中,最后能拿到结果。因为T1先拿到锁,完成更新。T2最后拿到的是T1更新操作的结果。T2在时间点8之前,甚至可以基于'test3'这个值做进一步update。
2.4 秒杀式场景pg_try_advisory_xact_lock
还有一种特殊的场景,那就是抢占式秒杀。谁第一个抢到那一行,并进行更新,就算完成,而其它所有会话不需要等待它更新完成,而是提前在是否能拿到锁的时候就知道是否抢到。
依然以上边的表为例, T1和T2两个会话去争抢id=3的那一行,并尝试更新其状态。
T1:
BEGIN; -- 1
select * from test_advisory where id = 3 and pg_try_advisory_xact_lock(3); -- 3
id | processed | col3
----+-----------+-------
3 | f | test3
(1 row)
update test_advisory set processed = TRUE where id = 3; -- 5
COMMIT; -- 6
T2:
BEGIN; -- 2
select * from test_advisory where id = 3 and pg_try_advisory_xact_lock(3); -- 4
-- 此处没有hang, 直接返回空结果集
commit; -- 7
这里要说明的是:select * from test_advisory where id = 3 and pg_try_advisory_xact_lock(3); 这条语句,即算有多达N个会话,也只可能有一个会话有结果返回。因为:pg_try_advisory_xact_lock(3);同一时候,最先拿到这个事务咨询锁的,返回的是true,其它事务中的这个调用全都是false。因为它立即返回,所以,不需要等待,可以得到最好的性能。相反,如果使用的是SELECT … FOR UPDATE,虽然最终也能看到人家拿值,但是要一直等到第一个事务提交以后,才能看到结果,花掉了更多的等待时间。在并发量大的情况下,尤其如此。
对相关性能感兴趣的朋友,可以直接用pgbench进行简单的压测对比。
稍等,有人会说:PG不是有:SELECT …. FOR UPDATE NOWAIT的语法吗?嗯,确实有:
请看:
T1:
BEGIN; -- 1
select * from test_advisory where id=3 for update nowait; -- 3
id | processed | col3
----+-----------+-------
3 | t | test3
(1 row)
COMMIT -- 6
T2:
BEGIN; -- 2
select * from test_advisory where id=3 for update nowait; -- 4
ERROR: could not obtain lock on row in relation "test_advisory" -- 此处虽然返回,但是是报错,事务的状态也不对了
ROLLBACK; -- 5
这种处理,应用端就需要捕获错误或异常来处理余下的事务。看起来也不是那么优雅。使用pg_try_advisory_xact_lock来代替它绝对有意义。
3.小结
这里简单的介绍了咨询锁中的pg_advisory_xact_lock (阻塞式)以及pg_try_advisory_xact_lock(非阻塞式)的使用,在一些场合是可以代替SELECT … FOR UPDATE类的行级锁。在秒杀场景更是可以巧妙地运用它来实现相应功能,损耗很小。
只是需要注意一点的是,要很好的构造参数中的key值。它并不针对特定的表。key值相同的话,就有竞争。无论访问的是不是相同的表。所以需要注意。你可以提前将key值布局好,不要在应用代码中滥用就好。
4.参考
[1]https://www.postgresql.org/docs/9.5/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS-TABLE
[2]https://postgres.cz/wiki/PostgreSQL_SQL_Tricks: https://postgres.cz/wiki/PostgreSQL_SQL_Tricks





