【问题标题】:Insert row if not exists leads to race condition?如果不存在插入行会导致竞争条件?
【发布时间】:2012-12-09 00:46:39
【问题描述】:

我正在使用 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. 查询提要网址,抓取提要标题和所有当前内容
  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;

这被称为“select channel_insert(link, ti​​tle);”如果不存在,则从我的应用程序中插入,然后返回相关行的 ID,无论它是已插入还是刚刚找到(上面列表中的第 2 步)。

这很好用!

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

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

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

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

感谢您的任何反馈。这种级别的 PostgreSQL 魔法超出了我的能力范围,因此我们将不胜感激。

【问题讨论】:

  • 无论你做什么,都要小心规范你的链接格式,这样你就不会出现大小写问题(Www.Example.Comwww.example.com),参数顺序问题(?a=b&c=d和@987654327) @) 等
  • 在重复键违规的情况下循环的 plpgsql 函数可以处理服务器端和默认隔离级别的竞争条件,这是 安全 一个典型的 最便宜的stackoverflow.com/questions/15939902/…

标签: postgresql database-design insert primary-key race-condition


【解决方案1】:

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

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

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

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

让我们从基本的LOCK开始:

LOCK TABLE feed_channel

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

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

现在,这是可用的最严格的锁,肯定会解决竞争条件,但可能不是您想要的。这是你必须决定的事情。所以,虽然 clear 你必须 LOCK 桌子,但不是 clear 如何.

你还有什么要决定的?

  1. 如何你想LOCK the table?研究该链接上的锁定选项以做出决定。
  2. 您想在哪里 LOCK 餐桌?或者换句话说,你想LOCK 在函数的 top (我认为你根据可能的竞争条件),或者你只是想在INSERT 之前LOCK

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

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


我希望这有助于指导你。

【讨论】:

    【解决方案2】:

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

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

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

    我稍微清理了函数。不需要 IF 构造,执行 INSERT WHERE NOT EXISTS first 效果一样好,甚至可能更好。

    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)
    

    【讨论】:

    • 如果我完全放弃存储过程而只使用:INSERT INTO feed_channel (source) SELECT %(source)s WHERE NOT EXISTS ( SELECT 1 FROM feed_channel WHERE source=%(source)s ); 假设“源”具有唯一约束,我是否会触发唯一违规?
    【解决方案3】:

    您的首要问题是serial 不能成为feed_channel 表的良好主键。如果title 可以是null,主键应该是(link, title) 或者只是(link)。那么任何插入现有提要的尝试都会引发主键错误。

    顺便说一句,只要 titlenullv_id 就会是 null

    WHERE link=p_link AND title=p_title
    

    【讨论】:

    • 这是一个有趣的想法,但是如果主键由多个字段组成,那么制作引用/外键会不会很痛苦?此外,我首先尝试创建一个唯一约束来获得相同的结果,但是,由于唯一约束我放弃了该方法,因此我还需要在插入失败时返回正确的 ID。此外,我发现当由于约束而突然失败时,处理提交是一件很麻烦的事情。
    • @invictus 如果主键由多个字段组成,为什么创建引用/外键会很痛苦?不会有ID。您不必返回任何内容,因为您已经知道您尝试插入的自然键(链接、标题)。
    猜你喜欢
    • 1970-01-01
    • 2021-09-03
    • 2014-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多