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
);
quelle
chat_user_messages.Id
berechnet? Können Sie DDL für Tabellen posten?id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT
Ich 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 aktualisiertAntworten:
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
Beachten Sie, dass ich den
user_chat_messages_from_user_id_foreign
Fremdschlüssel entfernt habe, da er auf dieusers
Tabelle verweist , die wir in unserem Beispiel nicht haben. Es ist nicht wichtig, das Problem zu reproduzieren.Deadlock reproduzieren
Verbindung 1
Verbindung 2
Verbindung 1
Zu diesem Zeitpunkt wartet Verbindung 1.
Verbindung 2
Hier löst Verbindung 2 einen Deadlock aus
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.Wiedergabe der gleichen Schritte wie zuvor
Verbindung 1
Verbindung 2
Verbindung 1
Zu diesem Zeitpunkt wird Verbindung 1 ausgeführt, anstatt wie zuvor zu warten.
Verbindung 2
Verbindung 2 ist jetzt diejenige, die jetzt wartet, aber sie ist nicht blockiert.
Verbindung 1
Verbindung 2 hört jetzt auf zu warten und führt ihren Befehl aus.
Verbindung 2
Fertig, ohne Deadlock.
Warum?
Schauen wir uns die Ausgabe von an
SHOW ENGINE INNODB STATUS
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 unsererINSERT
Erklärung) und dann eine exklusive Sperre (aus unserer Anweisung) erhalten wirdUPDATE
.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 deruser_chats
Tabelle. Fügen Sie stattdessen einen zusammengesetzten Indexuser_chat_messages
für die Spalten (user_chat_id
,created_at
) hinzu.Anschließend können Sie mit der folgenden Abfrage die letzte aktualisierte Zeit abrufen.
Diese Abfrage wird aufgrund des Index extrem schnell ausgeführt und erfordert nicht, dass Sie auch die späteste
updated_at
Zeit in deruser_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 ist1
, wie in meinem Beispiel.Dies ist im Wesentlichen das, was Rick James vorschlägt.
2. Sperren Sie die Tabellen, um Anforderungen zu serialisieren
Fügen Sie dies
SELECT ... FOR UPDATE
zum Start Ihrer Transaktion hinzu, und Ihre Anforderungen werden serialisiert. Stellen Sie nach wie vor sicher, dassid
der$message->getUserChatId()
Wert dynamisch festgelegt und nicht1
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.
quelle
Sperren Sie als ersten Schritt Ihrer Transaktion $ this-> db-> table ('user_chats') -> where ('id', $ message-> getUserChatId ()). Dadurch wird ein Deadlock vermieden.
quelle
Wenn es nur eine Reihe gibt
user_chats
? Wenn nicht, wie lautet die Semantikid
? 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
UPDATE
und dies stattdessen tun, wenn Sie das späteste Datum wünschen:quelle