Postgresql 插入行(如果不存在)导致竞争条件?

Postgresql 插入行(如果不存在)导致竞争条件?,postgresql,database-design,insert,primary-key,race-condition,Postgresql,Database Design,Insert,Primary Key,Race Condition,我正在使用python(不太相关)和Postgresql(9.2,如果相关)实现一个简单的基于web的RSS阅读器。数据库模式如下(基于RSS格式): 当我创建一个新频道(并查询更新的提要信息)时,我请求提要,将其数据插入提要频道表,选择新插入的ID(或现有的以避免重复),然后将提要数据添加到提要内容表。典型的情况是: 查询提要url、获取提要标题和所有当前内容 如果不存在,请将提要标头插入提要_通道。。。如果已经存在,则获取现有ID 对于每个提要项,在提要内容表中插入对存储的频道ID的引用 这

我正在使用python(不太相关)和Postgresql(9.2,如果相关)实现一个简单的基于web的RSS阅读器。数据库模式如下(基于RSS格式):

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

  • 查询提要url、获取提要标题和所有当前内容
  • 如果不存在,请将提要标头插入提要_通道。。。如果已经存在,则获取现有ID
  • 对于每个提要项,在提要内容表中插入对存储的频道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尝试添加一个新通道,从而执行通道插入
  • 几毫秒后,用户2尝试添加相同的频道并执行频道插入
  • 用户1对现有行的检查已完成,但在插入完成之前,用户2的检查已完成,并表示没有现有行
  • 在PostgreSQL中,这会是一种潜在的竞争条件吗?解决此问题以避免此类情况的最佳方法是什么?是否可以使整个存储过程原子化,即它只能同时执行一次

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


    谢谢你的反馈。我无法理解PostgreSQL的这种魔力,因此任何反馈都将不胜感激。

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

    顺便说一句,
    v_id
    title
    null
    时将为
    null

    WHERE link=p_link AND title=p_title
    
    在PostgreSQL中,这会是一种潜在的竞争条件吗

    是的,事实上,它会出现在任何数据库引擎中

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

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

    让我们从基本的
    锁开始:

    LOCK TABLE feed_channel
    
    这将使用
    ACCESS EXCLUSIVE
    lock选项锁定表

    与所有模式的锁冲突(访问共享、行共享、行独占、共享更新独占、共享、共享行独占、独占和访问独占)此模式保证持有人是以任何方式访问表的唯一交易。

    现在,这是可用的限制性最强的锁,肯定会解决争用条件,但可能不是您想要的。这是你必须做出的决定。因此,尽管它是清除的您必须
    锁定表,但它不是如何清除的

    剩下你来决定什么

  • 你想怎么做?研究该链接上的锁定选项以做出决定
  • 在哪里是否要
    锁定表?或者换句话说,您是想在函数的顶部
    锁定
    (我认为这是基于可能的竞争条件),还是只想在插入
    之前
    锁定
  • 是否可以使整个存储过程原子化,即它只能同时执行一次

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


    我希望这有助于指导您。

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

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

    最后,feed_channel表至少需要
    自然键{link,title}。这就是插入/不存在的全部内容。(以及该职能的全部目的)

    我稍微整理了一下函数。IF构造是不需要的,首先在不存在的地方插入同样有效,甚至可能更好

    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)
    

    这是一个有趣的想法,但是做一个推荐人/朋友不是很痛苦吗
    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)