Você está fora do gancho por não querer encapsular tudo em uma consulta grande, porque isso também não resolverá nada, apenas tornará menos provável.
O que você precisa são bloqueios nas linhas ou bloqueios no índice onde a nova linha seria inserida.
Então, como podemos obter bloqueios exclusivos?
Duas conexões, mysql1 e mysql2, cada uma delas solicitando um bloqueio exclusivo usando
SELECT ... FOR UPDATE
. A tabela 'history' tem uma coluna 'user_id' que é indexada. (Também é uma chave estrangeira.) Não há linhas encontradas, então ambas parecem prosseguir normalmente como se nada incomum fosse acontecer. O user_id 2808 é válido, mas não tem nada no histórico. 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);
... e não recebo meu prompt de volta ... sem resposta ... porque outra sessão também tem um bloqueio ... mas então:
mysql2> insert into history(user_id) values (2808);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Então mysql1 imediatamente retorna sucesso na inserção.
Query OK, 1 row affected (3.96 sec)
Tudo o que resta é o mysql1 para
COMMIT
e magicamente, impedimos que um usuário com 0 entradas inserisse mais de 1 entrada. O deadlock ocorreu porque ambas as sessões precisavam que coisas incompatíveis acontecessem:mysql1 precisava que mysql2 liberasse seu bloqueio antes de poder confirmar e mysql2 precisava que mysql1 liberasse seu bloqueio antes de poder inserir. Alguém tem que perder essa luta, e geralmente o segmento que deu menos trabalho é o perdedor. Mas e se já existissem 1 ou mais linhas quando eu fiz o
SELECT ... FOR UPDATE
? Nesse caso, o bloqueio estaria nas linhas, então a segunda sessão para tentar SELECT
na verdade bloquearia esperando pelo SELECT
até que a primeira sessão decidiu ou COMMIT
ou ROLLBACK
, momento em que a segunda sessão teria visto uma contagem precisa do número de linhas (incluindo as inseridas ou excluídas pela primeira sessão) e poderia ter decidido com precisão que o usuário já tinha o máximo permitido. Você não pode superar uma condição de corrida, mas pode bloqueá-los.