在事务完成后执行触发器

Executing a trigger AFTER the completion of a transaction

在 PostgreSQL 中,DEFERRED 触发器是在事务完成之前(之内)执行还是在事务完成之后执行?

documentation 说:

DEFERRABLE
NOT DEFERRABLE

This controls whether the constraint can be deferred. A constraint that is not deferrable will be checked immediately after every command. Checking of constraints that are deferrable can be postponed until the end of the transaction (using the SET CONSTRAINTS command).

没有说明是还在交易里面还是在交易外面。我个人的经验是在交易里面,需要在交易外面!

DEFERRED(或INITIALLY DEFERRED)触发器是否在事务内部执行?如果是,我如何将它们的执行推迟到交易完成的时间?

为了提示您我的目的,我正在使用 pg_notify 和 RabbitMQ (PostgreSQL LISTEN Exchange) 发送消息。我在外部应用程序中处理此类消息。现在我有一个触发器,它通过在消息中包含记录的 id 来通知外部应用程序新插入的记录。但是以一种不确定的方式,偶尔,当我尝试通过手头的 id select 一条记录时,找不到该记录。那是因为交易还没有完成,记录还没有真正添加到table。如果我只能将触发器的执行推迟到交易完成之后,一切都会好起来的。

为了得到更好的答案,让我解释一下更接近现实世界的情况。实际情况比我之前解释的要复杂一些。 source code can be found here 如果有人感兴趣的话。由于我不想深入研究的原因,我必须从另一个数据库发送通知,所以通知实际上是这样发送的:

PERFORM * FROM dblink('hq','SELECT pg_notify(''' || channel || ''', ''' || payload || ''')');

我敢肯定这会使整个情况变得更加复杂。

触发器(包括各种延迟触发器)在内部事务中触发。

但这不是这里的问题,因为通知是在 个交易之间传递的。

The manual on NOTIFY:

NOTIFY interacts with SQL transactions in some important ways. Firstly, if a NOTIFY is executed inside a transaction, the notify events are not delivered until and unless the transaction is committed. This is appropriate, since if the transaction is aborted, all the commands within it have had no effect, including NOTIFY. But it can be disconcerting if one is expecting the notification events to be delivered immediately. Secondly, if a listening session receives a notification signal while it is within a transaction, the notification event will not be delivered to its connected client until just after the transaction is completed (either committed or aborted). Again, the reasoning is that if a notification were delivered within a transaction that was later aborted, one would want the notification to be undone somehow — but the server cannot "take back" a notification once it has sent it to the client. So notification events are only delivered between transactions. The upshot of this is that applications using NOTIFY for real-time signaling should try to keep their transactions short.

大胆强调我的。

pg_notify() 只是 SQL NOTIFY 命令的一个方便的包装函数。

如果在收到通知后找不到某些行,则一定有其他原因!去找吧。可能的候选人:

  • 并发事务干扰
  • 触发器会做一些比你想象的更多或不同的事情。
  • 各种编程错误。

无论哪种方式,就像手册所建议的那样,保持发送通知的事务简短

dblink

更新: PROCEDUREDO 语句中的事务控制 Postgres 11 或更高版本简单多了。只需 COMMIT;(也)发送等待通知。


原始答案(主要针对 Postgres 10 或更早版本):

PERFORM * FROM dblink('hq','SELECT pg_notify(''' || channel || ''', ''' || payload || ''')');

... 应该用 format() 重写以简化并使语法安全:

PRERFORM dblink('hq', format('NOTIFY %I, %L', channel, payload));

dblink 在这里改变了游戏规则,因为它在另一个数据库中打开了一个单独的事务。这有时被用来伪造自主交易.

  • Does Postgres support nested or autonomous transactions?

  • How do I do large non-blocking updates in PostgreSQL?

dblink() 等待远程命令完成。所以远程事务很可能会首先提交。 The manual:

The function returns the row(s) produced by the query.

如果您可以从同一事务发送通知,那将是一个干净的解决方案

dblink

的解决方法

如果通知必须从不同的事务发送,有一个解决方法dblink_send_query()

dblink_send_query sends a query to be executed asynchronously, that is, without immediately waiting for the result.

DO  -- or plpgsql function
$$
BEGIN
   -- do stuff

   PERFORM dblink_connect   ('hq',   'your_connstr_or_foreign_server_here');
   PERFORM dblink_send_query('con1', format('SELECT pg_sleep(3); NOTIFY %I, %L ', 'Channel', 'payload'));
   PERFORM dblink_disconnect('con1');
END
$$;

如果您在事务结束前立即执行此操作,您的本地事务将有 3 秒 (pg_sleep(3)) 提前开始提交。选择合适的秒数。

这种方法存在固有的不确定性,因为如果出现任何问题,您不会收到任何错误消息。对于 secure 解决方案,您需要不同的设计。但是,在成功发送命令后,它仍然失败的可能性非常小。错过成功通知的几率似乎 高得多 ,但这已经内置到您当前的解决方案中。

安全的替代品

更安全的替代方法是写入队列 table 并像 中讨论的那样轮询它。这个相关的答案演示了如何安全地轮询:

我将此作为答案发布,假设您要解决的实际问题是将外部流程的执行推迟到事务完成之后(而不是您正在尝试的 X-Y "problem"使用触发器功夫解决)。

让数据库告诉应用程序做某事是一种错误的模式。它坏了,因为:

  1. 如果应用程序没有收到消息,则没有回退,例如因为它已关闭,网络爆炸,等等。即使应用程序回复确认(它不能),也无法解决这个问题(见下一点)
  2. 如果应用收到消息但未能完成(出于多种原因),则没有重试工作的明智方法

相比之下,将数据库用作持久队列,并让应用轮询它以进行工作,并在工作完成后将工作从队列中取出,存在 none 个上述问题。

有很多方法可以实现这一点。我更喜欢的方法是让一些进程(通常在插入、更新和删除时触发)将数据放入 "queue" table。让另一个进程轮询 table 来完成工作,并在工作完成后从 table 中删除。

它还增加了一些其他好处:

  • 工作的生产和消费是分离的,这意味着您可以安全地终止并重新启动您的应用程序(这必须不时发生,例如部署)- 队列 table 会随着应用程序的增长而愉快地增长已关闭,并且会在应用程序备份时耗尽。您甚至可以用全新的应用程序替换该应用程序
  • 如果您出于某种原因想要启动某些项目的处理,您可以手动将行插入队列 table。我自己使用这种技术来启动数据库中 all 项目的处理,这些项目需要通过放入队列一次进行初始化。重要的是,我不需要为了触发触发器而对每一行进行敷衍更新
  • 关于您的问题,可以通过向队列 table 添加时间戳列并让轮询查询仅 select 早于(例如)1 秒的行来引入轻微的延迟,这使数据库有时间完成其事务
  • 您不能使应用程序过载。该应用程序将只读取它可以处理的工作量。如果你的队列在增长,你需要一个更快的应用程序,或者更多的应用程序如果有多个消费者在操作,并发可以通过(例如)在队列中添加一个"token"列来解决table

由数据库支持的队列 tables 是如何在商业级基于队列的平台中实现持久队列的基础,因此该模式得到了很好的测试、使用和理解。

让数据库去做它最擅长的事情,也是它唯一擅长的事情:管理数据。不要试图将您的数据库服务器变成应用程序服务器。