Der AUSLÄNDISCHE SCHLÜSSEL user_chat_messages_user_chat_id_foreign
ist in dieser Situation die Ursache für Ihren Deadlock.
Glücklicherweise ist dies angesichts der von Ihnen angegebenen Informationen leicht zu reproduzieren.
Installieren
CREATE DATABASE dba210949;
USE dba210949;
CREATE TABLE user_chats
(
id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE user_chat_messages
(
id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
user_chat_id INT(10) unsigned NOT NULL,
from_user_id INT(10) unsigned NOT NULL,
content VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT user_chat_messages_user_chat_id_foreign FOREIGN KEY (user_chat_id) REFERENCES user_chats (id)
);
insert into user_chats (id,updated_at) values (1,NOW());
Beachten Sie, dass ich den user_chat_messages_from_user_id_foreign
Fremdschlüssel entfernt habe, da er auf die users
Tabelle verweist , die wir in unserem Beispiel nicht haben. Es ist nicht wichtig, das Problem zu reproduzieren.
Deadlock reproduzieren
Verbindung 1
USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
Verbindung 2
USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
Verbindung 1
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
Zu diesem Zeitpunkt wartet Verbindung 1.
Verbindung 2
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
Hier löst Verbindung 2 einen Deadlock aus
FEHLER 1213 (40001): Deadlock beim Versuch, eine Sperre zu erhalten; Versuchen Sie, die Transaktion neu zu starten
Wiederholen ohne den Fremdschlüssel
Wiederholen wir die gleichen Schritte, jedoch mit den folgenden Tabellenstrukturen. Der einzige Unterschied ist diesmal das Entfernen des user_chat_messages_user_chat_id_foreign
Fremdschlüssels.
CREATE DATABASE dba210949;
USE dba210949;
CREATE TABLE user_chats
(
id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE user_chat_messages
(
id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
user_chat_id INT(10) unsigned NOT NULL,
from_user_id INT(10) unsigned NOT NULL,
content VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
insert into user_chats (id,updated_at) values (1,NOW());
Wiedergabe der gleichen Schritte wie zuvor
Verbindung 1
USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
Verbindung 2
USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');
Verbindung 1
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
Zu diesem Zeitpunkt wird Verbindung 1 ausgeführt, anstatt wie zuvor zu warten.
Verbindung 2
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;
Verbindung 2 ist jetzt diejenige, die jetzt wartet, aber sie ist nicht blockiert.
Verbindung 1
commit;
Verbindung 2 hört jetzt auf zu warten und führt ihren Befehl aus.
Verbindung 2
commit;
Fertig, ohne Deadlock.
Warum?
Schauen wir uns die Ausgabe von an SHOW ENGINE INNODB STATUS
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-07-04 10:38:31 0x7fad84161700
*** (1) TRANSACTION:
TRANSACTION 42061, ACTIVE 55 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 2, OS thread handle 140383222380288, query id 81 localhost root updating
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42061 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 00000001; asc ;;
1: len 6; hex 00000000a44b; asc K;;
2: len 7; hex b90000012d0110; asc - ;;
3: len 4; hex 5b3ca335; asc [< 5;;
4: len 4; hex 5b3ca335; asc [< 5;;
*** (2) TRANSACTION:
TRANSACTION 42062, ACTIVE 46 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 140383222109952, query id 82 localhost root updating
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42062 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 00000001; asc ;;
1: len 6; hex 00000000a44b; asc K;;
2: len 7; hex b90000012d0110; asc - ;;
3: len 4; hex 5b3ca335; asc [< 5;;
4: len 4; hex 5b3ca335; asc [< 5;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42062 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 00000001; asc ;;
1: len 6; hex 00000000a44b; asc K;;
2: len 7; hex b90000012d0110; asc - ;;
3: len 4; hex 5b3ca335; asc [< 5;;
4: len 4; hex 5b3ca335; asc [< 5;;
*** WE ROLL BACK TRANSACTION (2)
Sie können sehen, dass Transaktion 1 einen lock_mode X auf dem PRIMARY-Schlüssel von hat user_chats
, während Transaktion 2 lock_mode S hat und auf lock_mode X wartet . Dies ist darauf zurückzuführen, dass zuerst eine gemeinsame Sperre (aus unserer INSERT
Erklärung) und dann eine exklusive Sperre (aus unserer Anweisung) erhalten wird UPDATE
.
Was also passiert, ist, dass Verbindung 1 zuerst die gemeinsam genutzte Sperre und dann Verbindung 2 eine gemeinsam genutzte Sperre für denselben Datensatz abruft. Das ist vorerst in Ordnung, da beide gemeinsame Sperren sind.
Verbindung 1 versucht dann, ein Upgrade auf eine exklusive Sperre durchzuführen, um das UPDATE durchzuführen, nur um festzustellen, dass Verbindung 2 bereits eine Sperre hat. Geteilte und exklusive Sperren passen nicht gut zusammen, wie Sie wahrscheinlich anhand ihres Namens ableiten können. Deshalb wartet es nach dem UPDATE
Befehl auf Verbindung 1.
Dann versucht Connection 2 UPDATE
, was eine exklusive Sperre erfordert, und InnoDB sagt "Welpe, ich werde diese Situation niemals alleine beheben können" und erklärt einen Deadlock. Dadurch wird Verbindung 2 beendet, die gemeinsam genutzte Sperre, die Verbindung 2 hielt, wird freigegeben, und Verbindung 1 kann normal abgeschlossen werden.
Lösung (en)
An diesem Punkt sind Sie wahrscheinlich bereit, mit dem Yap Yap Yap aufzuhören und eine Lösung zu suchen. Hier sind meine Vorschläge in der Reihenfolge meiner persönlichen Präferenz.
1. Vermeiden Sie das Update insgesamt
Kümmere dich überhaupt nicht um die updated_at
Spalte in der user_chats
Tabelle. Fügen Sie stattdessen einen zusammengesetzten Index user_chat_messages
für die Spalten ( user_chat_id
, created_at
) hinzu.
ALTER TABLE user_chat_messages
ADD INDEX `latest_message_for_user_chat` (`user_chat_id`,`created_at`)
Anschließend können Sie mit der folgenden Abfrage die letzte aktualisierte Zeit abrufen.
SELECT MAX(created_at) AS created_at FROM user_chat_messages WHERE user_chat_id = 1
Diese Abfrage wird aufgrund des Index extrem schnell ausgeführt und erfordert nicht, dass Sie auch die späteste updated_at
Zeit in der user_chats
Tabelle speichern . Dies hilft, Datenverdopplungen zu vermeiden, weshalb es meine bevorzugte Lösung ist.
Stellen Sie sicher, dass id
der $message->getUserChatId()
Wert dynamisch festgelegt und nicht fest codiert ist 1
, wie in meinem Beispiel.
Dies ist im Wesentlichen das, was Rick James vorschlägt.
2. Sperren Sie die Tabellen, um Anforderungen zu serialisieren
SELECT id FROM user_chats WHERE id=1 FOR UPDATE
Fügen Sie dies SELECT ... FOR UPDATE
zum Start Ihrer Transaktion hinzu, und Ihre Anforderungen werden serialisiert. Stellen Sie nach wie vor sicher, dass id
der $message->getUserChatId()
Wert dynamisch festgelegt und nicht 1
wie in meinem Beispiel fest codiert wird .
Dies schlägt Gerard H. Pille vor.
3. Lassen Sie den Fremdschlüssel fallen
Manchmal ist es einfach einfacher, die Quelle des Deadlocks zu entfernen. Lassen Sie einfach den user_chat_messages_user_chat_id_foreign
Fremdschlüssel fallen und das Problem ist gelöst.
Ich mag diese Lösung im Allgemeinen nicht besonders, da ich die Datenintegrität (die der Fremdschlüssel bietet) liebe, aber manchmal müssen Sie Kompromisse eingehen.
4. Wiederholen Sie den Befehl nach dem Deadlock
Dies ist die empfohlene Lösung für Deadlocks im Allgemeinen. Fangen Sie einfach den Fehler ab und wiederholen Sie die gesamte Anforderung. Es ist jedoch am einfachsten zu implementieren, wenn Sie sich von Anfang an darauf vorbereitet haben, und das Aktualisieren von Legacy-Code kann schwierig sein. Angesichts der Tatsache, dass es einfachere Lösungen gibt (wie 1 und 2 oben), ist dies meine am wenigsten empfohlene Lösung für Ihre Situation.
chat_user_messages.Id
berechnet? Können Sie DDL für Tabellen posten?