Warum führt diese Abfrage zu einem Deadlock?

7

Ich gebe unten die rohe MySQL-Abfrage und auch den Code an, in dem ich das programmgesteuert mache. Wenn zwei Anforderungen gleichzeitig ausgeführt werden, führt dies zu folgendem Fehlermuster:

SQLSTATE [40001]: Serialisierungsfehler: 1213 Deadlock beim Versuch, eine Sperre zu erhalten; versuchen einen Neustart Transaktion (SQL: update user_chats set updated_at = 2018-06-29 10:07:13 where id = 1)

Wenn ich dieselbe Abfrage ausführe, jedoch ohne Transaktionsblock, funktioniert sie bei vielen gleichzeitigen Aufrufen fehlerfrei. Warum ? (Die Transaktion wird gesperrt, oder?)

Gibt es eine Möglichkeit, dies zu lösen, ohne die gesamte Tabelle zu sperren? (Möchten Sie versuchen, Sperren auf Tabellenebene zu vermeiden?)

Ich weiß, dass eine Sperre zum Einfügen / Aktualisieren / Löschen von Tabellen in MySql mit InnoDB erworben wurde, verstehe aber immer noch nicht, warum der Deadlock hier auftritt und wie er am effizientesten gelöst werden kann.

    START TRANSACTION;

    insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`)
        values (1, 2, 'dfasfdfk);
    update `user_chats`
        set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

    COMMIT;

Oben ist die Rohabfrage, aber ich mache es in PHP Laravel Query Builder wie folgt:

    /**
     * @param UserChatMessageEntity $message
     * @return int
     * @throws \Exception
     */
    public function insertChatMessage(UserChatMessageEntity $message) : int
    {
        $this->db->beginTransaction();
        try
        {
            $id = $this->db->table('user_chat_messages')->insertGetId([
                    'user_chat_id' => $message->getUserChatId(),
                    'from_user_id' => $message->getFromUserId(),
                    'content' => $message->getContent()
                ]
            );

            //TODO results in lock error if many messages are sent same time
            $this->db->table('user_chats')
                ->where('id', $message->getUserChatId())
                ->update(['updated_at' => date('Y-m-d H:i:s')]);

            $this->db->commit();
            return $id;
        }
        catch (\Exception $e)
        {
            $this->db->rollBack();
            throw  $e;
        }
    }

DDL für die Tabellen:

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),
    CONSTRAINT user_chat_messages_from_user_id_foreign FOREIGN KEY (from_user_id) REFERENCES users (id)
);
CREATE INDEX user_chat_messages_from_user_id_index ON user_chat_messages (from_user_id);
CREATE INDEX user_chat_messages_user_chat_id_index ON user_chat_messages (user_chat_id);


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
);
Kristi Jorgji
quelle
Wie wird chat_user_messages.Idberechnet? Können Sie DDL für Tabellen posten?
Michael Kutz
Es ist ein Auto-Inkrement-Feld. id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENTIch habe einen Test durchgeführt und wenn ich die Transaktion entferne, tritt der Deadlock nicht mehr auf. Aber trotzdem muss es auf transaktionale Weise gemacht werden. Die Nachricht wird nur eingefügt, wenn auch user_chats aktualisiert wird. Ich habe die Frage mit den ddls für beide beteiligten Tabellen aktualisiert
Kristi Jorgji

Antworten:

14

Der AUSLÄNDISCHE SCHLÜSSEL user_chat_messages_user_chat_id_foreignist 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_foreignFremdschlüssel entfernt habe, da er auf die usersTabelle 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_foreignFremdschlü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 INSERTErklä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 UPDATEBefehl 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_atSpalte in der user_chatsTabelle. Fügen Sie stattdessen einen zusammengesetzten Index user_chat_messagesfü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_atZeit in der user_chatsTabelle speichern . Dies hilft, Datenverdopplungen zu vermeiden, weshalb es meine bevorzugte Lösung ist.

Stellen Sie sicher, dass idder $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 UPDATEzum Start Ihrer Transaktion hinzu, und Ihre Anforderungen werden serialisiert. Stellen Sie nach wie vor sicher, dass idder $message->getUserChatId()Wert dynamisch festgelegt und nicht 1wie 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_foreignFremdschlü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.

Willem Renzema
quelle
Danke für die tolle Antwort! (Ich kann leider wegen fehlendem Ruf nicht upvoten). Die ID ist nur hier in der Frage fest codiert, aber in meinem realen Code ist sie natürlich dynamisch. Die Benutzerchat-ID ist im Voraus bekannt und auf das UserChatMessage-Objekt festgelegt. Ich werde auch selbst alle von Ihnen bereitgestellten Schritte ausprobieren, damit ich einen solchen Fall in Zukunft vollständig verstehe und vermeide. Vielen Dank ! Ich hatte dieses Problem vor 2 Tagen gelöst, wie Gerard H. Pille vorgeschlagen hatte, und es funktioniert hervorragend. Ich werde auch alle Ihre Lösungen für Lernzwecke ausprobieren
Kristi Jorgji
Ja, gute Antwort. (Ich habe es positiv bewertet.) Ein weiterer Grund, warum FKs mehr stören als wert sein können.
Rick James
@ KristiJorgji Hört sich gut an. Ich habe Ihrer Frage eine positive Bewertung gegeben (sie hat es trotzdem verdient, mit all den guten Informationen, um das Problem zu reproduzieren), damit Sie möglicherweise genügend Repräsentanten haben, um eine positive Bewertung abzugeben und eine Antwort zu wählen. Da Gerards Lösung Ihr Problem behoben hat, würde ich vorschlagen, seine Antwort zu akzeptieren, aber Sie können auch die Antwort aller positiv bewerten, die nützlich war.
Willem Renzema
Hervorragende Antwort. Ich hatte ein ähnliches Problem. Ihre Antwort hat mir geholfen.
Chatsap
0

Sperren Sie als ersten Schritt Ihrer Transaktion $ this-> db-> table ('user_chats') -> where ('id', $ message-> getUserChatId ()). Dadurch wird ein Deadlock vermieden.

Gerard H. Pille
quelle
Sie schlagen also vor, zuerst eine select-Anweisung auszuführen, obwohl ich das Ergebnis nicht benötige, und dann den Rest so fortzusetzen, wie er tatsächlich ist? Bitte geben Sie mir die Details dieser Lösung einige Erklärungen, damit ich verstehe, wie dies das Problem löst und ich lerne daraus
Kristi Jorgji
Verstehst du einen Deadlock? Ich schlug vor, eine Sperre vorzunehmen (bei Oracle wäre dies eine "Auswahl ... für Aktualisierung"), um sicherzugehen, dass Sie eine Aktualisierung durchführen können, wird niemand dazwischen kommen (Ihre angeblich sehr kurze Transaktion).
Gerard H. Pille
Das ist die gleiche Syntax auch in MySql. Großer Dank wird das tun, dann nehmen Sie eine Sperre für diese Reihe
Kristi Jorgji
0

Wenn es nur eine Reihe gibt user_chats? Wenn nicht, wie lautet die Semantik id? Ist es ein "Benutzer"? Oder eine "Chatnummer"? Oder etwas anderes?

Es hört sich so an, als würden alle Verbindungen versuchen, die ID des letzten Chats zu erhöhen (ID = 1). Wenn Sie das brauchen, sollten Sie das werfen UPDATEund dies stattdessen tun, wenn Sie das späteste Datum wünschen:

SELECT MAX(created_at) FROM user_chat_messages.
Rick James
quelle
Die ID 1 ist nur für das Beispiel, das ich hier angegeben habe. In echtem Code können je nach Chat unterschiedliche IDs vorhanden sein. Dieser Deadlock tritt auf, wenn ein Benutzer mehrere Nachrichten an denselben Chat sendet (z. B. Chat mit ID 1). Natürlich kann ich js Frontend-Taste blockieren, bis ein Anruf beendet ist usw. Aber um zu lernen, etwas Robustes aufzubauen, wollte ich den Deadlock vermeiden können, selbst wenn viele Anfragen gestellt wurden, eine Nachricht in denselben Chat (bestimmte Chat-ID) in den Chat einzufügen gleiche Zeit. Ich habe das Problem behoben, indem ich zuerst den user_chat als erstes in der Transaktion zum Aktualisieren ausgewählt habe
Kristi Jorgji