座席予約データベースを検討してください。 nシートのリストがあり、それぞれに属性is_booked
。 0はそうでないことを意味し、1はそうであることを意味します。それ以上の数でオーバーブッキングがあります。
オーバーブッキングを許可せずに、複数のトランザクション(各トランザクションが同時にyシートのグループを予約する)の戦略は何ですか?
私は単にすべての未予約の座席を選択し、ランダムに選択されたyのグループを選択し、それらすべてを予約し、その予約が正しいかどうかを確認します(別名is_bookedの数が1を超えていないため、座席を予約した別のトランザクションとコミット)、次にコミットします。それ以外の場合は中止して、再試行してください。
これは、Postgresで分離レベルのRead Committedで実行されます。
あなたは私たちにあなたが必要とするものの多くを話していないので、私はすべてを推測します、そして可能な質問のいくつかを単純化するためにそれを適度に複雑にします。
MVCCについての最初のことは、非常に並行性の高いシステムでは、テーブルロックを回避したいということです。原則として、トランザクションのテーブルをロックしないと、存在しないものを特定することはできません。そのため、1つの選択肢が残ります。INSERT
に依存しないでください。
ここでは実際の予約アプリの練習問題はほとんど残していません。私たちは扱いません、
ここでのキーは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にハードコードされました。これを任意のイベントに設定する必要があります。customerid
およびtxnid
は基本的に座席を予約済みにし、誰が行ったかを伝えますそれ。 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番目にユーザーがシートを予約すると、UPDATE
がtxnidに配置されます。トランザクションテーブルは次のようになります。
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
を削除し、カスケードダウンしてシートを解放します。
UPDATE seats
SET is_booked = is_booked + 1
WHERE seat_id IN
(SELECT seat_id FROM seats WHERE is_booked = 0 LIMIT y);
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;
トランザクション自体更新しないでください座席表。すべて[〜#〜] insert [〜#〜]リクエストをキューテーブルに入れます。
A 個別のプロセスは、リクエスタにシートを割り当てることにより、キューテーブルからすべてのリクエストを取得して処理します。
利点:
-INSERTを使用することにより、ロック/競合が排除されます
-座席の割り当てに単一のプロセスを使用することにより、オーバーブッキングは保証されません
短所:
-座席の割り当ては即時ではありません
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エンジン)が自動的に行うからです。これは、少し凝ったdouble tableといくつかの制約を使用することで達成できると思います。
いくつかの(完全に正規化されていない)構造から始めましょう:
/* 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) ;
予約のためにsecondテーブルを作成しますが、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
制約が含まれます。 notは、元の予約が別のタスクによって更新されることを許可します。 [同じbookerを処理する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 ;
これは2つの事実に依存します。1[FOREIGN KEY
]制約では、データを破損にすることはできません。 2.予約テーブルを更新しますが、bookings_with_bookers 1(2番目のテーブル)に対してINSERTのみ(決して[〜#〜] update [〜#〜])は行いません。
ロジックを大幅に簡略化するSERIALIZABLE
分離レベルは必要ありません。ただし、実際にはdeadlocksが想定されており、データベースと対話するプログラムはそれらを処理するように設計する必要があります。