【问题标题】:PostgreSQL BEFORE INSERT trigger locking behavior in a concurrent environmentPostgreSQL BEFORE INSERT 触发并发环境中的锁定行为
【发布时间】:2017-11-14 16:19:21
【问题描述】:

我有一个通用函数可以操纵任何表的顺序(为什么与我的问题无关)。它读取当前值,计算出新值,设置它,然后返回它的计算,这就是插入的内容。这显然是一个多步骤的过程。

我从需要它的表上的 BEFORE INSERT 触发器调用它。

我只需要知道我是否保证在多用户环境中一次只能由一个调用者调用该函数?

具体来说,BEFORE INSERT 触发器是否必须在被另一个调用者再次调用之前完成?

从逻辑上讲,我会假设是的,但人们永远不知道幕后可能发生了什么。

如果答案是否定的,我需要对函数进行什么最小锁定以保证我可以以“线程安全”的方式读取和写入序列?

我正在使用 PG 10。

编辑

这里是用锁更新的函数

CREATE OR REPLACE FUNCTION public.uts_set()                                     
RETURNS TRIGGER AS                                      
$$                                      
DECLARE                                     
  sv        int8;                               
  seq       text := format('%I.%I_uts_seq', tg_table_schema, tg_table_name);                                
BEGIN
  EXECUTE   format('LOCK TABLE %I IN ROW EXCLUSIVE MODE;', tg_table_name);
  EXECUTE   'SELECT last_value+1 FROM ' || seq INTO sv; -- currval(seq) isn't useable           
  PERFORM   setval(seq, GREATEST(sv, (EXTRACT(epoch FROM localtimestamp) * 1000000)::int8), false);                             
  RETURN    NULL;
END;                                        
$$ LANGUAGE plpgsql;

但是,SELECT 已经获取了ROW EXCLUSIVE,因此该语句可能是多余的,可能需要更强的锁。或者,相反,它可能意味着不需要锁。

更新

如果我正确阅读了this SO question,那么我没有 LOCK 的原始版本应该可以工作,因为触发器获取了我更新后的函数冗余占用的相同锁。

【问题讨论】:

  • 您确定这是BEFORE INSERT 触发器吗?您返回的是 NULL 而不是 NEW ......触发器定义也存在其他问题,但在此之前,为什么您需要序列的最后一个值和当前 epoch * 1000000 之间的最大数字?
  • 我不能 100% 确定 last_value 会可靠地工作——即使没有并发也不能。
  • 它与一个呼叫者完美配合。测试良好。
  • @michel.milezzi 感谢您纠正 NULL 错误。该函数返回当前时间戳,但通过使用序列保证它是唯一的。因此,例如,如果要插入一千条记录,它们将从当前时间戳开始,并以 1 为增量。
  • @IamIC 好的,为后代编辑了我的答案。问候!

标签: postgresql concurrency triggers locking


【解决方案1】:

我只需要知道我是否保证在多用户环境中一次只能由一个调用者调用该函数?

没有。与调用函数本身无关,但可以通过SERIALIZABLE事务隔离级别实现此行为:

这个级别模拟所有提交的串行事务执行 交易;就好像交易一个接一个地执行, 连续,而不是同时

但是这种方法会带来一些折衷,例如让您的应用程序准备好在序列化失败的情况下重试事务。

也许错过了什么,但我真的相信你只需要NEXTVAL,如下所示:

CREATE OR REPLACE FUNCTION public.uts_set()                                     
RETURNS TRIGGER AS                                      
$$                                      
DECLARE                                     
  sv        int8;      
  -- First, use %I wildcard for identifiers instead of %s
  seq       text := format('%I.%I', tg_table_schema, tg_table_name || '_uts_seq');                                
BEGIN              
  -- Second, you couldn't call CURRVAL on a session 
  -- that you didn't issued NEXTVAL before
  sv := NEXTVAL(seq);                         

  -- Do your logic here...

  -- Result is ignored since this is an STATEMENT trigger   
  RETURN    NULL;                               
END;                                        
$$ LANGUAGE plpgsql;

请记住,CURRVAL 作用于会话 本地 范围,NEXTVAL 作用于 全局 范围,因此您拥有可靠的线程安全机制。

【讨论】:

  • 我发现我无法使用 NEXTVAL,因为它没有在新表上初始化。因此,永远无法插入记录,因为第一次插入失败。
  • 既然是触发器,那就是代码,所以我会把SET ISOLATION LEVEL SERIALIZABLE作为第一行,对吧?
  • 没那么简单,如果你的会话已经完成了一个操作,它就会失败......将整个事务启动为可序列化更安全:BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE
  • 我明白了。我正在尝试实现一个解决方案,在该解决方案中,我可以实现这个特殊的“唯一时间戳”目标,而不必担心记住将每个调用者包装在该隔离级别中。我希望解决方案对调用者透明。这可能吗?
  • 肯定有一个隔离可以一次只允许整个函数的一个调用者?或者有什么方法可以做到这一点?
【解决方案2】:

序列本身处理并发会话的线程安全。所以真正归结为与序列交互的代码。以下代码是线程安全的:

SELECT nextval('myseq');

如果该序列正在执行更多花哨的事情,例如setvalcurrval,我会更担心在高事务/多用户环境中完成。即便如此,在操作序列时,应该将序列本身与其他查询锁定。

【讨论】:

  • 可惜代码比较花哨,不能依赖序列的线程安全。
  • 你能在你的问题中提供一些代码吗?
猜你喜欢
  • 2021-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-24
  • 2021-11-25
  • 2015-02-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多