在事务完成后执行触发器

时间:2016-08-30 04:53:33

标签: postgresql triggers transactions notifications deferred-execution

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

documentation说:

  

DEFERRABLE
  NOT DEFERRABLE

     

这可以控制是否可以延迟约束。约束   不可延迟的将在每次之后立即检查   命令。可以推迟检查可延迟的约束   直到交易结束(使用SET CONSTRAINTS命令)。

它没有指定它是否仍在交易内或外。我个人的经验说它在交易中,我需要它在外面!

在事务中是否执行了DEFERRED(或INITIALLY DEFERRED)触发器?如果是,我怎么能将他们的执行推迟到交易完成的时间?

为了给你一个提示,我正在使用pg_notify和RabbitMQ(PostgreSQL LISTEN Exchange)来发送消息。我在外部应用程序中处理此类消息。现在我有一个触发器,它通过在消息中包含记录的id来通知外部应用程序新插入的记录。但是,以一种非确定性的方式,偶尔,当我尝试通过其手头的id选择记录时,无法找到记录。那是因为事务尚未完成,并且记录实际上没有添加到表中。如果我只能在事务完成后推迟执行触发器,那么一切都会成功。

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

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

我确信这会让整个情况变得更加复杂。

2 个答案:

答案 0 :(得分:11)

触发器(包括所有类型的延迟触发器)在内部触发

但这不是问题所在,因为无论如何都会在 交易之间传递

The manual on NOTIFY:

  

NOTIFY以某些重要方式与SQL事务交互。   首先,如果在事务内执行NOTIFY,则通知   除非交易发生,否则不会发送事件   承诺。这是合适的,因为如果交易中止,   其中的所有命令都没有效果,包括NOTIFY。但   如果人们期待通知事件,那可能会令人不安   立即交付。其次,如果听力会话收到了   通知信号在交易内,通知   事件将不会发送到其连接的客户端,直到之后   交易完成(已提交或已中止)。再次,   推理是,如果通知是在一个   后来中止的事务,人们希望通知   以某种方式撤消 - 但服务器不能"收回"通知   一旦它发送给客户端。 所以通知事件只是   在交易之间交付。这就是结果   使用NOTIFY进行实时信令的应用程序应该尽量保持   他们的交易很短。

大胆强调我的。

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

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

  • 并发交易干扰
  • 触发做比你想象的更多或更多的事情。
  • 各种编程错误。

无论哪种方式,如手册所示,保留发送简短通知的交易是个好主意。

dblink

至于你以后的补充:

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

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

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

dblink 是一个游戏规则改变者,因为它在另一个数据库中打开一个单独的事务。这有时用于伪造 自治交易

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

  

该函数返回查询生成的行。

如果您可以从同一交易发送通知,那将是清洁解决方案

dblink

的解决方法

如果必须从其他交易发送通知,则dblink_send_query()会有解决方法:

  

dblink_send_query发送一个异步执行的查询,即不立即等待结果。

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))开始提交。选择适当的秒数。

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

安全替代

更安全的替代方法是写入队列表并像@Bohemian's answer中讨论的那样轮询它。这个相关的答案演示了如何安全地进行调查:

答案 1 :(得分:7)

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

让数据库告诉应用程序执行某些操作是一种破碎的模式。它被打破了因为:

  1. 如果应用程序没有收到消息,则没有后备,例如,因为它已关闭,网络爆炸,无论如何。即使应用程序回复确认(它不能),也无法解决此问题(请参阅下一点)
  2. 如果应用获取消息但未能完成消息(由于很多原因),没有明智的方法可以重试工作。
  3. 相比之下,使用数据库作为持久队列,并让应用程序轮询它以便工作,并在工作完成时将工作从队列中取出,没有上述问题。

    有很多方法可以实现这一目标。我更喜欢的是让一些进程(通常在插入,更新和删除时触发)将数据放入“队列”表中。让另一个进程轮询该工作表,并在工作完成时从表中删除。

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

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

    由数据库表支持的队列是在商业级基于队列的平台中实现持久队列的基础,因此该模式经过了充分测试,使用和理解。

    让数据库做它最擅长的事情,并且它唯一做得很好:管理数据。不要试图将数据库服务器变成app服务器。