我有一个 Postgres 数据库,其中包含有关服务器集群的详细信息,例如服务器状态(“活动”、“备用”等)。活动服务器在任何时候都可能需要故障转移到备用服务器,我不关心具体使用哪个备用服务器。
我想要一个数据库查询来更改备用服务器的状态 - 只是一个 - 并返回要使用的服务器 IP。选择可以是任意的:因为服务器的状态会随着查询而改变,选择哪个备用服务器并不重要。
是否可以将我的查询限制为一次更新?
这是我到目前为止所拥有的:
UPDATE server_info SET status = 'active'
WHERE status = 'standby' [[LIMIT 1???]]
RETURNING server_ip;
Postgres 不喜欢这样。我能做些什么不同的事情?
没有并发写访问
在CTE(公用表表达式)中实现选择并
FROM
在UPDATE
.我最初在这里有一个普通的子查询,但是正如飞克指出的那样,它可以回避
LIMIT
某些查询计划:或者对简单的情况使用低相关的子查询
LIMIT
1
。更简单,更快:具有并发写访问权限
假设所有这些的默认隔离级别
READ COMMITTED
。更严格的隔离级别 (REPEATABLE READ
和SERIALIZABLE
) 仍可能导致序列化错误。看:在并发写入负载下,添加
FOR UPDATE SKIP LOCKED
以锁定行以避免竞争条件。SKIP LOCKED
在 Postgres 9.5中添加,旧版本见下文。手册:如果没有剩余的符合条件的未锁定行,则此查询中不会发生任何事情(没有更新任何行)并且您会得到一个空结果。对于不重要的操作,这意味着您已完成。
但是,并发事务可能已锁定行,但随后未完成更新(
ROLLBACK
或其他原因)。为了确保运行最终检查:SELECT
也看到锁定的行。即使不返回true
,一或多行仍未完成,事务仍可能回滚。(或者同时添加了新行。)等一下,然后循环两个步骤:(UPDATE
直到你没有得到任何行回来;SELECT
...)直到你得到true
.有关的:
SKIP LOCKED
在 PostgreSQL 9.4 或更早版本中没有试图锁定同一行的并发事务被阻塞,直到第一个释放它的锁。
如果第一个被回滚,则下一个事务获取锁并正常进行;队列中的其他人继续等待。
如果第一次提交,
WHERE
则重新评估条件,如果不再存在TRUE
(status
已更改),则 CTE(有点令人惊讶)不返回任何行。什么都没发生。当所有事务都想更新同一行时,这是期望的行为。但不是当每个事务都想更新下一行时。而且由于我们只想更新任意(或随机)行,因此根本没有必要等待。
我们可以在咨询锁的帮助下解除阻塞:
这样,尚未锁定的下一行将被更新。每个事务都有一个新的行来处理。我从捷克 Postgres Wiki那里得到了这个技巧的帮助。
id
是任何唯一的bigint
列(或任何具有隐式转换的类型,如int4
orint2
)。如果建议锁同时用于数据库中的多个表,请消除歧义
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 0
hack (prevents inlining)的子查询中。例子:或者(对于顺序扫描更便宜)将条件嵌套在
CASE
如下语句中:然而,这个
CASE
技巧也会阻止 Postgres 在status
. 如果这样的索引可用,您就不需要额外的嵌套:只有符合条件的行将被锁定在索引扫描中。由于您无法确定每次调用都使用索引,因此您可以:
这
CASE
在逻辑上是多余的,但它服务于讨论的目的。如果命令是长事务的一部分,请考虑可以(并且必须)手动释放的会话级锁。因此,您可以在完成锁定行后立即解锁:
pg_try_advisory_lock()
和pg_advisory_unlock()
. 手册:有关的: