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;

我尝试了两种不同的事务隔离级别:

总的来说,我对 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",而是 timestampdate .

递归选择

要获得完整的限制行数如果可用,请使用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