Bloqueo en Postgres para la combinación ACTUALIZAR / INSERTAR


11

Tengo dos mesas. Uno es una tabla de registro; otro contiene, esencialmente, códigos de cupón que solo pueden usarse una vez.

El usuario debe poder canjear un cupón, que insertará una fila en la tabla de registro y marcará el cupón como se usa (actualizando la usedcolumna a true).

Naturalmente, hay un problema obvio de condición / seguridad de carrera aquí.

He hecho cosas similares en el pasado en el mundo de mySQL. En ese mundo, bloquearía ambas tablas globalmente, haría la lógica de forma segura sabiendo que esto solo podría suceder una vez a la vez, y luego desbloquearía las tablas una vez que hubiera terminado.

¿Hay una mejor manera en Postgres para hacer esto? En particular, me preocupa que el bloqueo sea global, pero no tiene que serlo. Realmente solo necesito asegurarme de que nadie más esté tratando de ingresar ese código en particular, ¿entonces tal vez funcionaría un bloqueo a nivel de fila?

Respuestas:


15

He oído hablar de problemas de concurrencia como ese en MySQL antes. No es así en Postgres.

Los bloqueos de nivel de fila incorporados en el READ COMMITTEDnivel de aislamiento de transacción predeterminado son suficientes.

Sugiero una sola declaración con un CTE modificador de datos (algo que MySQL tampoco tiene) porque es conveniente pasar valores de una tabla a otra directamente (si es necesario). Si no necesita nada de la coupontabla, puede usar una transacción con declaraciones separadas UPDATEy INSERTtambién.

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

Debería ser raro que más de una transacción intente canjear el mismo cupón. Tienen un número único, ¿no? Sin embargo, más de una transacción que intente en el mismo momento en el tiempo debería ser mucho más rara. (¿Tal vez un error de aplicación o alguien tratando de jugar el sistema?)

Sea como fuere, el UPDATEúnico tiene éxito exactamente para una transacción, pase lo que pase. Un UPDATEadquiere un bloqueo de nivel de fila en cada fila de destino antes de actualizar. Si una transacción concurrente intenta UPDATEla misma fila, verá el bloqueo en la fila y esperará hasta que finalice la transacción de bloqueo ( ROLLBACKo COMMIT), luego será el primero en la cola de bloqueo:

  • Si se confirma, vuelva a verificar la condición. Si todavía está NOT used, bloquee la fila y continúe. De lo contrario, UPDATEahora no encuentra ninguna fila de calificación y no hace nada , no devuelve ninguna fila, por lo que INSERTtampoco hace nada.

  • Si retrocede, bloquee la fila y continúe.

No hay potencial para una condición de carrera .

No hay potencial para un punto muerto a menos que coloque más escrituras en la misma transacción o bloquee más filas que solo la única.

El INSERTes sin preocupaciones. Si, por algún error, el coupon_idya está en la logtabla (y tiene una restricción UNIQUE o PK activada log.coupon_id), la transacción completa se revertirá después de una violación única. Indicaría un estado ilegal en su base de datos. Si la declaración anterior es la única forma de escribir en la logtabla, eso nunca debería ocurrir.


De hecho, debería ser algo raro que más de una transacción intente canjear el mismo código, pero sus sospechas son correctas ya que esto será exclusivamente cuando alguien esté tratando de jugar el sistema. Muchas gracias por esto: los CTE fueron una gran atracción para mí al mudarme a Postgres, pero no me di cuenta de que el bloqueo implícito sería lo suficientemente bueno para esto.
Rob Miller
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.