建议锁让应用使用一个标识来协调工作,不要求这个标识对应数据库里某一行。比如,多名工作者可以约定,同一时间只能有一个连接刷新某份报表。PostgreSQL 负责管理锁的持有与竞争,应用则负责定义标识的业务含义,以及哪些操作必须先拿锁才能执行。这个分工决定了它能提供的保障,也决定了绕过协议时会发生什么。

坏掉的样子

建议锁的键只能是一个大整数,或者两个普通整数,没有接受文本名字的函数重载。这两种键空间彼此独立。如果应用通过哈希把名称映射为数字,必须明确一致的编码规则,也要考虑碰撞;同一资源被不同算法映射到不同数字,同样会破坏协调。直接维护有文档的整数命名空间,通常比隐藏在各个调用方里的临时哈希规则更容易核对。

建议锁是参与者共同遵守的协议。忽略它的连接,仍然可以更新所谓受保护的表行;它不能代替唯一约束、事务或权限检查。会话锁在回滚后仍然保留,直到对应次数的解锁完成,或服务器结束该会话。事务锁则随事务结束释放。如果处于自动提交模式,一条语句取得的事务锁,在后续语句开始之前就已经释放,无法保护接下来才执行的工作。

快速验证

本轮没有重跑原样保留的双会话示例:单用户后端不能复现会话间竞争。以下结果保留为预期,不是新的并发验证。

准备同一个 PostgreSQL 18 可丢弃数据库,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。为本实验预留下面的整数键,不要与已有业务约定混用。按照块的顺序切换终端。甲先创建计数器:

CREATE SCHEMA advisory_demo;
CREATE TABLE advisory_demo.counter (id integer PRIMARY KEY,n integer);
INSERT INTO advisory_demo.counter VALUES (1,0);

甲传入显式文本类型的键,预期返回 42883,即不存在匹配的函数。故意报错这一段结束后恢复遇错停止设置:

\set ON_ERROR_STOP off
SELECT pg_advisory_lock('job-name'::text);
\echo text_key_state :SQLSTATE
\set ON_ERROR_STOP on

甲在事务里把同一把会话锁取两次,然后回滚。注意函数名使用的是会话级形式:

BEGIN;
SELECT pg_advisory_lock(8610,2);
SELECT pg_advisory_lock(8610,2);
ROLLBACK;

乙尝试非阻塞取锁,接着故意不遵守建议锁协议,直接更新计数器:

SELECT pg_try_advisory_lock(8610,2);
UPDATE advisory_demo.counter SET n=n+1 WHERE id=1 RETURNING n;

取锁返回假,更新却返回一。两个结果分别证明:甲的回滚没有释放会话锁;建议锁也没有自动阻挡普通表写入。甲现在解锁一次:

SELECT pg_advisory_unlock(8610,2);

虽然解锁返回真,乙仍然无法获得同一把锁:

SELECT pg_try_advisory_lock(8610,2);

甲还需要解除第二次获取:

SELECT pg_advisory_unlock(8610,2);

这时乙能够成功取锁,并明确释放自己刚获得的那一次持有:

SELECT pg_try_advisory_lock(8610,2);
SELECT pg_advisory_unlock(8610,2);

两个调用都返回真。同一会话重复成功取锁会累加持有次数,所以一次解锁成功,并不能证明该会话已经完全放弃资源。下一步,甲对另一个键使用事务级函数,并保持显式事务打开:

BEGIN;
SELECT pg_advisory_xact_lock(8610,3);

乙的非阻塞事务锁请求返回假,说明甲仍拥有该键:

SELECT pg_try_advisory_xact_lock(8610,3);

甲回滚,结束事务级持有:

ROLLBACK;

乙在自动提交模式下重新申请,这次返回真:

SELECT pg_try_advisory_xact_lock(8610,3);

紧接着,甲也在自动提交模式下申请相同的事务锁,仍返回真:

SELECT pg_try_advisory_xact_lock(8610,3);

两个真是先后成功,不是同时拥有同一把互斥锁。每条语句的隐式事务结束时,就已释放当次锁。此刻双方都不能认为自己还保护着未来的临界区。最后由甲核对计数值仍是一,并删除实验模式:

SELECT n FROM advisory_demo.counter WHERE id=1;
DROP SCHEMA advisory_demo CASCADE;

非阻塞取锁失败不会增加需要释放的持有次数。只有成功获取,才会成为当前会话或事务实际拥有的锁。

让持有寿命覆盖完整操作

如果被保护的数据库工作可以放在一个事务中,应显式开启事务、获取事务锁,再完成读取、判断和写入,最后结束事务。非阻塞取锁返回假时,意味着调用方没有取得所有权;此时应跳过或重新安排任务,不能无视返回值继续执行受保护工作。阻塞版本可能等待,因此取锁预算和重试策略也必须纳入这个业务操作的总时限。

只有确实需要跨越多次事务的持有周期时,才选择会话锁,而且获取、工作和释放必须使用同一条物理数据库连接。如果持锁后把连接还给池,后台会话可能继续持锁,却开始服务不相关的请求。事务池化尤其不能支持“下次请求仍回到原来那个持锁会话”的假设。这里要核对真实连接寿命,而不是仅看应用对象是否还是同一个名字。

服务器结束会话时会自动清理会话锁,不存在会话已经终止但锁永久残留的正常语义。不过,网络连接丢失的检测可能有延迟,不能假定客户端消失后其他工作者立即获得锁。锁也不记录任务是否已经完成。后续工作者取得所有权后,仍应检查已提交状态,判断是否需要执行,以及重复执行是否符合任务语义,不能把“我现在拿到了锁”误当成“此前无人完成过”。

来源