12

我正在使用 python(不是很相关)和 Postgresql(如果相关,则为 9.2)实现一个简单的基于 Web 的 RSS 阅读器。数据库架构如下(基于RSS格式):

CREATE TABLE feed_channel
(
    id SERIAL PRIMARY KEY,
    name TEXT,
    link TEXT NOT NULL,
    title TEXT
);
CREATE TABLE feed_content
(
    id SERIAL PRIMARY KEY,
    channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE,
    guid TEXT UNIQUE NOT NULL,
    title TEXT,
    link TEXT,
    description TEXT,
    pubdate TIMESTAMP
);

当我创建一个新频道(并查询更新的提要信息)时,我请求提要,将其数据插入 feed_channel 表,选择新插入的 ID - 或现有 ID 以避免重复 - 然后将提要数据添加到 feed_content 表. 一个典型的场景是:

  1. 查询提要 url,抓取提要标题和所有当前内容
  2. 如果不存在,则将提要标题插入 feed_channel ... 如果已存在,则获取现有 ID
  3. 对于每个提要项目,插入 feed_content 表并引用存储的频道 ID

这是一个标准的“如果不存在则插入,但返回相关 ID”问题。为了解决这个问题,我实现了以下存储过程:

CREATE OR REPLACE FUNCTION channel_insert(
  p_link feed_channel.link%TYPE,
  p_title feed_channel.title%TYPE
) RETURNS feed_channel.id%TYPE AS $$
  DECLARE
    v_id feed_channel.id%TYPE;
  BEGIN
    SELECT id
    INTO v_id
    FROM feed_channel
    WHERE link=p_link AND title=p_title
    LIMIT 1;

    IF v_id IS NULL THEN
      INSERT INTO feed_channel(name,link,title)
      VALUES (DEFAULT,p_link,p_title)
      RETURNING id INTO v_id;
    END IF;

    RETURN v_id;

  END;
$$ LANGUAGE plpgsql;

然后将其称为“选择通道插入(链接,标题);” 如果不存在,则从我的应用程序中插入,然后返回相关行的 ID,无论它是插入还是刚刚找到(上面列表中的步骤 2)。

这很好用!

然而,我最近开始想知道如果这个过程使用相同的参数同时执行两次会发生什么。让我们假设以下内容:

  1. 用户 1 尝试添加一个新频道,从而执行 channel_insert
  2. 几毫秒后,用户 2 尝试添加相同的频道并执行 channel_insert
  3. 用户 1 对现有行的检查完成,但在插入完成之前,用户 2 的检查完成并说没有现有行。

这会是 PostgreSQL 中潜在的竞争条件吗?解决此问题以避免此类情况的最佳方法是什么?是否可以使整个存储过程原子化,即它只能同时执行一次?

我尝试的一个选项是使字段唯一,然后尝试首先插入,如果出现异常,请选择现有的...这有效,但是,SERIAL 字段会随着每次尝试而增加,从而在序列中留下很多空白. 我不知道从长远来看这是否会成为问题(可能不是),但有点烦人。也许这是首选的解决方案?

感谢您的任何反馈。这个级别的 PostgreSQL 魔法超出了我的能力范围,所以任何反馈都将不胜感激。

4

3 回答 3

5

这会是 PostgreSQL 中潜在的竞争条件吗?

是的,事实上它可以在任何数据库引擎中。

解决此问题以避免此类情况的最佳方法是什么?

这是一个加载的问题,需要深入了解多个用户对数据库的使用情况。不过,我会给你一些选择。简而言之,您唯一的选择是LOCK在此过程中对表进行锁定,但如何锁定该表将取决于数据库在一天中的使用情况

让我们从基本的开始LOCK

LOCK TABLE feed_channel

这将使用ACCESS EXCLUSIVElock 选项锁定表。

与所有模式的锁冲突(ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE 和 ACCESS EXCLUSIVE)。这种模式保证持有者是唯一以任何方式访问表的事务。

现在,这是可用的最严格的锁,并且肯定会解决竞争条件,但可能不是您想要的。这是你必须决定的事情。所以,虽然很明显你将不得不LOCK上桌,但不清楚如何。

你还有什么要决定的?

  1. 你想怎么LOCK上桌?研究该链接上的锁定选项以做出决定。
  2. 你想在哪里LOCK吃饭?或者换句话说,你想LOCK在函数的顶部(我认为你是根据可能的竞争条件做的),还是你只是想LOCKINSERT?

