Vorwort
Unsere Anwendung führt mehrere Threads aus, die DELETEAbfragen parallel ausführen . Die Abfragen wirken sich auf isolierte Daten aus, dh es sollte keine Möglichkeit bestehen, dass DELETEin denselben Zeilen von separaten Threads gleichzeitig etwas auftritt. Gemäß der Dokumentation verwendet MySQL jedoch die sogenannte "Next-Key" -Sperre für DELETEAnweisungen, 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 COMMITTEDIsolationsstufe.
Das Problem
Problem tritt auf, wenn komplexe DELETEAnweisungen mit JOINs 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 JOINed-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 COMMITTEDblockiert 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_trxenthä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 XSperre für eine Zeile mit Primärschlüssel = 53. Allerdings muss keine von beiden Zeilen aus der proc_warningsTabelle löschen . Ich verstehe nur nicht, warum der Index gesperrt ist. Darüber hinaus ist der Index weder gesperrt, wenn die proc_warningsTabelle leer ist, noch day_positionenthält die Tabelle weniger Zeilen (dh einhundert Zeilen).
Weitere Untersuchungen sollten EXPLAINdie ähnliche SELECTAbfrage durchlaufen. Es zeigt, dass das Abfrageoptimierungsprogramm keinen Index zum Abfragen von proc_warningsTabellen 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.
parentTabelle erstellen
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
childTabelle 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_positionTabelle 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?