Postgres UPDATE…LIMIT 1


77

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

我希望数据库查询更改备用数据库(仅一个)的状态,并返回要使用的服务器IP。选择可以是任意的:由于服务器的状态随查询而变化,因此选择哪个备用数据库都无关紧要。

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

这是我到目前为止的内容:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Postgres不喜欢这样。我可以做些什么?


只需选择代码中的服务器并将其添加为受约束的位置即可。这也使您可以首先检查其他条件(最旧,最新,最近运行,最小负载,相同直流电,不同机架,最小错误)。无论如何,大多数故障转移协议都需要某种形式的确定性。
eckes

@eckes这是一个有趣的想法。在我的情况下,“从代码中选择服务器”将意味着首先从数据库中读取可用服务器的列表,然后更新记录。因为应用程序的许多实例都可以执行此操作,所以存在争用条件,并且需要原子操作(或者是5年前)。选择权不必是确定性的。
6

Answers:


125

没有并发的写访问

CTE中实现选择并将其加入的FROM子句中UPDATE

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 server_ip;

我最初在这里有一个普通的子查询,但是可以LIMITFeike所指出的那样为某些查询计划回避:

策划者可以选择生成在执行嵌套循环计划LIMITing子查询,造成更多的UPDATEsLIMIT,如:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

重现测试用例

解决上述问题的方法是将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;

并发写访问

假设所有这些都是默认隔离级别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;

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

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

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

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;

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

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

如果是第一个提交的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;

这样,尚未锁定的下一行将被更新。每笔交易都有一个新的行可以处理。我从捷克Postgres Wiki获得了此技巧的帮助。

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

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

另外,Postgres可以WHERE按任何顺序自由测试条件。它可以在之前测试 pg_try_advisory_xact_lock()并获取一个锁,这可能导致不相关的行上出现其他咨询锁,这是不正确的。关于SO的相关问题: status = 'standby'status = 'standby'

通常,您可以忽略它。为了保证只有合格的行被锁定,您可以将谓词嵌套在上述CTE或带有OFFSET 0hack的子查询中(防止内联)。例:

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

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

但是,CASE技巧也会使Postgres不能在上使用索引status。如果有这样的索引可用,那么您就不需要额外的嵌套:只有合格的行才会在索引扫描中被锁定。

由于不能确定每次调用都使用索引,因此可以:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

CASE逻辑上讲,它是多余的,但它可以满足上述目的。

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

一旦在会话级别获取,将保持咨询锁,直到明确释放或会话结束为止。

有关:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.