【问题标题】:PostgreSQL concurrent transaction issuesPostgreSQL 并发事务问题
【发布时间】:2015-06-30 16:04:36
【问题描述】:

我目前正在构建一个爬虫。多个爬虫工作人员访问同一个 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 是域名(字符串)
  • last 是最后一次抓取页面的时间(日期)
  • locked 是一个布尔值,设置为指示当前正在抓取网页(布尔值)

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

  • ISOLATION LEVEL SERIALIZABLE,我收到类似 could not serialize access due to concurrent update 的错误
  • ISOLATION LEVEL READ COMMITTED,我从并发事务中得到重复的urls,因为从事务第一次提交时数据被“冻结”(我认为)

总的来说,我对 PostgreSQL 和 SQL 还很陌生,所以我真的不确定我能做些什么来解决这个问题。

更新:
PostgreSQL版本为9.2.x。
webpage表定义:

CREATE TABLE webpages (
  last timestamp with time zone,
  locked boolean DEFAULT false,
  url text NOT NULL,
  source character varying(255) PRIMARY KEY
);

【问题讨论】:

  • @ErwinBrandstetter:我在查询下方描述了不同隔离级别以及不同列类型和描述的问题。我正在使用 PostgreSQL 9.2。
  • 有干净的解决方案,具体取决于具体要求,不清楚。特别是,如果没有解释,这毫无意义:DISTINCT ON (source) url,也没有ORDER BY。它应该做什么?是否要根据source 检索一个(任意)url
  • 我想按源分散我发出的请求,以免请求淹没相同的主机。我不使用 ORDER BY 的原因是我需要为每个 url 找到不同的来源,而我没有使用 GROUP BY 的原因是因为我需要返回 url 但这是不可能的,因为如果你GROUP BY 源然后 url 不在 SELECT 中。
  • 显示所有约束的实际表定义会有所帮助(psql 中的\d webpages)。解释也应该在问题中。请编辑以提供完整的图片。您可以使用FOR UPDATE 行级锁或咨询锁来解决这个问题,具体取决于整个情况...
  • 另外,您有单独的source 表吗?

标签: sql postgresql concurrency transactions locking


【解决方案1】:

澄清

这个问题留有解释的余地​​。这就是我对任务的理解:

锁定最多limit 满足某些条件但尚未锁定的 URL。为了分散来源的负载,每个 URL 都应该来自不同的来源。

数据库设计

假设一个单独的表source:这使工作更快更容易。如果您没有这样的表,请创建它,无论如何它都是正确的设计:

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:

带有咨询锁的基本解决方案

即使在默认的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 *;

或者,您可以使用only咨询锁,而根本不使用表列locked。基本上只需运行SELECT 语句。锁一直保留到事务结束。您可以改用pg_try_advisory_lock() 来保持锁定直到会话结束。只有UPDATE一次在完成后设置last(并且可能释放咨询锁)。

其他要点

  • 在 Postgres 9.3 或更高版本中,您将使用 LATERAL 连接而不是相关子查询。

  • 我选择 pg_try_advisory_xact_lock() 是因为可以(并且应该)在事务结束时释放锁。咨询锁详解:

  • 如果某些来源没有更多网址可供抓取,您将获得少于limit 行。

  • 来源的随机选择是我的疯狂但有根据的猜测,因为信息不可用。如果您的source 表很大,有更快的方法:

  • refreshFrequency 真的应该被称为lastest_last,因为它不是“频率”,而是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 一起使用。

进一步阅读

在即将推出的 Postgres 9.5 中您会喜欢 SKIP LOCKED

相关:

【讨论】:

  • 目前,source 只是一个简单的列,具有字符串类型,每个网页记录都会重复。
  • 在这两个表(一个用于源,一个用于网页)架构中,当ORDER BY random()返回没有页面匹配(last &gt;= refreshFrequency) IS NOT TRUE条件的源时,会不会有问题?跨度>
  • 我还将webpages 表添加到我原来的问题帖子中。
  • @m_vdbeek:我添加了一个替代解决方案和更多信息。
【解决方案2】:

第一次尝试:

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 的记录。
假设表中有以下记录:

URL       locked
----------------
A         false
A         true

更新中的子查询将返回A
然后将执行外部更新:

   UPDATE webpages
    SET locked = TRUE
    WHERE url IN ( 'A' )

实际上包含 url=A 的表中的所有记录都将被更新,
考虑到它们在 locked 列中的值。

您需要将与子查询中相同的WHERE 条件应用于外部更新。

【讨论】:

  • 是否仍应将其包装在事务中?如果是,在哪个隔离级别?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-28
  • 2021-09-15
  • 2011-05-24
相关资源
最近更新 更多