是否可以使整个存储过程原子化,即它只能同时执行一次?

不,任何连接到数据库的人都可以执行代码。


我希望这对指导您有所帮助。

于 2012-12-26T14:23:47.967 回答
4

这里有一个不可避免的“竞赛”,因为两个会话不能“看到”彼此未提交的行。在发生冲突时,会话只能回滚(可能到保存点)并重试。这通常意味着:引用对方新插入的行,而不是创建私有副本。

这里有一个数据建模问题:feed_channel 似乎有很多候选键,并且来自 feed_content 的级联规则可能会孤立很多 feed_content 的行(我假设 content-> channel 是 1::M 关系;多个内容-row 可以指同一个频道)

最后,feed_channel 表至少需要自然键 {link,title}。这就是插入/不存在的全部意义所在。(以及此功能的全部目的)

我清理了一些功能。不需要 IF 构造,首先执行INSERT WHERE NOT EXISTS 也同样有效,甚至可能更好。

DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;

CREATE TABLE feed_channel
    ( id SERIAL PRIMARY KEY
    , name TEXT
    , link TEXT NOT NULL
    , title TEXT NOT NULL -- part of PK :: must be not nullable
    , CONSTRAINT feed_channel_nat UNIQUE (link,title) -- the natural key
);

CREATE TABLE feed_content
    ( id SERIAL PRIMARY KEY
    , channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE
    , guid TEXT UNIQUE NOT NULL -- yet another primary key
    , title TEXT --
    , link TEXT  -- title && link appear to be yet another candidate key
    , description TEXT
    , pubdate TIMESTAMP
    );

-- NOTE: omitted original function channel_insert() for brevity
CREATE OR REPLACE FUNCTION channel_insert_wp(
  p_link feed_channel.link%TYPE,
  p_title feed_channel.title%TYPE
) RETURNS feed_channel.id%TYPE AS $body$
   DECLARE
    v_id feed_channel.id%TYPE;
  BEGIN
      INSERT INTO feed_channel(link,title)
      SELECT p_link,p_title
      WHERE NOT EXISTS ( SELECT *
        FROM feed_channel nx
        WHERE nx.link= p_link
        AND nx.title= p_title
        )
        ;
    SELECT id INTO v_id
    FROM feed_channel ex
    WHERE ex.link= p_link
    AND ex.title= p_title
        ;

    RETURN v_id;

  END;
$body$ LANGUAGE plpgsql;

SELECT channel_insert('Bogus_link', 'Bogus_title');
SELECT channel_insert_wp('Bogus_link2', 'Bogus_title2');

SELECT * FROM feed_channel;

结果:

DROP SCHEMA
CREATE SCHEMA
SET
NOTICE:  CREATE TABLE will create implicit sequence "feed_channel_id_seq" for serial column "feed_channel.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "feed_channel_pkey" for table "feed_channel"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "feed_channel_nat" for table "feed_channel"
CREATE TABLE
NOTICE:  CREATE TABLE will create implicit sequence "feed_content_id_seq" for serial column "feed_content.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "feed_content_pkey" for table "feed_content"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "feed_content_guid_key" for table "feed_content"
CREATE TABLE
NOTICE:  type reference feed_channel.link%TYPE converted to text
NOTICE:  type reference feed_channel.title%TYPE converted to text
NOTICE:  type reference feed_channel.id%TYPE converted to integer
CREATE FUNCTION
NOTICE:  type reference feed_channel.link%TYPE converted to text
NOTICE:  type reference feed_channel.title%TYPE converted to text
NOTICE:  type reference feed_channel.id%TYPE converted to integer
CREATE FUNCTION
 channel_insert 
----------------
              1
(1 row)

 channel_insert_wp 
-------------------
                 2
(1 row)

 id | name |    link     |    title     
----+------+-------------+--------------
  1 |      | Bogus_link  | Bogus_title
  2 |      | Bogus_link2 | Bogus_title2
(2 rows)
于 2012-12-26T15:59:09.513 回答
3

您最重要的问题是 aserial不能为feed_channel表创建一个好的主键。主键应该是(link, title)或者只是(link)如果title可以是null。然后,任何插入现有提要的尝试都会引发主键错误。

顺便说一句v_idnull只要titlenull

WHERE link=p_link AND title=p_title
于 2012-12-21T13:17:45.270 回答