PostgreSQL select 更新锁,新行
PostgreSQL select for update lock, new rows
我有以下并发用例:可以随时调用端点并且应该发生操作。在伪代码中操作是这样的(当前隔离级别是READ COMMITTED
):
SELECT * FROM TABLE_A WHERE IS_LATEST=true FOR UPDATE
// DO SOME APP LOGIC TO TEST VALIDITY
// ALL GOES WELL => INSERT OR UPDATE NEW ROW WITH IS_LATEST=TRUE => COMMIT
// OTHERWISE => ROLLBACK (all good not interesting)
现在,如果这些操作中的两个在更新方面同时开始,那么 SELECT FOR UPDATE
的这种方法就可以了。因为两个事务看到相同的行数,一个将更新行,第二个事务将等待轮到它才能 SELECT FOR UPDATE
并且状态有效。
我遇到的问题是当我在第一个事务中插入时。发生的情况是,例如,当第一个事务锁定 SELECT FOR UPDATE
时,有两行,然后事务继续,在事务中间,第二个事务进来想要 SELECT FOR UPDATE
(最新)并等待第一个事务完成。第一个事务完成并且数据库中实际上有一个新的 third 项目,但是第二个事务在等待时只拾取了两行行锁被释放。 (这是因为在调用 SELECT FOR UPDATE
时快照不同,只有两行匹配 IS_LATEST=true
)。
有没有一种方法可以让 SELECT
锁在等待后获取最新的快照?
使用你的方法,第二个事务中的查询在锁消失后将 return 一个空结果,因为它在有问题的行上看到 is_latest = FALSE
,而新行不是却可见。所以在这种情况下您将不得不重试交易。
我建议您改用 REPEATABLE READ
隔离级别和乐观锁定:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM table_a WHERE is_latest; -- no locks!
/* perform your application ruminations */
UPDATE table_a SET is_latest = FALSE WHERE id = <id you found above>;
INSERT INTO table_a (is_latest, ...) VALUES (TRUE, ...);
COMMIT;
那么可能会发生三种情况:
您的查询找到一行,交易成功。
您的查询没有找到任何行,那么您可以插入第一行。
查询找到一行,但更新该行导致序列化错误。
在那种情况下,您知道并发事务受到干扰,并且您重复完整的事务作为响应。
问题是每个命令只能看到在查询开始之前已经提交的行。有多种可能的解决方案...
更严格的隔离级别
您可以使用更严格的隔离级别来解决这个问题,但这相对昂贵。
为此。
只需启动一个新命令
保持(便宜的)默认隔离级别 READ COMMITTED
,只需启动一个新命令。
只有几行要锁定
虽然只锁定了一整行,但最简单的解决方案是重复相同的 SELECT ... FOR UPDATE
。第二次迭代看到新提交的行并额外锁定它们。
理论上存在竞争条件,附加事务可能会在等待事务之前锁定新行。这将导致僵局。极不可能,但可以绝对确定,以一致的顺序锁定行:
BEGIN; -- default READ COMMITTED
SELECT FROM table_a WHERE is_latest ORDER BY id FOR UPDATE; -- consistent order
SELECT * FROM table_a WHERE is_latest ORDER BY id FOR UPDATE; -- just repeat !!
-- DO SOME APP LOGIC TO TEST VALIDITY
-- pseudo-code
IF all_good
UPDATE table_a SET is_latest = true WHERE ...;
INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
COMMIT;
ELSE
ROLLBACK;
END;
(id) WHERE is_latest
上的部分索引是理想的。
更多行要锁定
为了超过一手牌,我会创建一个专用的 one-row 代币 table。 bullet-proof 实现可能如下所示,运行 作为管理员或超级用户:
CREATE TABLE public.single_task_x (just_me bool CHECK (just_me) PRIMARY KEY DEFAULT true);
INSERT INTO public.single_task_x VALUES (true);
REVOKE ALL ON public.single_task_x FROM public;
GRANT SELECT, UPDATE ON public.single_task_x TO public; -- or just to those who need it
参见:
- How to allow only one row for a table?
然后:
BEGIN; -- default READ COMMITTED
SELECT FROM public.single_task_x FOR UPDATE;
SELECT * FROM table_a WHERE is_latest; -- FOR UPDATE? ①
-- DO SOME APP LOGIC TO TEST VALIDITY
-- pseudo-code
IF all_good
ROLLBACK;
ELSE
UPDATE table_a SET is_latest = true WHERE ...;
INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
COMMIT;
END;
单把锁更便宜
① 您可能想要也可能不想额外锁定,以防御 other 写入,可能使用较弱的锁定 ....
无论哪种方式,所有锁都会在事务结束时自动释放。
咨询锁
或使用 advisory lock。 pg_advisory_xact_lock()
在交易期间持续存在:
BEGIN; -- default READ COMMITTED
SELECT pg_advisory_xact_lock(123);
SELECT * FROM table_a WHERE is_latest;
-- do stuff
COMMIT; -- or ROLLBACK;
确保为您的特定任务使用唯一标记。 123
在我的例子中。如果您有许多不同的任务,请考虑 look-up table。
要在不同的时间点(不是事务结束时)释放锁,请考虑使用 pg_advisory_lock()
的 session-level 锁。然后您可以(并且必须)使用 pg_advisory_unlock()
手动解锁 - 或关闭会话。
这两个等待锁定资源。有 alternative functions 返回 false
而不是等待 ...
我有以下并发用例:可以随时调用端点并且应该发生操作。在伪代码中操作是这样的(当前隔离级别是READ COMMITTED
):
SELECT * FROM TABLE_A WHERE IS_LATEST=true FOR UPDATE
// DO SOME APP LOGIC TO TEST VALIDITY
// ALL GOES WELL => INSERT OR UPDATE NEW ROW WITH IS_LATEST=TRUE => COMMIT
// OTHERWISE => ROLLBACK (all good not interesting)
现在,如果这些操作中的两个在更新方面同时开始,那么 SELECT FOR UPDATE
的这种方法就可以了。因为两个事务看到相同的行数,一个将更新行,第二个事务将等待轮到它才能 SELECT FOR UPDATE
并且状态有效。
我遇到的问题是当我在第一个事务中插入时。发生的情况是,例如,当第一个事务锁定 SELECT FOR UPDATE
时,有两行,然后事务继续,在事务中间,第二个事务进来想要 SELECT FOR UPDATE
(最新)并等待第一个事务完成。第一个事务完成并且数据库中实际上有一个新的 third 项目,但是第二个事务在等待时只拾取了两行行锁被释放。 (这是因为在调用 SELECT FOR UPDATE
时快照不同,只有两行匹配 IS_LATEST=true
)。
有没有一种方法可以让 SELECT
锁在等待后获取最新的快照?
使用你的方法,第二个事务中的查询在锁消失后将 return 一个空结果,因为它在有问题的行上看到 is_latest = FALSE
,而新行不是却可见。所以在这种情况下您将不得不重试交易。
我建议您改用 REPEATABLE READ
隔离级别和乐观锁定:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM table_a WHERE is_latest; -- no locks!
/* perform your application ruminations */
UPDATE table_a SET is_latest = FALSE WHERE id = <id you found above>;
INSERT INTO table_a (is_latest, ...) VALUES (TRUE, ...);
COMMIT;
那么可能会发生三种情况:
您的查询找到一行,交易成功。
您的查询没有找到任何行,那么您可以插入第一行。
查询找到一行,但更新该行导致序列化错误。
在那种情况下,您知道并发事务受到干扰,并且您重复完整的事务作为响应。
问题是每个命令只能看到在查询开始之前已经提交的行。有多种可能的解决方案...
更严格的隔离级别
您可以使用更严格的隔离级别来解决这个问题,但这相对昂贵。
只需启动一个新命令
保持(便宜的)默认隔离级别 READ COMMITTED
,只需启动一个新命令。
只有几行要锁定
虽然只锁定了一整行,但最简单的解决方案是重复相同的 SELECT ... FOR UPDATE
。第二次迭代看到新提交的行并额外锁定它们。
理论上存在竞争条件,附加事务可能会在等待事务之前锁定新行。这将导致僵局。极不可能,但可以绝对确定,以一致的顺序锁定行:
BEGIN; -- default READ COMMITTED
SELECT FROM table_a WHERE is_latest ORDER BY id FOR UPDATE; -- consistent order
SELECT * FROM table_a WHERE is_latest ORDER BY id FOR UPDATE; -- just repeat !!
-- DO SOME APP LOGIC TO TEST VALIDITY
-- pseudo-code
IF all_good
UPDATE table_a SET is_latest = true WHERE ...;
INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
COMMIT;
ELSE
ROLLBACK;
END;
(id) WHERE is_latest
上的部分索引是理想的。
更多行要锁定
为了超过一手牌,我会创建一个专用的 one-row 代币 table。 bullet-proof 实现可能如下所示,运行 作为管理员或超级用户:
CREATE TABLE public.single_task_x (just_me bool CHECK (just_me) PRIMARY KEY DEFAULT true);
INSERT INTO public.single_task_x VALUES (true);
REVOKE ALL ON public.single_task_x FROM public;
GRANT SELECT, UPDATE ON public.single_task_x TO public; -- or just to those who need it
参见:
- How to allow only one row for a table?
然后:
BEGIN; -- default READ COMMITTED
SELECT FROM public.single_task_x FOR UPDATE;
SELECT * FROM table_a WHERE is_latest; -- FOR UPDATE? ①
-- DO SOME APP LOGIC TO TEST VALIDITY
-- pseudo-code
IF all_good
ROLLBACK;
ELSE
UPDATE table_a SET is_latest = true WHERE ...;
INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
COMMIT;
END;
单把锁更便宜
① 您可能想要也可能不想额外锁定,以防御 other 写入,可能使用较弱的锁定 ....
无论哪种方式,所有锁都会在事务结束时自动释放。
咨询锁
或使用 advisory lock。 pg_advisory_xact_lock()
在交易期间持续存在:
BEGIN; -- default READ COMMITTED
SELECT pg_advisory_xact_lock(123);
SELECT * FROM table_a WHERE is_latest;
-- do stuff
COMMIT; -- or ROLLBACK;
确保为您的特定任务使用唯一标记。 123
在我的例子中。如果您有许多不同的任务,请考虑 look-up table。
要在不同的时间点(不是事务结束时)释放锁,请考虑使用 pg_advisory_lock()
的 session-level 锁。然后您可以(并且必须)使用 pg_advisory_unlock()
手动解锁 - 或关闭会话。
这两个等待锁定资源。有 alternative functions 返回 false
而不是等待 ...