在事务完成后执行触发器

Meh*_*ran 13 postgresql notifications triggers transactions deferred-execution

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

文件说:

DEFERRABLE
NOT DEFERRABLE

这可以控制是否可以延迟约束.在每个命令之后立即检查不可延迟的约束.检查可延迟的约束可以推迟 到事务结束(使用SET CONSTRAINTS命令).

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

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

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

为了得到更好的答案,让我更接近现实世界来解释情况.实际情况比我之前解释的要复杂一些.该源代码可以在这里找到,如果任何人的兴趣.由于我不打算深入研究的原因,我必须从另一个数据库发送通知,以便实际发送通知,如:

PERFORM * FROM dblink('hq','SELECT pg_notify(''' || channel || ''', ''' || payload || ''')');
Run Code Online (Sandbox Code Playgroud)

我确信这使得整个情况变得更加复杂.

Erw*_*ter 11

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

但这不是问题所在,因为无论如何都要事务之间传递通知.

手册NOTIFY:

NOTIFY以某些重要方式与SQL事务交互.首先,如果NOTIFY在事务内部执行a,则除非提交事务,否则不会传递通知事件.这是合适的,因为如果事务被中止,则其中的所有命令都没有效果,包括NOTIFY.但如果人们期望立即传递通知事件,那就令人不安.其次,如果侦听会话在事务处理期间收到通知信号,则通知事件将不会在事务完成(提交或中止)之后传递到其连接的客户端.同样,原因在于,如果在稍后中止的事务中传递通知,则可能希望以某种方式撤消通知 - 但是一旦将通知发送到客户端,服务器就不能"收回"通知.因此,通知事件仅在事务之间传递.这样做的结果是,NOTIFY用于实时信令的应用程序应该尽量保持其交易简短.

大胆强调我的.

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

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

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

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

dblink

至于你以后的补充:

PERFORM * FROM dblink('hq','SELECT pg_notify(''' || channel || ''', ''' || payload || ''')');
Run Code Online (Sandbox Code Playgroud)

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

PRERFORM dblink('hq', format('NOTIFY %I, %L', channel, payload));
Run Code Online (Sandbox Code Playgroud)

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

dblink()等待远程命令完成.所以远程事务很可能首先提交.手册:

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

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

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
$$;
Run Code Online (Sandbox Code Playgroud)

如果您在事务结束之前执行此操作,则本地事务将获得3秒(pg_sleep(3))开头提交.选择适当的秒数.

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

安全的选择

更安全的替代方法是写入队列表并像在@ Bohemian的答案中讨论的那样进行轮询.此相关答案演示了如何安全地进行轮询:


Boh*_*ian 7

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

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

  1. 如果应用程序没有收到消息,则没有后备,例如,因为它已关闭,网络爆炸,无论如何.即使应用程序回复确认(它不能),也无法解决此问题(请参阅下一点)
  2. 如果应用程序收到消息但未能完成消息,则没有合理的方法来重试工作(由于很多原因)

相反,使用数据库作为持久性队列,并让应用程序轮询它以进行工作,并在工作完成时将工作从队列中取出,不会出现上述问题.

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

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

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

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

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