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(公用表表达式)中具体化一个选择,并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 s.server_ip;
Run Code Online (Sandbox Code Playgroud)
我最初在这里有一个简单的子查询,但是LIMIT正如Feike指出的那样,它可以避开某些查询计划:
计划者可以选择生成一个计划,该计划在
LIMITing子查询上执行嵌套循环,导致UPDATEs超过LIMIT,例如:
Run Code Online (Sandbox Code Playgroud)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
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 READ和SERIALIZABLE)可能仍会导致序列化错误。看:
在并发写入负载下,添加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则重新评估条件,如果不再TRUE(status已更改),则 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列(或具有隐式强制转换的任何类型,如int4或int2)。
如果建议锁同时用于数据库中的多个表,请消除歧义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 (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()。手册:
一旦在会话级别获得,建议锁将被保持,直到明确释放或会话结束。
有关的: