Debido a que no nos está diciendo mucho de lo que necesita, adivinaré todo, y haremos que sea moderadamente complejo simplificar algunas de las posibles preguntas.
Lo primero sobre MVCC es que en un sistema altamente concurrente desea evitar el bloqueo de la tabla. Como regla general, no puede decir qué no existe sin bloquear la tabla para la transacción. Eso te deja una opción: no confíes INSERT
.
Dejo muy poco como ejercicio para una aplicación de reserva real aquí. No manejamos,
- Overbooking (como característica)
- O qué hacer si no quedan asientos x restantes.
- Buildout a cliente y transacción.
La clave aquí está en UPDATE.
Bloqueamos solo las filas UPDATE
antes de que comience la transacción. Podemos hacer esto porque hemos insertado todos los billetes de los asientos a la venta en la tabla, event_venue_seats
.
Crea un esquema básico
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)
);
Datos de prueba
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);
Y ahora para la transacción de reserva
Ahora tenemos el eventid codificado en uno, debe configurarlo para cualquier evento que desee, customerid
y txnid
esencialmente hacer el asiento reservado y decirle quién lo hizo. La FOR UPDATE
es la clave. Esas filas están bloqueadas durante la actualización.
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;
Actualizaciones
Para reservas programadas
Usaría una reserva programada. Como cuando compra entradas para un concierto, tiene M minutos para confirmar la reserva, o alguien más tiene la oportunidad: Neil McGuigan hace 19 minutos
Lo que harías aquí es establecer el booking.event_venue_seats.txnid
como
txnid int REFERENCES transactions ON DELETE SET NULL
En el segundo en que el usuario reserva el set, el UPDATE
pone en el txnid. Su tabla de transacciones se parece a esto.
CREATE TABLE transactions (
txnid serial PRIMARY KEY,
txn_start timestamp DEFAULT now(),
txn_expire timestamp DEFAULT now() + '5 minutes'
);
Entonces en cada minuto corres
DELETE FROM transactions
WHERE txn_expire < now()
Puede pedirle al usuario que extienda el temporizador cuando esté a punto de caducar. O bien, simplemente deje que elimine txnid
y descienda en cascada liberando los asientos.
Creo que esto se puede lograr mediante el uso de una pequeña mesa doble elegante y algunas restricciones.
Comencemos por alguna estructura (no totalmente normalizada):
Las reservas de la mesa, en lugar de tener una
is_booked
columna, tienen unabooker
columna. Si es nulo, el asiento no está reservado, de lo contrario, este es el nombre (id) del reservante.Agregamos algunos datos de ejemplo ...
Creamos una segunda tabla para reservas, con una restricción:
Esta segunda tabla contendrá una COPIA de las tuplas (session_id, seat_number, booker), con una
FOREIGN KEY
restricción; eso no permitirá que las reservas originales sean ACTUALIZADAS por otra tarea. [Suponiendo que nunca hay dos tareas relacionadas con el mismo booker ; si ese fuera el caso, setask_id
debería agregar una columna determinada .]Siempre que necesitemos hacer una reserva, la secuencia de pasos seguidos dentro de la siguiente función muestra el camino:
Para realmente hacer una reserva, su programa debe intentar ejecutar algo como:
Esto se basa en dos hechos 1. La
FOREIGN KEY
restricción no permitirá que se rompan los datos . 2. ACTUALIZAMOS la tabla de reservas, pero solo INSERTAMOS (y nunca ACTUALIZAMOS ) en la tabla bookings_with_bookers (la segunda tabla).No necesita
SERIALIZABLE
nivel de aislamiento, lo que simplificaría enormemente la lógica. En la práctica, sin embargo, se esperan callejones sin salida , y el programa que interactúa con la base de datos debe estar diseñado para manejarlos.fuente
SERIALIZABLE
porque si se ejecutan dos book_sessions al mismo tiempo,count(*)
el segundo txn podría leer la tabla antes de que la primera book_session se complete con suINSERT
. Como regla general, no es seguro hacer una prueba de inexistencia wo /SERIALIZABLE
.Usaría una
CHECK
restricción para evitar el exceso de reservas y evitar el bloqueo explícito de filas.La tabla podría definirse así:
La reserva de un lote de asientos se realiza por un solo
UPDATE
:Su código debe tener una lógica de reintento. Normalmente, simplemente intente ejecutar esto
UPDATE
. La transacción consistiría en estaUPDATE
. Si no hubo problemas, puede estar seguro de que se ha reservado todo el lote. Si obtiene una infracción de restricción CHECK, debe volver a intentarlo.Entonces, este es un enfoque optimista.
UPDATE
, porque la restricción (es decir, el motor DB) lo hace por usted.fuente
Enfoque 1s - ACTUALIZACIÓN única:
Segundo enfoque - LOOP (plpgsql):
3er enfoque - Tabla de cola:
Las transacciones en sí mismas no actualizan la tabla de asientos. Todos INSERTAN sus solicitudes en una tabla de cola.
Un proceso separado toma todas las solicitudes de la tabla de colas y las maneja, asignando asientos a los solicitantes.
Ventajas:
- Al usar INSERT, se elimina el bloqueo / contención
- No se garantiza el exceso de reservas mediante el uso de un solo proceso para la asignación de asientos
Desventajas:
- La asignación de asientos no es inmediata
fuente