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. フィード アイテムごとに、格納されているチャネル ID への参照を指定して feed_content テーブルに挿入します。

これは標準的な「存在しない場合は挿入するが、関連する 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)。

これはうまくいきます!

しかし、最近、このプロシージャが同じ引数で同時に 2 回実行されたらどうなるのだろうと考え始めました。次のように仮定します。

  1. ユーザー 1 が新しいチャネルを追加しようとして、channel_insert を実行します
  2. 数ミリ秒後、ユーザー 2 は同じチャネルを追加しようとし、channel_insert も実行しようとします。
  3. ユーザー 1 の既存の行のチェックは完了しますが、挿入が完了する前に、ユーザー 2 のチェックが完了し、既存の行がないことが示されます。

これは PostgreSQL で競合状態になる可能性がありますか? このようなシナリオを回避するために、この問題を解決する最善の方法は何ですか? ストアド プロシージャ全体をアトミックに作成することは可能ですか。つまり、同時に 1 回しか実行できないようにすることはできますか?

私が試した1つのオプションは、フィールドを一意にしてから最初に挿入しようとすることでした.例外の場合は、代わりに既存のものを選択してください. . それが長期的に問題になるかどうかはわかりませんが(おそらくそうではないでしょう)、ちょっと面倒です。おそらくこれが好ましい解決策ですか?

フィードバックをお寄せいただきありがとうございます。このレベルの PostgreSQL マジックは私には理解できないので、フィードバックをいただければ幸いです。

4

3 に答える 3

5

これは PostgreSQL で競合状態になる可能性がありますか?

はい、実際、どのデータベース エンジンにもあります。

このようなシナリオを回避するために、この問題を解決する最善の方法は何ですか?

これは負荷の高い質問であり、複数のユーザーによるデータベースの使用に関する詳細な知識が必要です。ただし、いくつかのオプションを提供します。LOCKつまり、このプロセス中の唯一の選択肢はテーブルに対するものですが、そのテーブルをどのようにロックするかは、データベースが 1 日を通してどのように使用されるかによって異なります。

基本から始めましょうLOCK

LOCK TABLE feed_channel

ACCESS EXCLUSIVEこれにより、 lock オプションを使用してテーブルがロックされます。

すべてのモード (ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE、および ACCESS EXCLUSIVE) のロックと競合します。このモードでは、所有者が何らかの方法でテーブルにアクセスする唯一のトランザクションであることが保証されます。

現在、これは利用可能な最も制限的なロックであり、競合状態は確実に解決されますが、希望どおりではない可能性があります。それはあなたが決めなければならないことです。したがって、テーブルに着かなければならないことは明らかですが、その方法は明確ではありません。LOCK

あなたは何を決定するために残っていますか?

  1. LOCKテーブルにいかがですか?そのリンクでロック オプションを調べて、判断してください。
  2. どこLOCKのテーブルに行きたいまたは、言い換えれば、LOCK先頭にしたいですか (競合状態の可能性に基づいて行うと思います)、それとも単純LOCKに の直前にしINSERTですか?

ストアド プロシージャ全体をアトミックに作成することは可能ですか。つまり、同時に 1 回しか実行できないようにすることはできますか?

いいえ、データベースに接続していれば誰でもコードを実行できます。


これがあなたの指示に役立ったことを願っています。

于 2012-12-26T14:23:47.967 に答える
4

2 つのセッションはコミットされていない行をお互いに「見る」ことができないため、ここでは避けられない「競合」があります。競合が発生した場合、セッションはロールバック (おそらくセーブポイントまで) と再試行しかできませんでした。これは通常、プライベートな複製を作成するのではなく、新しく挿入された他の行を参照することを意味します。

ここにデータ モデリングの問題があります: feed_channel には多くの候補キーがあるように見え、feed_content からのカスケード ルールは多くの feed_content の行を孤立させる可能性があります (content-> channel は 1::M 関係であると仮定します; 複数のコンテンツ-行は同じチャネルを参照できます)

最後に、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

一番の問題は、 aがテーブルserialの適切な主キーにならないことです。feed_channel主キーは(link, title)または である(link)必要titleがありますnull。次に、既存のフィードを挿入しようとすると、主キー エラーが発生します。

ところで、いつでも次のようv_idになります。nulltitlenull

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