Postgres 更新...限制 1

vas*_*man 107 postgresql concurrency update queue

我有一个 Postgres 数据库,其中包含有关服务器集群的详细信息,例如服务器状态(“活动”、“待机”等)。活动服务器在任何时候都可能需要故障转移到备用服务器,我不在乎特别使用哪个备用服务器。

我想要一个数据库查询来更改备用服务器的状态 - 只有一个 - 并返回要使用的服务器 IP。选择可以是任意的:因为服务器的状态随着查询而改变,所以选择哪个备用数据库并不重要。

是否可以将我的查询限制为一次更新?

这是我到目前为止所拥有的:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;
Run Code Online (Sandbox Code Playgroud)

Postgres 不喜欢这样。我可以做些什么不同的事情?

Erw*_*ter 166

没有并发写访问

CTE(公用表表达式)中具体化一个选择,并FROMUPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING s.server_ip;
Run Code Online (Sandbox Code Playgroud)

我最初在这里有一个简单的子查询,但是LIMIT正如Feike指出的那样,它可以避开某些查询计划:

计划者可以选择生成一个计划,该计划在LIMITing子查询上执行嵌套循环,导致UPDATEs超过LIMIT,例如:

 Update on buganalysis [...] rows=5
Run Code Online (Sandbox Code Playgroud)
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis
Run Code Online (Sandbox Code Playgroud)

重现测试用例

解决上述问题的方法是将LIMIT子查询包装在它自己的 CTE 中,因为 CTE 被具体化,它不会在嵌套循环的不同迭代中返回不同的结果。

或者对简单的情况使用低相关子查询LIMIT 1。更简单、更快:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;
Run Code Online (Sandbox Code Playgroud)

具有并发写访问

假设所有这些的默认隔离级别READ COMMITTED。更严格的隔离级别(REPEATABLE READSERIALIZABLE)可能仍会导致序列化错误。看:

在并发写入负载下,添加FOR UPDATE SKIP LOCKED以锁定行以避免竞争条件。SKIP LOCKED已在 Postgres 9.5中添加,对于旧版本,请参见下文。手册:

使用SKIP LOCKED,将跳过任何无法立即锁定的选定行。跳过锁定的行提供了不一致的数据视图,因此这不适用于通用工作,但可用于避免多个使用者访问类似队列的表的锁争用。

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;
Run Code Online (Sandbox Code Playgroud)

如果没有合格的未锁定行,则此查询中不会发生任何事情(没有行被更新)并且您会得到一个空结果。对于非关键操作,这意味着您已完成。

但是,并发事务可能已锁定行,但随后没有完成更新(ROLLBACK或其他原因)。确保运行最终检查:

SELECT NOT EXISTS (
   SELECT FROM server_info
   WHERE  status = 'standby'
   );
Run Code Online (Sandbox Code Playgroud)

SELECT还可以看到锁定的行。如果不返回true,一行或多行仍未完成,事务仍然可以回滚。(或者同时添加了新行。)稍等,然后循环两个步骤:(UPDATE直到没有行返回;SELECT...)直到得到true.

有关的:

如果没有SKIP LOCKED在PostgreSQL的9.4或以上

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;
Run Code Online (Sandbox Code Playgroud)

试图锁定同一行的并发事务将被阻塞,直到第一个事务释放其锁。

如果第一个被回滚,则下一个事务获取锁并正常进行;队列中的其他人继续等待。

如果第一次提交,WHERE则重新评估条件,如果不再TRUEstatus已更改),则 CTE(有点令人惊讶)不返回任何行。没发生什么事。这时候,所有的交易要更新所需的行为同一
但不是当每个事务想要更新下一。由于我们只想更新任意(或随机)行,因此根本没有等待的意义。

我们可以在咨询锁的帮助下解除封锁:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;
Run Code Online (Sandbox Code Playgroud)

这样,尚未锁定的下一行将被更新。每个事务都有一个新的行来处理。我从捷克 Postgres Wiki那里得到了这个技巧的帮助。

id是任何唯一的bigint列(或具有隐式强制转换的任何类型,如int4int2)。

如果建议锁同时用于数据库中的多个表,请消除歧义pg_try_advisory_xact_lock(tableoid::int, id)-idinteger此处是唯一的。
由于tableoid是一个bigint量,理论上可以溢出integer。如果您足够偏执,请(tableoid::bigint % 2147483648)::int改用 - 为真正偏执的人留下理论上的“哈希冲突”......

此外,Postgres 可以以WHERE任何顺序自由测试条件。它可以在之前测试 pg_try_advisory_xact_lock()和获取锁,这可能会导致对不相关行的额外咨询锁,其中不为真。关于SO的相关问题: status = 'standby'status = 'standby'

通常,您可以忽略这一点。为了保证只锁定符合条件的行,您可以将谓词嵌套在像上面这样的 CTE 或带有OFFSET 0hack (prevents inlining)的子查询中。例子:

或者(顺序扫描更便宜)将条件嵌套在如下CASE语句中:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Run Code Online (Sandbox Code Playgroud)

然而,这个CASE技巧也会阻止 Postgres 在 上使用索引status。如果这样的索引可用,您不需要额外的嵌套开始:只有符合条件的行将在索引扫描中被锁定。

由于您无法确定每次调用都使用索引,因此您可以:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Run Code Online (Sandbox Code Playgroud)

CASE在逻辑上是多余的,但它服务于讨论的目的。

如果命令是长事务的一部分,请考虑可以(并且必须)手动释放会话级锁。因此,您可以在完成锁定行后立即解锁:pg_try_advisory_lock()pg_advisory_unlock()手册:

一旦在会话级别获得,建议锁将被保持,直到明确释放或会话结束。

有关的:

  • 自 postgres 13 起,CTE 不一定会实现。这是否意味着即使使用 CTE,某些查询计划忽略限制的问题也已经开始发生? (3认同)