并发事务导致竞争条件对插入具有唯一约束

Ell*_*urn 14 postgresql concurrency transaction plpgsql upsert

我有一个 Web 服务(http api),它允许用户安静地创建资源。在身份验证和验证之后,我将数据传递给 Postgres 函数,并允许它检查授权并在数据库中创建记录。

我今天发现了一个错误,即在同一秒内发出了两个 http 请求,导致使用相同的数据两次调用此函数。函数内部有一个子句,它在表上进行选择以查看值是否存在,如果存在,则获取 ID 并在下一个操作中使用该 ID,如果不存在,则插入数据,获取返回 ID,然后在下一个操作中使用它。下面是一个简单的例子。

select id into articleId from articles where title = 'my new blog';
if articleId is null then
    insert into articles (title, content) values (_title, _content)
    returning id into articleId;
end if;
-- Continue, using articleId to represent the article for next operations...
Run Code Online (Sandbox Code Playgroud)

正如您可能猜到的那样,我对数据进行了幻读,其中两个事务都进入了if articleId is null then块并试图插入到表中。一个成功了,另一个失败了,因为一个领域的独特限制。

我已经环顾四周,看看如何抵御这种情况,并找到了一些不同的选择,但由于某些原因,它们似乎都不适合我们的需求,我正在努力寻找任何替代方案。

  1. insert ... on conflict do nothing/update...我首先查看了on conflict看起来不错的选项,但是唯一的选项是do nothing不返回导致冲突的记录的 ID,并且do update不会工作,因为它会导致触发器在实际数据时被触发没有改变。在某些情况下,这不是问题,但在许多情况下,这可能会使会话用户会话无效,这是我们无法做到的。
  2. set transaction isolation level serializable;这似乎是最吸引人的答案,但是即使是我们的测试套件也会导致读/写依赖关系,就像上面一样,我们想在不存在的情况下插入并在存在时返回它并继续进行进一步的操作。如果我们有几个运行上述代码的待处理事务,它将导致读/写依赖错误,如 Postgres 文档的事务iso 中所述

这种并发读/写事务应该如何处理?

我自己或我的团队都没有声称自己是数据库专家,更不用说 Postgres 专家了,但觉得这必须是一个已解决的问题,或者过去有人遇到过。我们乐于接受任何建议。如果以上提供的信息不够,请发表评论,我会根据需要添加更多信息。

CL.*_*CL. 8

尝试第insert一个,使用on conflict ... do nothingreturning id。如果该值已经存在,您将不会从该语句中得到任何结果,因此您必须执行 aselect来获取 ID。

如果两个事务同时尝试这样做,其中一个将阻塞insert(因为数据库尚不知道另一个事务将提交还是回滚),并且只有在另一个事务完成后才继续。


Erw*_*ter 8

问题的根源在于,在默认READ COMMITTED隔离级别下,每个并发 UPSERT(或任何查询,就此而言)只能看到在查询开始时可见的行。手册:

当事务使用此隔离级别时,SELECT查询(没有FOR UPDATE/SHARE子句)只会看到查询开始之前提交的数据;它永远不会看到未提交的数据或并发事务在查询执行期间提交的更改。

但是UNIQUE索引是绝对的,并且仍然必须考虑并发输入的行 - 即使是不可见的行。因此,您可以获得唯一违规的异常,但您仍然不到同一查询中的冲突行。手册:

INSERTON CONFLICT DO NOTHING由于另一个事务的结果对INSERT快照不可见,因此带有子句的插入可能不会继续进行。同样,这仅适用于 Read Committed 模式。

这个问题的蛮力“解决方案”是用ON CONFLICT ... DO UPDATE. 然后在同一查询中可以看到新的行版本。但是有几个副作用,我建议不要这样做。其中之一是UPDATE触发器被触发——这是你想要明确避免的事情。与 SO 密切相关的答案:

剩下的选项是启动一个新命令(在同一个事务中),然后可以看到来自上一个查询的这些冲突行。现有的两个答案都表明了这一点。又是说明书:

但是,SELECT确实会看到在其自己的事务中执行的先前更新的影响,即使它们尚未提交。另请注意SELECT,如果其他事务在第一个SELECT启动之后和第二个SELECT启动之前提交更改,则两个连续的命令可以看到不同的数据,即使它们在单个事务中。

你想要更多

-- 继续,使用 articleId 来表示接下来操作的文章...

如果并发写入操作也许能够更改或删除的行,要绝对肯定,你也必须锁定选定的行。(无论如何,插入的行被锁定。)

而且由于您似乎有非常有竞争力的交易,为了确保您成功,请循环直到成功。包装成一个plpgsql函数:

CREATE OR REPLACE FUNCTION f_articleid(_title text, _content text, OUT _articleid int) AS
$func$
BEGIN
   LOOP
      SELECT articleid
      FROM   articles
      WHERE  title = _title
      FOR    UPDATE          -- or maybe a weaker lock 
      INTO   _articleid;

      EXIT WHEN FOUND;

      INSERT INTO articles AS a (title, content)
      VALUES (_title, _content)
      ON     CONFLICT (title) DO NOTHING  -- (new?) _content is discarded
      RETURNING a.articleid
      INTO   _articleid;

      EXIT WHEN FOUND;
   END LOOP;
END
$func$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

详细解释: