Vorwort
Unsere Anwendung führt mehrere Threads aus, die DELETE
Abfragen parallel ausführen . Die Abfragen wirken sich auf isolierte Daten aus, dh es sollte keine Möglichkeit bestehen, dass DELETE
in denselben Zeilen von separaten Threads gleichzeitig etwas auftritt. Gemäß der Dokumentation verwendet MySQL jedoch die sogenannte "Next-Key" -Sperre für DELETE
Anweisungen, die sowohl den passenden Schlüssel als auch eine gewisse Lücke sperrt. Diese Sache führt zu Deadlocks und die einzige Lösung, die wir gefunden haben, ist die Verwendung der READ COMMITTED
Isolationsstufe.
Das Problem
Problem tritt auf, wenn komplexe DELETE
Anweisungen mit JOIN
s großer Tabellen ausgeführt werden. In einem bestimmten Fall haben wir eine Tabelle mit Warnungen, die nur zwei Zeilen enthält, aber die Abfrage muss alle Warnungen, die zu bestimmten Entitäten gehören, aus zwei separaten INNER JOIN
ed-Tabellen löschen. Die Abfrage lautet wie folgt:
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=? AND dp.dirty_data=1
Wenn die Tabelle day_position groß genug ist (in meinem Testfall gibt es 1448 Zeilen), READ COMMITTED
blockiert jede Transaktion auch im Isolationsmodus die gesamte proc_warnings
Tabelle.
Das Problem wird immer in diesen Beispieldaten reproduziert - http://yadi.sk/d/QDuwBtpW1BxB9 sowohl in MySQL 5.1 (geprüft in 5.1.59) als auch in MySQL 5.5 (geprüft in MySQL 5.5.24).
BEARBEITEN: Die verknüpften Beispieldaten enthalten auch Schema und Indizes für die Abfragetabellen, die hier zur Vereinfachung wiedergegeben werden:
CREATE TABLE `proc_warnings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`warning` varchar(2048) NOT NULL,
PRIMARY KEY (`id`),
KEY `proc_warnings__transaction` (`transaction_id`)
);
CREATE TABLE `day_position` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_day_id` int(10) unsigned DEFAULT NULL,
`dirty_data` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `day_position__trans` (`transaction_id`),
KEY `day_position__is` (`ivehicle_day_id`,`sort_index`),
KEY `day_position__id` (`ivehicle_day_id`,`dirty_data`)
) ;
CREATE TABLE `ivehicle_days` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`d` date DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ivehicle_days__is` (`ivehicle_id`,`sort_index`),
KEY `ivehicle_days__d` (`d`)
);
Abfragen pro Transaktion lauten wie folgt:
Transaktion 1
set transaction isolation level read committed; set autocommit=0; begin; DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
Transaktion 2
set transaction isolation level read committed; set autocommit=0; begin; DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1;
Einer von ihnen schlägt immer mit dem Fehler "Wartezeit für Sperren überschritten ..." fehl. Das information_schema.innodb_trx
enthält folgende Zeilen:
| trx_id | trx_state | trx_started | trx_requested_lock_id | trx_wait_started | trx_wait | trx_mysql_thread_id | trx_query |
| '1A2973A4' | 'LOCK WAIT' | '2012-12-12 20:03:25' | '1A2973A4:0:3172298:2' | '2012-12-12 20:03:25' | '2' | '3089' | 'DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1' |
| '1A296F67' | 'RUNNING' | '2012-12-12 19:58:02' | NULL | NULL | '7' | '3087' | NULL |
information_schema.innodb_locks
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
Wie ich sehen kann, möchten beide Abfragen eine exklusive X
Sperre für eine Zeile mit Primärschlüssel = 53. Allerdings muss keine von beiden Zeilen aus der proc_warnings
Tabelle löschen . Ich verstehe nur nicht, warum der Index gesperrt ist. Darüber hinaus ist der Index weder gesperrt, wenn die proc_warnings
Tabelle leer ist, noch day_position
enthält die Tabelle weniger Zeilen (dh einhundert Zeilen).
Weitere Untersuchungen sollten EXPLAIN
die ähnliche SELECT
Abfrage durchlaufen. Es zeigt, dass das Abfrageoptimierungsprogramm keinen Index zum Abfragen von proc_warnings
Tabellen verwendet. Dies ist der einzige Grund, warum ich mir vorstellen kann, warum es den gesamten Primärschlüsselindex blockiert.
Vereinfachter Fall
Das Problem kann auch in einem einfacheren Fall reproduziert werden, wenn nur zwei Tabellen mit einigen Datensätzen vorhanden sind, die untergeordnete Tabelle jedoch keinen Index für die übergeordnete Tabellenreferenzspalte enthält.
parent
Tabelle erstellen
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
child
Tabelle erstellen
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Tabellen füllen
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Test in zwei parallelen Transaktionen:
Transaktion 1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET AUTOCOMMIT=0; BEGIN; DELETE c FROM child c INNER JOIN parent p ON p.id = c.parent_id WHERE p.id = 1;
Transaktion 2
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET AUTOCOMMIT=0; BEGIN; DELETE c FROM child c INNER JOIN parent p ON p.id = c.parent_id WHERE p.id = 2;
In beiden Fällen ist gemeinsam, dass MySQL keine Indizes verwendet. Ich glaube, das ist der Grund für die Sperre des gesamten Tisches.
Unsere Lösung
Die einzige Lösung, die wir derzeit sehen können, besteht darin, das Standard-Wartezeitlimit für Sperren von 50 Sekunden auf 500 Sekunden zu erhöhen, damit der Thread die Bereinigung beendet. Dann drücken Sie die Daumen.
Jede Hilfe geschätzt.
day_position
Tabelle normalerweise, wenn sie so langsam ausgeführt wird, dass Sie das Zeitlimit auf 500 Sekunden erhöhen müssen? 2) Wie lange dauert die Ausführung, wenn Sie nur die Beispieldaten haben?Antworten:
NEUE ANTWORT (dynamisches SQL im MySQL-Stil): Ok, dieses Problem wird auf die Art und Weise gelöst, wie eines der anderen Poster beschrieben hat. Dabei wird die Reihenfolge umgekehrt, in der miteinander inkompatible exklusive Sperren erfasst werden, sodass sie unabhängig davon, wie viele auftreten, nur für auftreten die geringste Zeit am Ende der Transaktionsausführung.
Dies wird erreicht, indem der gelesene Teil der Anweisung in eine eigene select-Anweisung aufgeteilt wird und dynamisch eine delete-Anweisung generiert wird, die aufgrund der Reihenfolge des Auftretens der Anweisung zuletzt ausgeführt werden muss und nur die Tabelle proc_warnings betrifft.
Eine Demo ist bei sql fiddle erhältlich:
Dieser Link zeigt das Schema mit Beispieldaten und eine einfache Abfrage nach Zeilen, die übereinstimmen
ivehicle_id=2
. Es ergeben sich 2 Zeilen, da keine von ihnen gelöscht wurde.Dieser Link zeigt dasselbe Schema und dieselben Beispieldaten, übergibt jedoch einen Wert 2 an das gespeicherte Programm DeleteEntries und weist den SP an,
proc_warnings
Einträge für zu löschenivehicle_id=2
. Die einfache Abfrage für Zeilen gibt keine Ergebnisse zurück, da alle erfolgreich gelöscht wurden. Die Demo-Links zeigen nur, dass der Code wie vorgesehen zum Löschen funktioniert. Der Benutzer mit der richtigen Testumgebung kann kommentieren, ob dies das Problem des blockierten Threads löst.Hier ist auch der Code für die Bequemlichkeit:
Dies ist die Syntax zum Aufrufen des Programms aus einer Transaktion heraus:
ORIGINAL ANTWORT (denke immer noch, dass es nicht zu schäbig ist) Sieht aus wie 2 Probleme: 1) langsame Abfrage 2) unerwartetes Sperrverhalten
In Bezug auf Problem Nr. 1 werden langsame Abfragen häufig durch dieselben zwei Techniken bei der Vereinfachung von Tandem-Abfrageanweisungen und nützlichen Ergänzungen oder Änderungen an Indizes gelöst. Sie selbst haben bereits die Verbindung zu Indizes hergestellt. Ohne diese Indizes kann der Optimierer nicht nach einer begrenzten Anzahl von zu verarbeitenden Zeilen suchen, und jede Zeile aus jeder Tabelle, die pro zusätzliche Zeile multipliziert wird, scannt den zusätzlichen Arbeitsaufwand.
ÜBERARBEITET NACH DEM ANZEIGEN VON SCHEMA UND INDEXEN: Aber ich kann mir vorstellen, dass Sie den größten Leistungsvorteil für Ihre Abfrage erzielen, wenn Sie sicherstellen, dass Sie über eine gute Indexkonfiguration verfügen. Zu diesem Zweck können Sie eine bessere Löschleistung und möglicherweise sogar eine bessere Löschleistung erzielen, indem Sie größere Indizes abwägen und die Einfügeleistung für dieselben Tabellen, zu denen eine zusätzliche Indexstruktur hinzugefügt wird, möglicherweise merklich verlangsamen.
ETWAS BESSER:
AUCH HIER ÜBERARBEITET: Da es so lange dauert, bis es ausgeführt wird, würde ich die schmutzigen_Daten im Index belassen, und ich habe es auch mit Sicherheit falsch verstanden, als ich sie nach der ivehicle_day_id in Indexreihenfolge platziert habe - es sollte zuerst sein.
Aber wenn ich es zu diesem Zeitpunkt in den Händen hätte, da es eine gute Datenmenge geben muss, damit es so lange dauert, würde ich einfach alle abdeckenden Indizes verwenden, um sicherzugehen, dass ich die beste Indizierung dafür bekomme Meine Zeit zur Fehlerbehebung könnte sich kaufen, wenn nichts anderes diesen Teil des Problems ausschließt.
BESTE / ABDECKENDE INDEXE:
In den letzten beiden Änderungsvorschlägen werden zwei Ziele für die Leistungsoptimierung angestrebt:
1) Wenn die Suchschlüssel für Tabellen, auf die nacheinander zugegriffen wird, nicht mit den Ergebnissen der Clusterschlüssel übereinstimmen, die für die Tabelle zurückgegeben werden, auf die aktuell zugegriffen wird, wird die Notwendigkeit beseitigt ein zweiter Satz von Index-Such-mit-Scan-Operationen für den Clustered-Index
2) Wenn letzteres nicht der Fall ist, besteht immer noch zumindest die Möglichkeit, dass der Optimierer einen effizienteren Verknüpfungsalgorithmus auswählen kann, da die Indizes den beibehalten erforderliche Join-Schlüssel in sortierter Reihenfolge.
Ihre Anfrage scheint so einfach wie möglich zu sein (hier kopiert, falls sie später bearbeitet wird):
Es sei denn natürlich, es gibt etwas an der schriftlichen Verknüpfungsreihenfolge, das sich auf die Vorgehensweise des Abfrageoptimierers auswirkt. In diesem Fall können Sie einige der von anderen bereitgestellten Vorschläge zum Umschreiben ausprobieren, einschließlich möglicherweise dieses mit Indexhinweisen (optional):
In Bezug auf # 2 unerwartetes Sperrverhalten.
Ich denke, es wäre der Index, der gesperrt ist, da sich die zu sperrende Datenzeile in einem Clustered-Index befindet, dh die einzelne Datenzeile selbst befindet sich im Index.
Es wäre gesperrt, weil:
1) gemäß http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html
Sie haben auch oben erwähnt:
und lieferte die folgende Referenz dafür:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed
Was dasselbe sagt wie Sie, außer dass es gemäß derselben Referenz eine Bedingung gibt, unter der ein Schloss freigegeben werden soll:
Dies wird auch auf dieser Handbuchseite http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html wiederholt
Daher wird uns mitgeteilt, dass die WHERE-Bedingung ausgewertet werden muss, bevor die Sperre aufgehoben werden kann. Leider wird uns nicht mitgeteilt, wann die WHERE-Bedingung ausgewertet wird, und es kann sich wahrscheinlich etwas von einem Plan zum anderen ändern, der vom Optimierer erstellt wurde. Es zeigt uns jedoch, dass die Freigabe von Sperren in irgendeiner Weise von der Leistung der Abfrageausführung abhängt, deren Optimierung, wie oben erläutert, vom sorgfältigen Schreiben der Anweisung und der umsichtigen Verwendung von Indizes abhängt. Es kann auch durch ein besseres Tischdesign verbessert werden, aber das wäre wahrscheinlich am besten einer separaten Frage überlassen.
Die Datenbank kann keine Datensätze im Index sperren, wenn keine vorhanden sind.
Dies könnte zahlreiche Dinge bedeuten, wie zum Beispiel: einen anderen Ausführungsplan aufgrund einer Änderung der Statistik, eine zu kurze Sperre, um beobachtet zu werden, aufgrund einer viel schnelleren Ausführung aufgrund eines viel kleineren Datensatzes / Join-Operation.
quelle
WHERE
Bedingung wird ausgewertet, wenn die Abfrage abgeschlossen ist. Ist es nicht? Ich dachte, dass die Sperre sofort nach der Ausführung einiger gleichzeitiger Abfragen aufgehoben wird. Das ist das natürliche Verhalten. Dies ist jedoch nicht der Fall. Keine der in diesem Thread vorgeschlagenen Abfragen hilft dabei, das Sperren des Clustered-Index in derproc_warnings
Tabelle zu vermeiden . Ich denke, ich werde einen Fehler bei MySQL melden. Danke für Ihre Hilfe.Ich kann sehen, wie READ_COMMITTED diese Situation verursachen kann.
READ_COMMITTED ermöglicht drei Dinge:
Dies schafft ein internes Paradigma für die Transaktion selbst, da die Transaktion Kontakt halten muss mit:
Wenn zwei unterschiedliche READ_COMMITTED- Transaktionen auf dieselben Tabellen / Zeilen zugreifen, die auf dieselbe Weise aktualisiert werden, müssen Sie nicht mit einer Tabellensperre, sondern mit einer exklusiven Sperre innerhalb des gen_clust_index (auch bekannt als Clustered Index) rechnen . Angesichts der Fragen aus Ihrem vereinfachten Fall:
Transaktion 1
Transaktion 2
Sie sperren denselben Speicherort im gen_clust_index. Man kann sagen, "aber jede Transaktion hat einen anderen Primärschlüssel." Leider ist dies in den Augen von InnoDB nicht der Fall. Es kommt einfach so vor, dass sich ID 1 und ID 2 auf derselben Seite befinden.
Schauen Sie zurück auf
information_schema.innodb_locks
Sie in der Frage angegebenMit Ausnahme von
lock_id
istlock_trx_id
der Rest der Schlossbeschreibung identisch. Da die Transaktionen gleiche Wettbewerbsbedingungen haben (gleiche Transaktionsisolation), sollte dies tatsächlich passieren .Glauben Sie mir, ich habe diese Art von Situation schon einmal angesprochen. Hier sind meine letzten Beiträge dazu:
Nov 05, 2012
: Wie analysiere ich den Innodb-Status bei Deadlock in Insert Query?Aug 08, 2011
: Sind InnoDB Deadlocks exklusiv für INSERT / UPDATE / DELETE?Jun 14, 2011
: Gründe für gelegentlich langsame Abfragen?Jun 08, 2011
: Werden diese beiden Abfragen zu einem Deadlock führen, wenn sie nacheinander ausgeführt werden?Jun 06, 2011
: Fehler beim Entschlüsseln eines Deadlocks in einem Innodb-Statusprotokollquelle
Look back at information_schema.innodb_locks you supplied in the Question
)DELETE
Anweisung keine nicht übereinstimmenden Zeilensperren freigegeben werden .Ich schaute auf die Abfrage und die Erklärung. Ich bin mir nicht sicher, habe aber das Gefühl, dass das Problem das folgende ist. Schauen wir uns die Abfrage an:
Das äquivalente SELECT lautet:
Wenn Sie sich die Erklärung ansehen, werden Sie sehen, dass der Ausführungsplan mit der
proc_warnings
Tabelle beginnt . Das bedeutet, dass MySQL den Primärschlüssel in der Tabelle durchsucht und für jede Zeile prüft, ob die Bedingung erfüllt ist und ob die Zeile gelöscht wird. Das heißt, MySQL muss den gesamten Primärschlüssel sperren.Sie müssen die JOIN-Reihenfolge invertieren, dh alle Transaktions-IDs mit finden
vd.ivehicle_id=16 AND dp.dirty_data=1
und in derproc_warnings
Tabelle verknüpfen .Das heißt, Sie müssen einen der Indizes patchen:
und schreiben Sie die Löschabfrage neu:
quelle
proc_warnings
immer noch gesperrt. Danke trotzdem.Wenn Sie die Transaktionsebene so festlegen, wie Sie es tun, wird das Read Committed nur auf die nächste Transaktion angewendet (also Auto Commit festlegen). Dies bedeutet, dass Sie nach Autocommit = 0 nicht mehr in Read Committed sind. Ich würde es so schreiben:
Sie können überprüfen, in welcher Isolationsstufe Sie sich befinden, indem Sie abfragen
quelle
SET AUTOCOMMIT=0
sollte die Isolationsstufe für die nächste Transaktion zurückgesetzt werden? Ich glaube, es startet eine neue Transaktion, wenn noch keine gestartet wurde (was mein Fall ist). Genauer gesagt ist die nächsteSTART TRANSACTION
oderBEGIN
Aussage nicht notwendig. Mein Zweck beim Deaktivieren der automatischen Festschreibung besteht darin, die Transaktion nach derDELETE
Ausführung der Anweisung geöffnet zu lassen .