如何在函数内部强制 COMMIT 以便其他会话可以看到更新的行?

Kof*_*fiB 9 postgresql stored-procedures transactions plpgsql task-queue

在 Postgres 12 数据库中,我在一个函数中有多个查询(SELECTUPDATE、 ...),总共需要大约 20 分钟才能完成。我在顶部有一个检查,它执行UPDATEifstatus未运行:

create or replace function aaa.fnc_work() returns varchar as 
$body$
    begin
        if (select count(*) from aaa.monitor where id='invoicing' and status='running')=0 then
           return 'running';
        else
           update aaa.monitor set status='running' where id='invoicing';
        end if;
        --- rest of code ---
        --finally
        update aaa.monitor set status='idle' where id='invoicing';
        return '';
    exception when others then
         return SQLERRM::varchar;
    end
$body$
language plpgsql;
Run Code Online (Sandbox Code Playgroud)

这个想法是为了防止其他用户执行直到--- rest of code ---空闲status

然而,其他人(调用相同的函数)似乎看不到更新的状态,他们也继续并开始执行--- rest of code ---。如何在以下情况后强制提交:

更新 aaa.monitor 设置 status='running' where id='invoicing'

这样所有其他用户会话都可以看到更新status并相应退出。

我需要交易吗?

Erw*_*ter 16

继续阅读。我把最好的留到最后。

\n

概念证明PROCEDURE

\n

PostgresFUNCTION始终是原子的(在单个事务包装器内运行)并且无法处理事务。所以COMMIT是不允许的。您可以使用技巧来dblink解决这个问题。看:

\n\n

但对于像这样的嵌套事务,请考虑使用 aPROCEDURE代替。随Postgres 11引入。您可以在那里管理交易:

\n
CREATE OR REPLACE PROCEDURE aaa.proc_work(_id text, INOUT _result text = NULL)\n  LANGUAGE plpgsql AS\n$proc$\nBEGIN\n   -- optionally assert steering row exists\n   PERFORM FROM aaa.monitor WHERE id = _id FOR KEY SHARE SKIP LOCKED;\n\n   IF NOT FOUND THEN   \n      RAISE EXCEPTION \'monitor.id = % not found or blocked!\', quote_literal(_id);\n   END IF;\n\n   -- try UPDATE\n   UPDATE aaa.monitor\n   SET    status = \'running\'\n   WHERE  id = _id                   -- assuming valid _id\n   AND    status <> \'running\';       -- assuming "status" is NOT NULL\n\n   IF NOT FOUND THEN\n      _result := \'running\'; RETURN;  -- this is how you return with INOUT params\n   END IF;\n\n   COMMIT;                           -- HERE !!!\n\n   BEGIN                             -- start new code block\n\n      ----- code for big work HERE -----\n      -- PERFORM 1/0;                -- debug: test exception?\n      -- PERFORM pg_sleep(5);        -- debug: test concurrency?\n\n      _result := \'\';\n\n   -- also catching QUERY_CANCELED and ASSERT_FAILURE\n   -- is a radical step to try and release \'running\' rows no matter what\n   EXCEPTION WHEN OTHERS OR QUERY_CANCELED OR ASSERT_FAILURE THEN\n      -- ROLLBACK;                   -- roll back (unfinished?) big work\n      _result := SQLERRM;\n   END;                              -- end of nested block\n\n   UPDATE aaa.monitor                -- final reset\n   SET    status = \'idle\'\n   WHERE  id = _id\n   AND    status <> \'idle\';          -- only if needed\nEND\n$proc$;\n
Run Code Online (Sandbox Code Playgroud)\n

致电(重要!):

\n
CALL aaa.proc_work(\'invoicing\');  -- stand-alone call!\n
Run Code Online (Sandbox Code Playgroud)\n

重要笔记

\n

添加COMMITUPDATE. 之后,并发事务就可以看到更新的行。

\n

但没有额外的BEGINSTART TRANSACTION手册:

\n
\n

在命令调用的过程CALL以及匿名代码块(DO命令)中,可以使用命令COMMIT和结束事务ROLLBACK。使用这些命令结束事务后,\n会自动启动新事务,因此\n没有单独的START TRANSACTION命令。(请注意,BEGIN和\nEND在 PL/pgSQL 中具有不同的含义。)

\n
\n

我们需要一个单独的 PL/pgSQL代码块,因为您有一个自定义异常处理程序,并且(引用手册):

\n
\n

事务无法在带有异常处理程序的块内结束。

\n
\n

(但我们可以在处理程序中COMMIT/ 。)ROLLBACKEXCEPTION

\n

您不能在外部事务内部调用此过程,也不能与任何其他 DML 语句一起调用,否则会强制使用外部事务包装器。必须是独立的CALL。看:

\n\n

注意最后的UPDATE aaa.monitor SET status = \'idle\' WHERE .... 否则(已提交!)status将在异常后无限期地保持“运行”。

\n

关于从过程返回值:

\n\n

我添加DEFAULT NULL了参数INOUT,因此您不必在调用时提供参数。

\n

UPDATE直接地。如果该行正在“运行”,则不会发生更新。(这也修复了逻辑:当没有找到任何行时,您的IF表达式似乎向后返回“正在运行” 。似乎您想要相反的情况。)status=\'running\'

\n

我添加了一个(可选!)断言以确保表中的行aaa.monitor存在。添加FOR KEY SHARE锁还可以消除断言和后续之间的竞争条件的微小时间窗口UPDATE。锁与删除或更新 PK 列冲突,但更新status. 所以在正常操作中永远不会引发异常!手册:

\n
\n

目前,针对该情况考虑的列集UPDATE是\n那些具有可在外键中使用的唯一索引的列(因此不考虑部分索引和表达式索引),\n但这在将来可能会发生变化。

\n
\n

SKIP LOCK在锁冲突的情况下不等待。添加的异常永远不应该发生。只是展示一个无懈可击的概念证明。

\n

您的更新显示了 25 行aaa.monitor,因此我添加了参数_id

\n

优越的方法

\n

上述内容对于保留更多信息以供全世界查看可能是有意义的。对于队列操作,还有更有效的解决方案。而是使用,其他人立即“可见”。那么你不需要一个嵌套事务来开始,一个简单的事务FUNCTION就可以了:

\n
CALL aaa.proc_work(\'invoicing\');  -- stand-alone call!\n
Run Code Online (Sandbox Code Playgroud)\n

称呼:

\n
SELECT aaa.fnc_work(\'invoicing\');\n
Run Code Online (Sandbox Code Playgroud)\n

该调用可以以任何您想要的方式嵌套。只要一项事务正在完成这项重大工作,其他事务就不会启动。

\n

同样,可选的断言会取出一个FOR KEY SHARE锁来消除竞争条件的时间窗口,并且添加的异常在正常操作中永远不应该发生。

\n

status我们根本不需要这个列。行锁本身就是看门人。SELECT因此中的空列表PERFORM FROM aaa.monitor ...。附带好处:这也不会通过来回更新行而产生死元组。如果由于其他原因您仍然需要更新status,那么您又回到了上一章的可见性问题。您可以将两者结合起来...

\n

关于PERFORM

\n\n

关于行锁:

\n\n