PostgreSQL 并发事务问题
PostgreSQL concurrent transaction issues
我目前正在构建一个爬虫。多个爬行工作者访问同一个 PostgreSQL 数据库。遗憾的是,我在此处介绍的主要交易中遇到了问题:
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE webpages
SET locked = TRUE
WHERE url IN
(
SELECT DISTINCT ON (source) url
FROM webpages
WHERE
(
last IS NULL
OR
last < refreshFrequency
)
AND
locked = FALSE
LIMIT limit
)
RETURNING *;
COMMIT;
url
是一个 URL(字符串)
source
是域名(String)
last
是最后一次抓取页面的时间(日期)
locked
是一个布尔值,设置为表示当前正在抓取网页(布尔值)
我尝试了两种不同的事务隔离级别:
ISOLATION LEVEL SERIALIZABLE
,我收到类似 could not serialize access due to concurrent update
的错误
ISOLATION LEVEL READ COMMITTED
,我从并发事务中得到重复的 url
s,因为数据是 "frozen" 从事务首次提交(我认为)
总的来说,我对 PostgreSQL 和 SQL 还很陌生,所以我真的不确定我能做些什么来解决这个问题。
更新:
PostgreSQL 版本是 9.2.x.
webpage
table 定义:
CREATE TABLE webpages (
last timestamp with time zone,
locked boolean DEFAULT false,
url text NOT NULL,
source character varying(255) PRIMARY KEY
);
第一次尝试:
UPDATE webpages
SET locked = TRUE
WHERE url IN
(
SELECT DISTINCT ON (source) url
FROM webpages
WHERE
(
last IS NULL
OR
last < refreshFrequency
)
AND
locked = FALSE
LIMIT limit
)
WHERE
(
last IS NULL
OR
last < refreshFrequency
)
AND
locked = FALSE
您正在尝试仅更新 locked = FALSE
的记录。
假设在table中有如下记录:
URL locked
----------------
A false
A true
您更新中的子查询将重新运行 A
。
然后外部更新将执行:
UPDATE webpages
SET locked = TRUE
WHERE url IN ( 'A' )
实际上 table 中包含 url=A
的所有记录都将被更新,
无论它们在 locked
列中的值如何。
您需要向外部更新应用与子查询中相同的 WHERE
条件。
澄清
这个问题有解释的余地。我是这样理解这个任务的:
最多锁定 limit
URL 个满足某些条件但尚未锁定的对象。为了分散来源的负载,每个 URL 都应该来自不同的来源。
数据库设计
假设有一个单独的 table source
:这会使工作更快更容易。如果你没有这样的table,创建它,无论如何它都是正确的设计:
CREATE TABLE source (
source_id serial NOT NULL PRIMARY KEY
, source text NOT NULL
);
CREATE TABLE webpage (
source_id int NOT NULL REFERENCES source
url text NOT NULL PRIMARY KEY
locked boolean NOT NULL DEFAULT false, -- may not be needed
last timestamp NOT NULL DEFAULT '-infinity' -- makes query simpler
);
或者,您可以有效地使用递归 CTE:
- Optimize GROUP BY query to retrieve latest record per user
带咨询锁的基本解决方案
即使在默认 read committed
隔离级别下,我正在使用 advisory locks 来使它安全且便宜:
UPDATE webpage w
SET locked = TRUE
FROM (
SELECT (SELECT url
FROM webpage
WHERE source_id = s.source_id
AND (last >= refreshFrequency) IS NOT TRUE
AND locked = FALSE
AND pg_try_advisory_xact_lock(url) -- only true is free
LIMIT 1 -- get 1 URL per source
) AS url
FROM (
SELECT source_id -- the FK column in webpage
FROM source
ORDER BY random()
LIMIT limit -- random selection of "limit" sources
) s
FOR UPDATE
) l
WHERE w.url = l.url
RETURNING *;
或者,您可以仅使用 建议锁,而根本不使用 table 列 locked
。基本上只是 运行 SELECT
语句。锁一直保持到事务结束。您可以使用 pg_try_advisory_lock()
来保持锁定直到会话结束。完成后仅 UPDATE
一次 设置 last
(并可能释放咨询锁)。
其他要点
在 Postgres 9.3 或更高版本中,您将使用 LATERAL
连接而不是相关子查询。
我选择pg_try_advisory_xact_lock()
是因为锁可以(也应该)在事务结束时释放。咨询锁详解:
如果某些源没有更多 URL 可供抓取,您得到 少于 limit
行。
随机选择来源是我的大胆但有根据的猜测,因为没有可用的信息。如果你的 source
table 很大,还有更快的方法:
- Best way to select random rows PostgreSQL
refreshFrequency
实际上应该被称为 lastest_last
,因为它不是 "frequency",而是 timestamp
或 date
.
递归选择
要获得完整的限制行数如果可用,请使用RECURSIVE
CTE并迭代所有源,直到您找到足够或找不到更多。
正如我上面提到的,您可能根本不需要列 locked
并且仅使用建议锁(更便宜)进行操作。只需在交易结束时设置 last
,然后再开始下一轮。
WITH RECURSIVE s AS (
SELECT source_id, row_number() OVER (ORDER BY random()) AS rn
FROM source -- you might exclude "empty" sources early ...
)
, page(source_id, rn, ct, url) AS (
SELECT 0, 0, 0, ''::text -- dummy init row
UNION ALL
SELECT s.source_id, s.rn
, CASE WHEN t.url <> ''
THEN p.ct + 1
ELSE p.ct END -- only inc. if url found last round
, (SELECT url
FROM webpage
WHERE source_id = t.source_id
AND (last >= refreshFrequency) IS NOT TRUE
AND locked = FALSE -- may not be needed
AND pg_try_advisory_xact_lock(url) -- only true is free
LIMIT 1 -- get 1 URL per source
) AS url -- try, may come up empty
FROM page p
JOIN s ON s.rn = p.rn + 1
WHERE CASE WHEN p.url <> ''
THEN p.ct + 1
ELSE p.ct END < limit -- your limit here
)
SELECT url
FROM page
WHERE url <> ''; -- exclude '' and NULL
或者,如果您也需要管理 locked
,请将此查询与上述 UPDATE
一起使用。
进一步阅读
你会喜欢 SKIP LOCKED
即将推出的 Postgres 9.5:
相关:
- Atomic UPDATE .. SELECT in Postgres
我目前正在构建一个爬虫。多个爬行工作者访问同一个 PostgreSQL 数据库。遗憾的是,我在此处介绍的主要交易中遇到了问题:
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE webpages
SET locked = TRUE
WHERE url IN
(
SELECT DISTINCT ON (source) url
FROM webpages
WHERE
(
last IS NULL
OR
last < refreshFrequency
)
AND
locked = FALSE
LIMIT limit
)
RETURNING *;
COMMIT;
url
是一个 URL(字符串)source
是域名(String)last
是最后一次抓取页面的时间(日期)locked
是一个布尔值,设置为表示当前正在抓取网页(布尔值)
我尝试了两种不同的事务隔离级别:
ISOLATION LEVEL SERIALIZABLE
,我收到类似could not serialize access due to concurrent update
的错误
ISOLATION LEVEL READ COMMITTED
,我从并发事务中得到重复的url
s,因为数据是 "frozen" 从事务首次提交(我认为)
总的来说,我对 PostgreSQL 和 SQL 还很陌生,所以我真的不确定我能做些什么来解决这个问题。
更新:
PostgreSQL 版本是 9.2.x.
webpage
table 定义:
CREATE TABLE webpages (
last timestamp with time zone,
locked boolean DEFAULT false,
url text NOT NULL,
source character varying(255) PRIMARY KEY
);
第一次尝试:
UPDATE webpages
SET locked = TRUE
WHERE url IN
(
SELECT DISTINCT ON (source) url
FROM webpages
WHERE
(
last IS NULL
OR
last < refreshFrequency
)
AND
locked = FALSE
LIMIT limit
)
WHERE
(
last IS NULL
OR
last < refreshFrequency
)
AND
locked = FALSE
您正在尝试仅更新 locked = FALSE
的记录。
假设在table中有如下记录:
URL locked
----------------
A false
A true
您更新中的子查询将重新运行 A
。
然后外部更新将执行:
UPDATE webpages
SET locked = TRUE
WHERE url IN ( 'A' )
实际上 table 中包含 url=A
的所有记录都将被更新,
无论它们在 locked
列中的值如何。
您需要向外部更新应用与子查询中相同的 WHERE
条件。
澄清
这个问题有解释的余地。我是这样理解这个任务的:
最多锁定 limit
URL 个满足某些条件但尚未锁定的对象。为了分散来源的负载,每个 URL 都应该来自不同的来源。
数据库设计
假设有一个单独的 table source
:这会使工作更快更容易。如果你没有这样的table,创建它,无论如何它都是正确的设计:
CREATE TABLE source (
source_id serial NOT NULL PRIMARY KEY
, source text NOT NULL
);
CREATE TABLE webpage (
source_id int NOT NULL REFERENCES source
url text NOT NULL PRIMARY KEY
locked boolean NOT NULL DEFAULT false, -- may not be needed
last timestamp NOT NULL DEFAULT '-infinity' -- makes query simpler
);
或者,您可以有效地使用递归 CTE:
- Optimize GROUP BY query to retrieve latest record per user
带咨询锁的基本解决方案
即使在默认 read committed
隔离级别下,我正在使用 advisory locks 来使它安全且便宜:
UPDATE webpage w
SET locked = TRUE
FROM (
SELECT (SELECT url
FROM webpage
WHERE source_id = s.source_id
AND (last >= refreshFrequency) IS NOT TRUE
AND locked = FALSE
AND pg_try_advisory_xact_lock(url) -- only true is free
LIMIT 1 -- get 1 URL per source
) AS url
FROM (
SELECT source_id -- the FK column in webpage
FROM source
ORDER BY random()
LIMIT limit -- random selection of "limit" sources
) s
FOR UPDATE
) l
WHERE w.url = l.url
RETURNING *;
或者,您可以仅使用 建议锁,而根本不使用 table 列 locked
。基本上只是 运行 SELECT
语句。锁一直保持到事务结束。您可以使用 pg_try_advisory_lock()
来保持锁定直到会话结束。完成后仅 UPDATE
一次 设置 last
(并可能释放咨询锁)。
其他要点
在 Postgres 9.3 或更高版本中,您将使用
LATERAL
连接而不是相关子查询。我选择
pg_try_advisory_xact_lock()
是因为锁可以(也应该)在事务结束时释放。咨询锁详解:如果某些源没有更多 URL 可供抓取,您得到 少于
limit
行。随机选择来源是我的大胆但有根据的猜测,因为没有可用的信息。如果你的
source
table 很大,还有更快的方法:- Best way to select random rows PostgreSQL
refreshFrequency
实际上应该被称为lastest_last
,因为它不是 "frequency",而是timestamp
或date
.
递归选择
要获得完整的限制行数如果可用,请使用RECURSIVE
CTE并迭代所有源,直到您找到足够或找不到更多。
正如我上面提到的,您可能根本不需要列 locked
并且仅使用建议锁(更便宜)进行操作。只需在交易结束时设置 last
,然后再开始下一轮。
WITH RECURSIVE s AS (
SELECT source_id, row_number() OVER (ORDER BY random()) AS rn
FROM source -- you might exclude "empty" sources early ...
)
, page(source_id, rn, ct, url) AS (
SELECT 0, 0, 0, ''::text -- dummy init row
UNION ALL
SELECT s.source_id, s.rn
, CASE WHEN t.url <> ''
THEN p.ct + 1
ELSE p.ct END -- only inc. if url found last round
, (SELECT url
FROM webpage
WHERE source_id = t.source_id
AND (last >= refreshFrequency) IS NOT TRUE
AND locked = FALSE -- may not be needed
AND pg_try_advisory_xact_lock(url) -- only true is free
LIMIT 1 -- get 1 URL per source
) AS url -- try, may come up empty
FROM page p
JOIN s ON s.rn = p.rn + 1
WHERE CASE WHEN p.url <> ''
THEN p.ct + 1
ELSE p.ct END < limit -- your limit here
)
SELECT url
FROM page
WHERE url <> ''; -- exclude '' and NULL
或者,如果您也需要管理 locked
,请将此查询与上述 UPDATE
一起使用。
进一步阅读
你会喜欢 SKIP LOCKED
即将推出的 Postgres 9.5:
相关:
- Atomic UPDATE .. SELECT in Postgres