两个不同表上的并发更新失败

Val*_*aty 4 postgresql deadlock subquery update where

我使用 PostgreSQL 和以下数据库架构:

CREATE TABLE plans (
    slug VARCHAR(500) PRIMARY KEY
);

CREATE TABLE users (
    id VARCHAR(16) PRIMARY KEY,
    org_id VARCHAR(16) NOT NULL
);

CREATE TABLE orgs (
    id VARCHAR(16) PRIMARY KEY,
    plan_slug VARCHAR(500) NOT NULL,
    last_write_at DOUBLE PRECISION
);
Run Code Online (Sandbox Code Playgroud)

就我而言,我想编写一个查询来更新某些组织plan_slug并保护它免受其他可能的并发更新的影响。为此,我SELECT在子查询中使用 a 并按FOR UPDATE特定顺序锁定行以避免死锁。就像下面的查询一样:

UPDATE orgs
SET plan_slug = 'plan_1'
WHERE id = ANY(
    SELECT subquery_orgs.id
    FROM orgs AS subquery_orgs
    JOIN users ON users.org_id = subquery_orgs.id
    WHERE users.id = ANY('{user_1, user_2, user_3}')
    ORDER BY subquery_orgs.id
    FOR UPDATE
);
Run Code Online (Sandbox Code Playgroud)

我注意到,如果此请求需要很长时间才能运行,则很可能与尝试更新组织的另一个查询last_write_at(已由第一个查询更新的组织)发生冲突。

如下:

UPDATE orgs 
SET last_write_at = 999 
FROM plans 
WHERE orgs.id = 'org_1';
Run Code Online (Sandbox Code Playgroud)

查询就会成功。但是如果我在查询的子句plan_slug中添加 the ,它总是会更新失败。WHEREPostgre 返回UPDATE 0.

查询如下:

UPDATE orgs 
SET last_write_at = 999 
FROM plans 
WHERE orgs.id = 'org_1' 
AND plans.slug = orgs.plan_slug;
Run Code Online (Sandbox Code Playgroud)

据我了解,流程应该是:

  1. 第一个查询执行。该plan_slug行已锁定。
  2. 执行第二个查询。它停止执行并等待该行解锁。
  3. 第一个查询完成更新。
  4. 第二个查询重新启动并重新评估更改。
  5. plan_slug更改,但表中存在新的plans,第二个查询应该成功。

plan_slug那么为什么当我将 the 添加到子句时查询无法更新WHERE

dwh*_*emv 5

相关问题

问题

在步骤 4 之前,您对该过程的总结是正确的。锁定后查询不会重新启动;读取修改后的行版本并替换旧的行版本。在采取锁定之前,已发生表联接,新的行版本使联接变得无意义,并且行被丢弃。

这是我对事件的修订版本:

  1. 第一个查询开始执行。中的行orgs被锁定。

  2. 第二个查询开始执行。

    A。从和UPDATE读取行版本(忽略#1,因为它还没有)。orgsplansCOMMIT

    b. 对和UPDATE执行联接并有资格构建目标行列表。orgsplansorgs.id

    C。UPDATE尝试将目标行FOR NO KEY SHARE和块锁定在第一个查询的FOR UPDATE锁上。

  3. 第一个查询COMMIT

  4. 第二个查询解除阻止并注意到它所定位的行已更改。

    A。读取新的行版本并替换联接中的现有行版本。

    b. 重新评估条件,现在连接条件失败,因为orgs.plan_slug已更改并且不再plans与之前连接的行匹配。这些行将被丢弃。

  5. 第二个查询没有找到要使用 定位的行UPDATE

以图形方式显示它:

  • 第二个查询以连接开始,如下所示:

    ID 计划_slug 最后写入时间 蛞蝓
    用户_1 计划_1 第888章 计划_1
  • 第一个查询完成,orgs.plan_slug从 更改plan_1plan_2

  • 第二个查询继续,读取orgs行版本,并将其连接状态更改为:

    ID 计划_slug 最后写入时间 蛞蝓
    用户_1 计划_2 第888章 计划_1
  • 第二个查询现在重新评估其WHERE条件。呃哦,plan_2!= plan_1,行被丢弃。

解决方案

  1. 不要FROM plans在你的UPDATE last_write_at.... 老实说,我不明白你的查询的目的是什么;中的列plans没有以任何有意义的方式引用。我认为那里发生了更多事情,为了简化问题而删除了这些内容。也许您可以更新问题以阐明为什么会这样。
  2. REPEATABLE READ对您的事务使用隔离级别UPDATE last_write_at...。在该级别,您不会更新任何行,而是会收到错误ERROR: could not serialize access due to concurrent update。您可以中止并重试事务,因为知道它正在获取新数据。
  3. SELECT .. FOR UPDATE在事务中使用 previous来锁定 before 的行UPDATE。然后UPDATE永远不必应对从其下方发生变化的行。在 PL/pgSQL 中,您可以使用PERFORM 1 ... FOR UPDATE代替来避免输出行。举个例子:
    BEGIN;
    SELECT id FROM orgs WHERE id='org_1' FOR UPDATE;
    UPDATE orgs SET last_write_at = 999 FROM plans 
      WHERE orgs.id = 'org_1' AND plans.slug = orgs.plan_slug;
    COMMIT;
    
    Run Code Online (Sandbox Code Playgroud)

制作人员