同時グループ予約の戦略は?


8

座席予約データベースを検討してください。nシートのリストがあり、それぞれに属性がありますis_booked。0はそうでないことを意味し、1はそうであることを意味します。それ以上の数でオーバーブッキングがあります。

オーバーブッキングを許可せずに複数のトランザクション(各トランザクションが同時にyシートのグループを予約する)の戦略は何ですか?

私は単にすべての未予約の座席を選択し、ランダムに選択されたyのグループを選択し、それらすべてを予約し、その予約が正しいかどうかを確認します(別名is_bookedの数が1を超えていない場合、座席を予約した別のトランザクションとコミット)、次にコミットします。それ以外の場合は中止して、再試行してください。

これは、Postgresで分離レベルのRead Committedで実行されます。

回答:


5

あなたは私たちにあなたが必要とするものの多くを話していないので、私はすべてについて推測します、そして可能な質問のいくつかを単純化するためにそれを適度に複雑にします。

MVCCについての最初の事柄は、非常に並行性の高いシステムでは、テーブルロックを回避したいということです。原則として、トランザクションのテーブルをロックしないと、存在しないものを特定することはできません。それはあなたに一つの選択肢を残します:に依存しないでくださいINSERT

ここでは、実際の予約アプリの練習問題はほとんど残していません。私たちは扱いません、

  • オーバーブッキング(機能として)
  • または、残りのx席がない場合の対処方法。
  • 顧客と取引の構築。

ここで重要なのは、トランザクションが開始UPDATE.するUPDATE前の行のみをロックすることです。これは、販売するすべての座席チケットをテーブルに挿入したためevent_venue_seatsです。

基本的なスキーマを作成する

CREATE SCHEMA booking;
CREATE TABLE booking.venue (
  venueid    serial PRIMARY KEY,
  venue_name text   NOT NULL
  -- stuff
);
CREATE TABLE booking.seats (
  seatid        serial PRIMARY KEY,
  venueid       int    REFERENCES booking.venue,
  seatnum       int,
  special_notes text,
  UNIQUE (venueid, seatnum)
  --stuff
);
CREATE TABLE booking.event (
  eventid         serial     PRIMARY KEY,
  event_name      text,
  event_timestamp timestamp  NOT NULL
  --stuff
);
CREATE TABLE booking.event_venue_seats (
  eventid    int     REFERENCES booking.event,
  seatid     int     REFERENCES booking.seats,
  txnid      int,
  customerid int,
  PRIMARY KEY (eventid, seatid)
);

テストデータ

INSERT INTO booking.venue (venue_name)
VALUES ('Madison Square Garden');

INSERT INTO booking.seats (venueid, seatnum)
SELECT venueid, s
FROM booking.venue
  CROSS JOIN generate_series(1,42) AS s;

INSERT INTO booking.event (event_name, event_timestamp)
VALUES ('Evan Birthday Bash', now());

-- INSERT all the possible seat permutations for the first event
INSERT INTO booking.event_venue_seats (eventid,seatid)
SELECT eventid, seatid
FROM booking.seats
INNER JOIN booking.venue
  USING (venueid)
INNER JOIN booking.event
  ON (eventid = 1);

そして今、予約取引のために

これでeventidが1にハードコードされました。これを任意のイベントに設定しcustomeridtxnid基本的に座席を予約して、誰がそれを行ったかを通知する必要があります。FOR UPDATEキーです。これらの行は、更新中にロックされます。

UPDATE booking.event_venue_seats
SET customerid = 1,
  txnid = 1
FROM (
  SELECT eventid, seatid
  FROM booking.event_venue_seats
  JOIN booking.seats
    USING (seatid)
  INNER JOIN booking.venue
    USING (venueid)
  INNER JOIN booking.event
    USING (eventid)
  WHERE txnid IS NULL
    AND customerid IS NULL
    -- for which event
    AND eventid = 1
  OFFSET 0 ROWS
  -- how many seats do you want? (they're all locked)
  FETCH NEXT 7 ROWS ONLY
  FOR UPDATE
) AS t
WHERE
  event_venue_seats.seatid = t.seatid
  AND event_venue_seats.eventid = t.eventid;

アップデート

時限予約の場合

時限予約を使用します。コンサートのチケットを購入するときのように、予約を確定するまでにM分かかるか、他の誰かがチャンスを手に入れます– Neil McGuigan 19分前

あなたがここにするだろうが設定されbooking.event_venue_seats.txnid

txnid int REFERENCES transactions ON DELETE SET NULL

2番目に、ユーザーはシートを予約し、txnidにUPDATE書き込みます。トランザクションテーブルは次のようになります。

CREATE TABLE transactions (
  txnid       serial PRIMARY KEY,
  txn_start   timestamp DEFAULT now(),
  txn_expire  timestamp DEFAULT now() + '5 minutes'
);

その後、毎分実行します

DELETE FROM transactions
WHERE txn_expire < now()

有効期限が近づいたときに、タイマーを延長するようにユーザーに促すことができます。または、それを削除しtxnidてカスケードダウンさせ、シートを解放します。


これは優れたインテリジェントなアプローチです。トランザクションテーブルは、2番目の予約テーブルのロックの役割を果たします。と余分な用途があります。
joanolo 2017年

「booking transaction」セクションのupdateステートメントの内部selectサブクエリで、event_venue_seatsにまだ格納されていないデータを使用していないのに、なぜ座席、会場、イベントに参加するのですか?
Ynv 2018年

1

これは、少し凝ったダブルテーブルといくつかの制約を使用することで達成できると思います。

いくつかの(完全に正規化されていない)構造から始めましょう。

/* Everything goes to one schema... */
CREATE SCHEMA bookings ;
SET search_path = bookings ;

/* A table for theatre sessions (or events, or ...) */
CREATE TABLE sessions
(
    session_id integer /* serial */ PRIMARY KEY,
    session_theater TEXT NOT NULL,   /* Should be normalized */
    session_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    performance_name TEXT,           /* Should be normalized */
    UNIQUE (session_theater, session_timestamp) /* Alternate natural key */
) ;

/* And one for bookings */
CREATE TABLE bookings
(
    session_id INTEGER NOT NULL REFERENCES sessions (session_id),
    seat_number INTEGER NOT NULL /* REFERENCES ... */,
    booker TEXT NULL,
    PRIMARY KEY (session_id, seat_number),
    UNIQUE (session_id, seat_number, booker) /* Needed redundance */
) ;

テーブルの予約は、is_booked列の代わりに列を持っていbookerます。nullの場合、座席は予約されません。それ以外の場合、これは予約者の名前(id)です。

いくつかのサンプルデータを追加します...

-- Sample data
INSERT INTO sessions 
    (session_id, session_theater, session_timestamp, performance_name)
VALUES 
    (1, 'Her Majesty''s Theatre', 
        '2017-01-06 19:30 Europe/London', 'The Phantom of the Opera'),
    (2, 'Her Majesty''s Theatre', 
        '2017-01-07 14:30 Europe/London', 'The Phantom of the Opera'),
    (3, 'Her Majesty''s Theatre', 
        '2017-01-07 19:30 Europe/London', 'The Phantom of the Opera') ;

-- ALl sessions have 100 free seats 
INSERT INTO bookings (session_id, seat_number)
SELECT
    session_id, seat_number
FROM
    generate_series(1, 3)   AS x(session_id),
    generate_series(1, 100) AS y(seat_number) ;

予約用に2つ目のテーブルを作成しますが、1つの制限があります。

CREATE TABLE bookings_with_bookers
(
    session_id INTEGER NOT NULL,
    seat_number INTEGER NOT NULL,
    booker TEXT NOT NULL,
    PRIMARY KEY (session_id, seat_number)
) ;

-- Restraint bookings_with_bookers: they must match bookings
ALTER TABLE bookings_with_bookers
  ADD FOREIGN KEY (session_id, seat_number, booker) 
  REFERENCES bookings.bookings (session_id, seat_number, booker) MATCH FULL
   ON UPDATE RESTRICT ON DELETE RESTRICT
   DEFERRABLE INITIALLY DEFERRED;

この2番目のテーブルには、(session_id、seat_number、booker)タプルのCOPYが含まれ、1つのFOREIGN KEY制約があります。元の予約を別のタスクで更新することできません。[同じ予約者を扱う2つのタスクは決してないと仮定します。その場合は、特定のtask_id列を追加する必要があります。]

予約を行う必要があるときはいつでも、次の関数内で実行される一連のステップが方法を示します。

CREATE or REPLACE FUNCTION book_session 
    (IN _booker text, IN _session_id integer, IN _number_of_seats integer) 
RETURNS integer  /* number of seats really booked */ AS
$BODY$

DECLARE
    number_really_booked INTEGER ;
BEGIN
    -- Choose a random sample of seats, assign them to the booker.

    -- Take a list of free seats
    WITH free_seats AS
    (
    SELECT
        b.seat_number
    FROM
        bookings.bookings b
    WHERE
        b.session_id = _session_id
        AND b.booker IS NULL
    ORDER BY
        random()     /* In practice, you'd never do it */
    LIMIT
        _number_of_seats
    FOR UPDATE       /* We want to update those rows, and book them */
    )

    -- Update the 'bookings' table to have our _booker set in.
    , update_bookings AS 
    (
    UPDATE
        bookings.bookings b
    SET
        booker = _booker
    FROM
        free_seats
    WHERE
        b.session_id  = _session_id AND 
        b.seat_number = free_seats.seat_number
    RETURNING
        b.session_id, b.seat_number, b.booker
    )

    -- Insert all this information in our second table, 
    -- that acts as a 'lock'
    , insert_into_bookings_with_bookers AS
    (
    INSERT INTO
        bookings.bookings_with_bookers (session_id, seat_number, booker)
    SELECT
        update_bookings.session_id, 
        update_bookings.seat_number, 
        update_bookings.booker
    FROM
        update_bookings
    RETURNING
        bookings.bookings_with_bookers.seat_number
    )

    -- Count real number of seats booked, and return it
    SELECT 
        count(seat_number) 
    INTO
        number_really_booked
    FROM
        insert_into_bookings_with_bookers ;

    RETURN number_really_booked ;
END ;
$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF STRICT
COST 10000 ;

実際に予約するには、プログラムは次のようなものを実行する必要があります。

-- Whenever we wich to book 37 seats for session 2...
BEGIN TRANSACTION  ;
SELECT
    book_session('Andrew the Theater-goer', 2, 37) ;

/* Three things can happen:
    - The select returns the wished number of seats  
         => COMMIT 
           This can cause an EXCEPTION, and a need for (implicit)
           ROLLBACK which should be handled and the process 
           retried a number of times
           if no exception => the process is finished, you have your booking
    - The select returns less than the wished number of seats
         => ROLLBACK and RETRY
           we don't have enough seats, or some rows changed during function
           execution
    - (There can be a deadlock condition... that should be handled)
*/
COMMIT /* or ROLLBACK */ TRANSACTION ;

これは二つの事実に依存している1. FOREIGN KEY制約は、データをすることができません壊れました。2.予約テーブルをUPDATEしますが、bookings_with_bookers 1(2番目のテーブル)に対してはINSERTのみUPDATEはしない)のみを行います。

SERIALIZABLEロジックを大幅に簡略化する分離レベルは必要ありません。ただし、実際にはデッドロックが予想され、データベースと対話するプログラムはそれらを処理するように設計する必要があります。


SERIALIZABLE2つのbook_sessionが同時に実行される場合count(*)、最初のbook_sessionがで完了する前に、2番目のtxnからテーブルが読み取られる可能性があるため、これは必要INSERTです。原則として、存在しないかどうかをテストするのは安全ではありませんSERIALIZABLE
エヴァンキャロル

@EvanCarroll:2つのテーブルを組み合わせてCTEを使用すると、この必要性を回避できると思います。制約により、トランザクションの最後にすべてが一貫している、または中止されることが保証されるという事実を試してみます。これは、serializableと非常によく似た方法で動作します。
joanolo 2017年

1

CHECK制約を使用してオーバーブッキングを防ぎ、行の明示的なロックを回避します。

テーブルは次のように定義できます。

CREATE TABLE seats
(
    id serial PRIMARY KEY,
    is_booked int NOT NULL,
    extra_info text NOT NULL,
    CONSTRAINT check_overbooking CHECK (is_booked >= 0 AND is_booked <= 1)
);

座席のバッチの予約は単一で行われますUPDATE

UPDATE seats
SET is_booked = is_booked + 1
WHERE 
    id IN
    (
        SELECT s2.id
        FROM seats AS s2
        WHERE
            s2.is_booked = 0
        ORDER BY random() -- or id, or some other order to choose seats
        LIMIT <number of seats to book>
    )
;
-- in practice use RETURNING to get back a list of booked seats,
-- or prepare the list of seat ids which you'll try to book
-- in a separate step before this UPDATE, not on the fly like here.

コードには再試行ロジックが必要です。通常は、これを実行してみてくださいUPDATE。トランザクションはこれで構成されますUPDATE。問題がなければ、バッチ全体が予約されたことを確認できます。CHECK制約違反が発生した場合は、再試行する必要があります。

したがって、これは楽観的なアプローチです。

  • 明示的にロックしないでください。
  • 変更を試みてください。
  • 制約に違反している場合は、再試行してください。
  • UPDATE制約(つまり、DBエンジン)が自動的に行うため、の後に明示的なチェックは必要ありません。

1

1秒アプローチ-単一更新:

UPDATE seats
SET is_booked = is_booked + 1
WHERE seat_id IN
(SELECT seat_id FROM seats WHERE is_booked = 0 LIMIT y);

2番目のアプローチ-LOOP(plpgsql):

v_counter:= 0;
WHILE v_counter < y LOOP
  SELECT seat_id INTO STRICT v_seat_id FROM seats WHERE is_booked = 0 LIMIT 1;
  UPDATE seats SET is_booked = 1 WHERE seat_id = v_seat_id AND is_booked = 0;
  GET DIAGNOSTICS v_rowcount = ROW_COUNT;
  IF v_rowcount > 0 THEN v_counter:= v_counter + 1; END IF;
END LOOP;

3番目のアプローチ-キューテーブル:

トランザクション自体、座席表を更新ません。彼らはすべて自分のリクエストをキューテーブルに挿入します。別のプロセスは、キュー表からのすべての要求を受け取り、要求者への議席を割り当てることによって、それらを処理します。

利点: -INSERTを使用することで
、ロック/競合が排除されます
-シート割り当てに単一のプロセスを使用することでオーバーブッキングが保証されません

短所:
-座席の割り当てがすぐにできない

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.