PostgreSQL并发事务问题

时间:2015-04-22 19:24:42

标签: sql postgresql concurrency transactions locking

我目前正在构建一个抓取工具。多个爬网工作者访问同一个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,因为数据已被冻结&#34;从交易首次提交时起(我认为)

我对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
);

2 个答案:

答案 0 :(得分:3)

澄清

这个问题留有解释空间。这就是我理解任务的方式:

锁定最多符合某些条件并且尚未锁定的limit个网址。要在源上分散负载,每个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 *;

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

其他要点

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

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

  • 如果某些来源没有要抓取的网址,则小于limit 行。

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

  • refreshFrequency应该被称为lastest_last,因为它不是&#34;频率&#34;,而是timestamp或{{ 1}}。

递归交替

要获取完整的限制(如果可用),请使用date CTE并迭代所有来源,直到找到足够的或者找不到更多来源

正如我上面提到的,您可能根本不需要列RECURSIVE,只使用咨询锁(更便宜)。只需在交易结束时设置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一起使用。

进一步阅读

在即将到来的 Postgres 9.5 中,您会喜欢 UPDATE

相关:

答案 1 :(得分:0)

首先尝试:

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条件。