暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL中咨询锁与行级锁、事务隔离级的相关应用方法

数据库杂记 2023-12-26
158


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 (1false'test1');
insert into test_advisory values (2false'test2');
insert into test_advisory values (3false'test3');
insert into test_advisory values (4false'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



文章转载自数据库杂记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论