如何在函数内部强制 COMMIT 以便其他会话可以看到更新的行? 带有 PROCEDURE 的概念证明重要说明

问题描述

在 Postgres 12 数据库中,我在一个函数中有多个查询SELECTUPDATE、...),所有这些查询总共需要大约 20 分钟才能完成。 如果 UPDATE 未运行,我会在顶部检查 status

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;

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

但是,似乎其他人(调用相同的函数)看不到更新的状态,他们也继续执行 --- rest of code ---。我如何在之后强制提交:

update aaa.monitor set status='running' where id='invoicing';

以便所有其他用户会话都可以看到更新的 status 并相应地退出

我需要交易吗?

解决方法

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

带有 PROCEDURE 的概念证明

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

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

CREATE OR REPLACE PROCEDURE aaa.proc_work(_id text,INOUT _result text = NULL)
  LANGUAGE plpgsql AS
$proc$
BEGIN
   -- optionally assert that the steering row exists
   PERFORM FROM aaa.monitor WHERE id = _id FOR KEY SHARE SKIP LOCKED;
   IF NOT FOUND THEN   
      RAISE EXCEPTION 'aaa.monitor.id = % not found or blocked!',quote_literal(_id);
   END IF;

   -- try UPDATE
   UPDATE aaa.monitor
   SET    status = 'running'
   WHERE  id = _id
   AND    status <> 'running';  -- assuming column is NOT NULL

   IF NOT FOUND THEN
      _result := 'running'; RETURN;  -- this is how you return with INOUT params
   END IF;

   COMMIT;                                   -- HERE !!!

   <<big_work>>   -- optional label for the nested block
   BEGIN          -- start new code block
      --- rest of code ---
      -- PERFORM 1/0;          -- test exception?
      -- PERFORM pg_sleep(5);  -- test concurrency?

      -- finally
      UPDATE aaa.monitor
      SET    status = 'idle'
      WHERE  id = _id;
      _result := ''; RETURN;

   EXCEPTION WHEN OTHERS THEN
      UPDATE aaa.monitor
      SET    status = 'idle'  -- reset!
      WHERE  id = _id;
      _result := SQLERRM;
   END big_work;
END
$proc$;

致电(重要!):

CALL aaa.proc_work('invoicing');  -- stand-alone call!

重要说明

我在 COMMIT 之后添加了一个 UPDATE。现在,并发事务可以看到更新的行。

但是没有额外的 BEGINSTART TRANSACTIONThe manual:

在由 CALL 命令调用的过程以及匿名代码中 块(DO 命令),可以使用 命令 COMMITROLLBACK。一个新的事务开始 使用这些命令结束交易后自动,所以 没有单独的 START TRANSACTION 命令。 (注意 BEGINEND 在 PL/pgSQL 中有不同的含义。)

我们需要一个单独的 PL/pgSQL code block,因为您有一个自定义异常处理程序,并且(引用 the manual):

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

您不能在另一个事务中调用此过程,也不能与任何其他 DML 语句一起调用,这将强制使用外部事务包装器。必须是独立的 CALL。见:

注意异常处理程序中添加的 UPDATE aaa.monitor SET status = 'idle' WHERE ...。否则(已提交!)status 将在异常后无限期地保持“运行”状态。

关于从过程中返回一个值:

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

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

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

目前,为 UPDATE 情况考虑的列集是 那些有唯一索引可以在国外使用的 键(因此不考虑部分索引和表达式索引), 但这在未来可能会改变。

SKIP LOCK 在锁冲突的情况下不等待。添加的异常不应该发生。只是展示了一个防水的概念证明。

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

优越的方法

以上内容对于保留更多信息供全世界查看可能是有意义的。除了排队操作之外,还有效率更高的解决方案。使用代替,其他人可以立即“看到”。那么你不需要一个嵌套的事务开始,一个普通的 FUNCTION 就可以了:

CREATE OR REPLACE FUNCTION aaa.fnc_work(_id text)
  RETURNS text
  LANGUAGE plpgsql AS
$func$
BEGIN
   -- optionally assert that the steering row exists
   PERFORM FROM aaa.monitor WHERE id = _id FOR KEY SHARE SKIP LOCKED;
   IF NOT FOUND THEN   
      RAISE EXCEPTION 'aaa.monitor.id = % not found or blocked!',quote_literal(_id);
   END IF;

   -- lock row
   PERFORM FROM aaa.monitor WHERE id = _id FOR NO KEY UPDATE SKIP LOCKED;

   IF NOT FOUND THEN
      -- we made sure the row exists,so it must be locked
      RETURN 'running';
   END IF;

   --- rest of code ---
   -- PERFORM 1/0;          -- test exception?
   -- PERFORM pg_sleep(5);  -- test concurrency?

   RETURN '';

EXCEPTION WHEN OTHERS THEN
   RETURN SQLERRM;

END
$func$;

调用:

SELECT aaa.fnc_work('invoicing');

该调用可以以任何您想要的方式嵌套。只要一个事务在处理大工作,其他事务就不会启动。

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

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

关于PERFORM

关于行锁:

,

您要完成的是自治事务。 PostgreSQL 没有简单的方法来做到这一点。此链接 here 讨论了一些替代方案。

但上面链接的文章中讨论的一种方法是使用 PostgreSQL dblink 扩展。

您需要将扩展​​添加到服务器

CREATE EXTENSION dblink; 

然后你可以创建一个从你的函数内部调用的新函数

CREATE FUNCTION update_monitor_via_dblink(msg text)
 RETURNS void
 LANGUAGE sql
AS $function$
   select dblink('host=/var/run/postgresql port=5432 user=postgres dbname=postgres',format(' update aaa.monitor set status= %M',msg::text)
$function$;

您可能要考虑的另一件事是使用 PostgreSQL 锁。可以在 here 中找到更多信息。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...