Está fuera de peligro por no querer encapsular todo en una consulta grande, porque eso tampoco resolverá nada, solo lo hace menos probable.
Lo que necesita son bloqueos en las filas o bloqueos en el índice donde se insertaría la nueva fila.
Entonces, ¿cómo obtenemos bloqueos exclusivos?
Dos conexiones, mysql1 y mysql2, cada una de ellas solicitando un bloqueo exclusivo usando SELECT ... FOR UPDATE
. La tabla 'historial' tiene una columna 'user_id' que está indexada. (También es una clave externa). No se encontraron filas, por lo que ambas parecen proceder normalmente, como si nada inusual fuera a suceder. El user_id 2808 es válido pero no tiene nada en el historial.
mysql1> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql2> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql1> select * from history where user_id = 2808 for update;
Empty set (0.00 sec)
mysql2> select * from history where user_id = 2808 for update;
Empty set (0.00 sec)
mysql1> insert into history(user_id) values (2808);
... y no me devuelven el aviso... no hay respuesta... porque otra sesión también tiene un bloqueo... pero entonces:
mysql2> insert into history(user_id) values (2808);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Luego, mysql1 devuelve inmediatamente el éxito en la inserción.
Query OK, 1 row affected (3.96 sec)
Todo lo que queda es que mysql1 COMMIT
y mágicamente, evitamos que un usuario con 0 entradas inserte más de 1 entrada. El interbloqueo ocurrió porque ambas sesiones necesitaban que sucedieran cosas incompatibles:mysql1 necesitaba que mysql2 liberara su bloqueo antes de poder confirmar y mysql2 necesitaba que mysql1 liberara su bloqueo antes de poder insertar. Alguien tiene que perder esa pelea, y generalmente el hilo que ha hecho menos trabajo es el perdedor.
Pero, ¿qué sucede si ya existían 1 o más filas cuando hice SELECT ... FOR UPDATE
? ? En ese caso, el bloqueo habría estado en las filas, por lo que la segunda sesión para intentar SELECT
en realidad bloquearía la espera de SELECT
hasta que la primera sesión decidió COMMIT
o ROLLBACK
, momento en el que la segunda sesión habría visto un recuento exacto de la cantidad de filas (incluidas las insertadas o eliminadas por la primera sesión) y podría haber decidido con precisión que el usuario ya tenía el máximo permitido.
No puedes superar una condición de carrera, pero puedes bloquearlos.