Considérez une base de données de réservation de siège. Il y a une liste de n places et chacun a un attribut is_booked
. 0 signifie que ce n'est pas, 1 signifie que c'est. Tout nombre plus élevé et il y a un surréservement.
Quelle est la stratégie d'avoir plusieurs transactions (où chaque transaction réservera un groupe de sièges y simultanément) sans permettre des réservations?
Je sélectionnerais simplement tous les sièges non gréables, sélectionnez un groupe sélectionné de manière aléatoire de Y d'entre eux, réservez-les tous et vérifiez si cette réservation est correcte (AKA Le numéro de IS_Booked n'est pas sur un, ce qui signifierait une autre transaction ayant réservé le siège et engagé), puis commettre. sinon abandonner et essayer à nouveau.
Ceci est exécuté au niveau d'isolement lu que vous avez commis à Postgres.
Parce que vous ne nous disiez pas beaucoup de ce dont vous avez besoin, je vais deviner pour tout, et nous le ferons modérément complexe pour simplifier certaines des questions possibles.
La première chose à propos de MVCC est que dans un système hautement concoureur, vous souhaitez éviter le verrouillage de table. En règle générale, vous ne pouvez pas dire ce qui n'existe pas sans verrouiller la table pour la transaction. Cela vous laisse une option: Ne comptez pas sur INSERT
.
Je laisse très peu d'exercice pour une vraie application de réservation ici. Nous ne gérons pas,
La clé ici est dans le UPDATE.
Nous verrouillons uniquement les lignes pour UPDATE
avant que la transaction ne commence. Nous pouvons faire cela parce que nous avons inséré tous les tickets de siège à la vente dans la table, 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);
Maintenant, nous avons l'Epidit Hard CoDed à un, vous devez définir ceci sur n'importe quel événement que vous souhaitez, customerid
et txnid
_ Essentiellement faire le siège réservé et vous dire qui a fait qui a fait il. Le FOR UPDATE
C est la clé. Ces lignes sont verrouillées pendant la mise à jour.
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;
Vous utiliseriez une réservation chronométrée. Comme lorsque vous achetez des billets pour un concert, vous avez m minutes pour confirmer la réservation, ou que quelqu'un d'autre a la chance - Neil McGuigan il y a 19 minutes
Ce que vous feriez ici est défini le booking.event_venue_seats.txnid
comme
txnid int REFERENCES transactions ON DELETE SET NULL
La seconde que l'utilisateur se réserve le SEET, le UPDATE
met dans le TXNID. Votre table de transaction ressemble à quelque chose comme ça.
CREATE TABLE transactions (
txnid serial PRIMARY KEY,
txn_start timestamp DEFAULT now(),
txn_expire timestamp DEFAULT now() + '5 minutes'
);
Puis dans chaque minute, vous courez
DELETE FROM transactions
WHERE txn_expire < now()
Vous pouvez inviter l'utilisateur à étendre la minuterie lors de la prochaine expiration. Ou, laissez-la simplement supprimer le txnid
et cascade Down, libérant ainsi les sièges.
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;
Les transactions elles-mêmes Ne mettez pas à jour la table des sièges. Ils sont tous INSERT leurs demandes dans une table de file d'attente.
[.____] a processus séparé prend toutes les demandes de la table d'attente et les gère, en allouant des sièges aux demandeurs.
Avantages:
[.____] - En utilisant Insert, le verrouillage/la conflit est éliminé
- Aucun surbooking n'est assuré en utilisant un seul processus d'allocation de siège
Désavantages:
[.____] - L'allocation de siège n'est pas immédiate
J'utiliserais un CHECK
contrainte pour empêcher le surbooking et éviter le verrouillage explicite des rangées.
La table pourrait être définie comme ceci:
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)
);
La réservation d'un lot de sièges est effectuée par un seul 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.
Votre code devrait avoir une nouvelle logique. Normalement, essayez simplement d'exécuter ce UPDATE
. La transaction serait composée de celui-ci UPDATE
. S'il n'y avait aucun problème, vous pouvez être sûr que tout le lot a été réservé. Si vous obtenez une violation de la contrainte de contrôle, vous devriez réessayer.
C'est donc une approche optimiste.
UPDATE
, car la contrainte (c'est-à-dire le moteur DB) pour vous.Je pense que cela peut être accompli par l'utilisation d'un peu de fantaisie double table et certaines contraintes.
Commençons par une structure (non normalisée):
/* 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 */
) ;
Les réservations de table, au lieu d'avoir un is_booked
colonne a une colonne booker
. Si c'est NULL, le siège n'est pas réservé, sinon il s'agit du nom (ID) du booker.
Nous ajoutons d'exemple de données ...
-- 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) ;
Nous créons une table A deuxième pour les réservations, avec une restriction:
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;
Cette deuxième table contiendra une copie des tuples (session_id, siège_number, booker), avec un FOREIGN KEY
contrainte; Ce sera non Autoriser les réservations originales à mettre à jour par une autre tâche. [En supposant qu'il n'y a jamais deux tâches traitant du même booker; Si c'était le cas, un certain task_id
la colonne doit être ajoutée.]
Chaque fois que nous devons faire une réservation, la séquence des étapes suivies dans la fonction suivante montre la manière suivante:
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 ;
Pour vraiment faire une réservation, votre programme devrait essayer d'exécuter quelque chose comme:
-- 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 ;
Cela s'appuie sur deux faits 1. Le FOREIGN KEY
La contrainte n'autorise pas les données à être cassées. 2. Nous mettons à jour la table des réservations, mais seul insert (et jamais MISE À JOUR ) sur les réservations_with_bookers One (la deuxième table).
Il n'a pas besoin de SERIALIZABLE
niveau d'isolement, ce qui simplifierait grandement la logique. En pratique, cependant, impasse doivent être attendus et le programme interagissant avec la base de données doit être conçu pour les gérer.