没有并发的写访问
在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;
我最初在这里有一个普通的子查询,但是可以LIMIT
像Feike所指出的那样为某些查询计划回避:
策划者可以选择生成在执行嵌套循环计划LIMITing
子查询,造成更多的UPDATEs
比LIMIT
,如:
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 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;
如果没有合格的未锁定行,则此查询中无任何反应(没有行被更新),并且您将得到一个空结果。对于非关键操作,这意味着您已经完成。
但是,并发事务可能具有锁定的行,但随后没有完成更新(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
条件,将重新评估条件,如果条件不再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;
这样,尚未锁定的下一行将被更新。每笔交易都有一个新的行可以处理。我从捷克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 0
hack的子查询中(防止内联)。例:
或者(对顺序扫描更便宜)将条件嵌套在如下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()
。手册:
一旦在会话级别获取,将保持咨询锁,直到明确释放或会话结束为止。
有关: