2011-02-24 12 views
5

Estoy escribiendo un script que necesita reservar asientos en el cine.Bloquear filas en una tabla para SELECCIONAR y ACTUALIZAR

  1. de usuario le pide 2 plazas
  2. Si hay 2 asientos disponibles, el sistema les ofrece a clientes
  3. cliente puede aceptar o solicitar otros 2 asientos.
  4. Cuando finalmente acepta, los asientos están marcados como "vendidos"

Puesto que puede haber varios usuarios que utilizan el sistema al mismo tiempo, necesito una manera de "bloquear" las filas se ofrecen a los clientes actuales hasta que pase cierto tiempo, o él solicita otros asientos.

Actualmente estoy marcando asientos ofrecidos como "bloqueado" con un ID de cliente, y el uso de SELECT para devolverlos al cliente (esto es para MySQL, pero la base de datos de destino es Postgres)

UPDATE seats SET status = "locked", lock_time = NOW(), lock_id = "lock1" LIMIT 2 
SELECT * FROM seats WHERE lock_id = "lock1" AND lock_time > DATE_SUB(NOW(), INTERVAL 2 MINUTE) 

Hay una problema con eso: si solo hay 1 asiento disponible, seguirá marcado como "bloqueado" y tendré que soltar el bloqueo de inmediato.

También estoy bastante seguro de que hay una manera más inteligente de hacerlo. ¿Cuál es la forma correcta de lidiar con una tarea como esa?

Respuesta

0

Condición de carrera - Creo que esto sería mejor como una inserción en lugar de una actualización. Se pueden ejecutar dos actualizaciones al mismo tiempo y no entrarán en conflicto entre sí. Si tuviera la tabla de "asientos bloqueados", podría hacer referencia al seat_id y hacerlo único. De esa forma, las condiciones de una carrera fallarían. Pero, en cualquier caso, escribí esto como una actualización como lo tienes en la pregunta, aunque podrías cambiarla a una inserción.

Parece que no quiere ser capaz de bloquear los asientos en primer lugar si no hay suficientes disponibles.Esto es fácil con autocombinaciones:

create temp table seats 
(
    id   serial, 
    event_id integer, 
    locked  boolean default false 
); 
insert into seats (event_id) values (1),(1),(1),(2); 
-- this will not lock event_id = 2 since it will not have a high enough count 
update seats 
set locked = true 
from 
(
    -- get the counts so we can drop events without enough seats 
    select count(*), event_id from seats group by event_id 
) as sum, 
(
    -- you can not put limits in update; need to self-join 
    select id from seats limit 2 
) as t 
where sum.event_id = seats.event_id 
and seats.id = t.id 
and count >= 2 

;

UPDATE 2 
id | event_id | locked 
----+----------+-------- 
    3 |  1 | f 
    4 |  2 | f 
    2 |  1 | t 
    1 |  1 | t 
(4 rows) 

Así que este 'esclusas dos asientos para cada evento que tiene por lo menos dos asientos :)

+0

Gracias, decidieron ir con su solución. Tuve que modificar la consulta un poco, pero funciona como se esperaba –

1

Puede usar un SELECT ... FOR UPDATE que bloqueará esas filas para usted - a continuación, puede averiguar cuántas filas ha seleccionado y si hay suficientes puede actualizarlas con el valor de bloqueo y la marca de tiempo. Si ya no desea estas filas, puede ROLLBACK para liberar las cerraduras. http://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE

Sin embargo, estos sólo funcionan durante la duración de una transacción, si se pierde la transacción, se dará a conocer estos bloqueos, por lo que no puede utilizar el bloqueo SELECT ... FOR UPDATE para sostener la fila abierta, tendrá que marcar ellos como reservado de alguna manera.

En general, bloquear y esperar una respuesta del usuario es una mala técnica. ¿Qué sucede si el usuario se va a tomar una ducha, etc.? Entonces se queda con muchas filas bloqueadas.

Parece que tienes dos opciones:

  1. No bloquear nada, si el usuario intenta seleccionar un asiento y que luego se vende a cabo, simplemente pedir disculpas y presentarlos con algunos otros asientos disponibles. Además, conserve las estadísticas sobre la frecuencia con la que esto sucede y, si encuentra que sucede con demasiada frecuencia, puede considerar un plan de bloqueo.

  2. Haga lo que describa en su pregunta, y establezca una regla de que las reservas de asientos caducan después de 2 minutos, etc. De esta manera no tiene que preocuparse por liberar explícitamente las cerraduras, puede verificar las marcas de tiempo de cuando se establecieron

+0

No creo alemán está hablando de db fila nivel de bloqueo - que está hablando lógicamente "bloqueo" o "reservar" un asiento que se ofrece a un cliente para que no sea reservado por otro cliente antes de que el primer cliente tome una decisión. –

+0

sí, mi respuesta era poco clara, pero quise decir que solo debería usar el 'SELECCIONAR ... PARA ACTUALIZAR' para asegurarse de que obtiene suficientes filas para satisfacer la solicitud del usuario y luego poner las marcas de tiempo de bloqueo con una actualización. no mantener el bloqueo de nivel db durante todo el tiempo que dure la decisión del usuario. –

+0

'seleccionar ... para actualizar' no bloquea las filas durante la duración de una conexión, pero la duración de una transacción –

0

Si he entendido su problema correctamente, creo que una solución podría ser la siguiente:

hacer la siguiente operación (está en pseudo-código, por supuesto)

<lock seats table> 

    result=SELECT count(*) FROM seats 
    WHERE status="unlocked" 
    GROUP BY status 
    HAVING count(*)>=2 

IF result EXISTS 

    UPDATE seats SET status = "locked", lock_time = NOW(), lock_id = "lock1" 
    WHERE status="unlocked"LIMIT 2 


<unlock seats table> 

Usted libera la mesa se traba rápidamente de esta manera.Entonces, si el usuario no quiere su pedido, puede cancelarlo con una simple actualización. Además, el índice de bloqueo no previene otras actualizaciones

Otro enfoque más racional en mi humilde opinión es el siguiente pseudocódigo. Creo que es un buen enfoque, ya que es sin estado y no se mantienen los registros bloqueados (el uso de bloqueos de base de datos) a la espera de que el usuario decida (que debe ser evitado a toda costa)

result=SELECT * FROM seats 
     WHERE status="unlocked" 
<present the result to the user and let the user decide which n seats they want> 
array[] choices:=<get from the user> 

//note that we do not lock the table here and the available seats presented to the  
//user might be taken while he is making his choices. But that's OK since we should 
//not keep the table locked while he is making his choices. 

<lock seats table> 
//now since the user either wants all the seats together or none of them, all the 
//seats rows that they want should be unlocked at first. If any of them 
// is locked when the UPDATE command is updating the row, then we should rollback all 
// the updates. Unfortunately there is no way to determine that by standard update  
// command. Thus here I use the following select query before making the update to 
// make sure every choice is there. 

result= SELECT count(*) 
     FROM seats 
     WHERE status="unlocked" AND seat_id IN (choice[1],choice[2], ...,choice[n]) 

IF result=length(choices) 

    UPDATE seats SET status = "locked", lock_time = NOW(), lock_id = "lock1" 
    WHERE seat_id IN (choice[1],choice[2], ...,choice[n]) 

    <unlock seats table> 

Saludos Amir

0

Simplemente una alternativa: ¿cuál es la implicación si solo "permite" que el próximo cliente también compre los asientos y brinde una ventana emergente de error al primer usuario, en caso de que seleccione esos asientos después de que el primer cliente los haya comprado?

Todo el caso de uso se puede cambiar un poco (estoy pensando en reserva de entradas como es el caso aquí) -

cliente selecciona película - mostrar los tiempos - se muestra una lista de todos asientos vacíos, actualizada en tiempo real - con código de color. Luego, dependiendo de los asientos que el cliente seleccione, se aceptan para el pago.

El mecanismo de bloqueo que está utilizando siempre mostrará más asientos, ya que están agotados de lo que realmente son, lo que puede ocasionar innecesariamente la pérdida de ventas. Por otro lado, si solo tiene un cheque simple cuando el usuario realmente compra los asientos, para verificar si los asientos se han vendido a otra persona durante el tiempo transcurrido entre la recuperación de esos asientos y la reserva, entonces siempre puede mostrar un mensaje de error. . Incluso más arriba, después de que el cliente selecciona los asientos hasta el pago, es necesario bloquearlos; pero entonces no enfrentarás el problema de que el sistema seleccione los asientos, ¡es el cliente que los elige!

+0

Esta sería mi primera opción para hacerlo, pero desafortunadamente el requisito del proyecto es que los asientos se ofrezcan a pedido, no seleccionado –

3

Lo que está comentando es un sistema de reserva. La forma en que he construido tales sistemas es tener una tabla de reservas y una tabla de asientos.

Create Table Reservations 
    (
    EventId ... not null References Events (Id) 
    , SeatNumber varchar(10) not null 
    , Expiration datetime not null 
    , CustomerId ... not null References Customers(Id) 
    , Constraint FK_Reservations_Seats 
     Foreign Key(EventId, SeatNumber) 
     References EventSeats(EventId, SeatNumber) 
    ) 

Create Table EventSeats 
    (
    EventId ... References Events (Id) 
    , SeatNumber varchar(10) not null 
    , CustomerId ... null References Customers(Id) 
    , PurchaseDate datetime not null 
    ) 

Cuando alguien hace una reserva que hagas una inserción en la tabla Reservas con un valor de fecha y hora de un período de tiempo determinado en el futuro. Cuando usted está buscando para los asientos disponibles, la consulta se parece a:

Select S.EventId, S.SeatNumber 
From EventSeats As S 
Where S.EventId = ... 
    And S.CustomerId Is Null 
    And Not Exists (
        Select 1 
        From Reservations As R 
        Where R.EventId = S.EventId 
         And R.SeatNumber = S.SeatNumber 
         And R.Expiration > CURRENT_TIMESTAMP 
        ) 

Esto permite que alguien ponga una retención temporal de un asiento, si quieren. Si desean comprar los asientos, inserte otro registro de reserva durante un período en el futuro. De hecho, el sistema que diseñé insertó una nueva reserva en cada paso del proceso de compra que estaba a 10 minutos en el futuro solo para ayudar al usuario a finalizar el proceso de compra antes de que expirara la reserva. Una vez que completan la compra, actualiza la tabla EventSeats con su información y ahora este asiento se toma de forma permanente.

+0

Su solución se ve muy bien, intentaré aplicarla, solo para ver cómo se compara con la que acepté. –

0

Realiza tus instrucciones de actualización/selección en una transacción. Si solo recuperas una fila, revierte la transacción y los bloqueos se revertirán.

Cuestiones relacionadas