PostgreSQL从8.2版本就开始支持advisory lock这一功能。那么什么是advisory lock呢?官方文档的解释是:
PostgreSQL提供了一种方法创建由应用定义其含义的锁。这种锁被称为咨询锁,因为系统并不强迫其使用——而是由应用来保证其正确的使用。
advisory lock分为会话级别和事务级别两种(9.1版本加入事务级别)。事务级别的advisory lock不能显性的释放。advisory lock和系统的MVCC不是一个概念,基本上完全不相干。锁的数量由 max_locks_per_transaction and max_connections 决定。
advisory lock相关的函数:
advisory lock 不受事务的影响,在同一个会话中 advisory lock 可以多次lock,但是必须与unlock成对使用。
可能看了上面的解释你还是会有些云里雾里的感觉,那么我们就看看advisory lock究竟能用在哪些场景下吧!
两个会话,同时对同一个ID值的记录执行删除后插入,有一个会话会出现删除0,插入失败的现象。
建表:
bill@bill=>CREATE TABLE test (id INT PRIMARY KEY);
CREATE TABLE
bill@bill=>INSERT INTO TEST VALUES (1);
INSERT 0 1
bill@bill=>INSERT INTO TEST VALUES (2);
INSERT 0 1
分别在两个会话中执行下列语句:
begin;
1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;
有点经验的人可能立刻就会发现:在会话二中会因为会话一产生锁等待,直到会话一结束就可以执行自己会话中的语句了。事实真的如此吗?
会话一:
bill@bill=>begin;
BEGIN
bill@bill=>DELETE FROM test WHERE id=1;
DELETE 1
bill@bill=>INSERT INTO test VALUES (1);
INSERT 0 1
bill@bill=>commit;
COMMIT
会话二:
bill@bill=>begin;
BEGIN
bill@bill=>DELETE FROM test WHERE id=1;
DELETE 0
bill@bill=>INSERT INTO test VALUES (1);
ERROR: duplicate key value violates unique constraint "test_pkey"
DETAIL: Key (id)=(1) already exists.
可以看到,会话二在会话一提交前确实会一直等待,但是等会话一提交完,会话二中delete语句删除记录数为0,insert插入会报错!那么我们该怎么解决这一问题呢?这种情况下我们就可以使用advisory lock。
会话一:
bill@bill=>begin;
BEGIN
bill@bill=>select pg_try_advisory_xact_lock(1);
pg_try_advisory_xact_lock
---------------------------
t
(1 row)
bill@bill=>delete from test where id =1;
DELETE 1
bill@bill=>insert into test values(1);
INSERT 0 1
bill@bill=>end;
COMMIT
会话二:
bill@bill=>begin;
BEGIN
bill@bill=>select pg_try_advisory_xact_lock(1); --会话一不提交则返回f
pg_try_advisory_xact_lock
---------------------------
f
(1 row)
bill@bill=>select pg_try_advisory_xact_lock(1); --会话一提交后返回t
pg_try_advisory_xact_lock
---------------------------
t
(1 row)
bill@bill=>DELETE FROM test WHERE id=1;
DELETE 1
bill@bill=>INSERT INTO test VALUES (1);
INSERT 0 1
bill@bill=>end;
COMMIT
使用advisory lock消除行锁冲突。
通常在数据库中最小粒度的锁是行锁,当一个事务正在更新某条记录时,另一个事务如果要更新同一条记录(或者申请这一条记录的锁),则必须等待锁释放。
通常持锁的时间需要保持到事务结束,也就是说,如果一个长事务持有了某条记录的锁,其他会话要持有这条记录的锁,可能要等很久。
那么对大表进行全表或者大部分数据的直接更新必然会很慢。
例如:
bill@bill=>create unlogged table t1(id int primary key, info int[]);
CREATE TABLE
bill@bill=>insert into t1 select generate_series(1,10000), (select array_agg(id) from generate_series(1,100000) t(id));
INSERT 0 10000
bill@bill=>\dt+ t1
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+-------+---------+-------------
bill | t1 | table | bill | 3961 MB |
(1 row)
更新:
需要87秒。
bill@bill=>begin;
BEGIN
Time: 0.146 ms
bill@bill=>update t1 set info=array_append(info,1);
UPDATE 10000
Time: 87225.118 ms (01:27.225)
bill@bill=>rollback ;
ROLLBACK
Time: 0.298 ms
使用advisory lock来更新:
使用扫描式的获取advisory lock,保证不会重复获取即可。
create or replace function update() returns void as $$
declare
v_id int;
begin
for v_id in select id from t1 -- 扫描式
loop
if pg_try_advisory_xact_lock(v_id) then -- 获取到ID的LOCK才会实施更新,否则继续扫描
update t1 set info=array_append(info,1) where id=v_id;
end if;
end loop;
end;
$$ language plpgsql strict;
使用32个并行度测试:
vi test.sql
select update();
pgbench -M prepared -n -r -f ./test.sql -c 32 -j 32 -t 1
耗时5s。
pg12@oracle-> pgbench -M prepared -n -r -f ./test.sql -c 32 -j 32 -t 1
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 32
number of threads: 32
number of transactions per client: 1
number of transactions actually processed: 32/32
latency average = 5231.399 ms
tps = 6.116910 (including connections establishing)
tps = 6.135842 (excluding connections establishing)
statement latencies in milliseconds:
5205.401 select update();
使用advisory lock需要注意:adlock的id是库级冲突,例如lock(1),在同一个数据库再lock(1)就会冲突。因此,如果在同一个库里面,期望对不同的表里面的数据使用同样的adlock手段,建议不同的表错开ID段,或者使用全局ID。