MySQL InnoDB sperrt den Primärschlüssel beim Löschen auch in READ COMMITTED


11

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.


Ich habe eine Frage: Haben Sie COMMIT in einer der Transaktionen ausgeführt?
RolandoMySQLDBA

Natürlich. Das Problem ist, dass alle anderen Transaktionen warten müssen, bis eine von ihnen Änderungen festschreibt. Der einfache Testfall enthält keine Commit-Anweisung, die zeigt, wie das Problem reproduziert werden kann. Wenn Sie Commit oder Rollback in einer nicht wartenden Transaktion ausführen, wird die Sperre gleichzeitig aufgehoben und die wartende Transaktion schließt ihre Arbeit ab.
Vitalidze

Wenn Sie sagen, dass MySQL in beiden Fällen keine Indizes verwendet, liegt das daran, dass es im realen Szenario keine gibt? Wenn es Indizes gibt, können Sie den Code dafür bereitstellen? Ist es möglich, einen der unten aufgeführten Indexvorschläge auszuprobieren? Wenn es keine Indizes gibt und es nicht möglich ist, irgendwelche hinzuzufügen, kann MySQL den von jedem Thread verarbeiteten Datensatz nicht einschränken. Wenn dies der Fall ist, würden N Threads die Server-Workload einfach mit N multiplizieren, und es wäre effizienter, nur einen Thread mit einer Parameterliste wie {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = laufen zu lassen 1;}.
JM Hicks

Ok, ich habe die Indizes in der verknüpften Beispieldatendatei versteckt gefunden.
JM Hicks

Noch ein paar Fragen: 1) Wie viele Zeilen enthält die 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?
JM Hicks

Antworten:


3

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_warningsEinträge für zu löschen ivehicle_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:

CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN

    SELECT @idstring:= '';
    SELECT @idnum:= 0;
    SELECT @del_stmt:= '';

    SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
    FROM proc_warnings
    WHERE EXISTS (
        SELECT 0
        FROM day_position
        WHERE day_position.transaction_id = proc_warnings.transaction_id
        AND day_position.dirty_data = 1
        AND EXISTS (
            SELECT 0
            FROM ivehicle_days
            WHERE ivehicle_days.id = day_position.ivehicle_day_id
            AND ivehicle_days.ivehicle_id = input_vid
        )
    )
    ORDER BY idnum_col DESC
    LIMIT 1;

    IF (@idnum > 0) THEN
        SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');

        PREPARE del_stmt_hndl FROM @del_stmt;
        EXECUTE del_stmt_hndl;
        DEALLOCATE PREPARE del_stmt_hndl;
    END IF;
END;

Dies ist die Syntax zum Aufrufen des Programms aus einer Transaktion heraus:

CALL DeleteEntries(2);

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:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)

) ;


CREATE TABLE  `ivehicle_days` (
    ...,
    KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);

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:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;

CREATE TABLE  `ivehicle_days` (
    ...,
    UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);

CREATE TABLE  `proc_warnings` (

    .., /*rename primary key*/
    CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
    UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);

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):

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;

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):

DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
    SELECT 0
    FROM day_position
    FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)  
    WHERE day_position.transaction_id = proc_warnings.transaction_id
    AND day_position.dirty_data = 1
    AND EXISTS (
        SELECT 0
        FROM ivehicle_days
        FORCE INDEX (`ivehicle_days__vid_id_cvrng`)  
        WHERE ivehicle_days.id = day_position.ivehicle_day_id
        AND ivehicle_days.ivehicle_id = ?
    )
);

In Bezug auf # 2 unerwartetes Sperrverhalten.

Wie ich sehen kann, möchten beide Abfragen eine exklusive X-Sperre für eine Zeile mit Primärschlüssel = 53. Keiner von beiden muss jedoch Zeilen aus der Tabelle proc_warnings löschen. Ich verstehe nur nicht, warum der Index gesperrt ist.

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

... ein DELETE setzt im Allgemeinen Datensatzsperren für jeden Indexdatensatz, der bei der Verarbeitung der SQL-Anweisung gescannt wird. Es spielt keine Rolle, ob die Anweisung WHERE-Bedingungen enthält, die die Zeile ausschließen würden. InnoDB merkt sich nicht die genaue WHERE-Bedingung, sondern weiß nur, welche Indexbereiche gescannt wurden.

Sie haben auch oben erwähnt:

... für mich ist das Hauptmerkmal von READ COMMITTED, wie es mit Schlössern umgeht. Die Indexsperren nicht übereinstimmender Zeilen sollten freigegeben werden, dies ist jedoch nicht der Fall.

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:

Außerdem werden Datensatzsperren für nicht übereinstimmende Zeilen freigegeben, nachdem MySQL die WHERE-Bedingung ausgewertet hat.

Dies wird auch auf dieser Handbuchseite http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html wiederholt

Es gibt auch andere Auswirkungen der Verwendung der Isolationsstufe READ COMMITTED oder der Aktivierung von innodb_locks_unsafe_for_binlog: Datensatzsperren für nicht übereinstimmende Zeilen werden freigegeben, nachdem MySQL die WHERE-Bedingung ausgewertet hat.

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.

Darüber hinaus ist der Index auch dann nicht gesperrt, wenn die Tabelle proc_warnings leer ist

Die Datenbank kann keine Datensätze im Index sperren, wenn keine vorhanden sind.

Darüber hinaus ist der Index nicht gesperrt, wenn ... die Tabelle day_position weniger Zeilen enthält (dh einhundert Zeilen).

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.


Die WHEREBedingung 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 der proc_warningsTabelle zu vermeiden . Ich denke, ich werde einen Fehler bei MySQL melden. Danke für Ihre Hilfe.
Vitalidze

Ich würde auch nicht erwarten, dass sie das Sperrverhalten vermeiden. Ich würde erwarten, dass es gesperrt wird, da ich denke, dass die Dokumentation besagt, dass dies erwartet wird, unabhängig davon, ob wir die Abfrage auf diese Weise verarbeiten möchten oder nicht. Ich würde nur erwarten, dass das Entfernen des Leistungsproblems verhindert, dass die gleichzeitige Abfrage für eine so offensichtlich lange Zeitspanne (über 500 Sekunden) blockiert wird.
JM Hicks

Obwohl Ihr {WHERE} während der Join-Verarbeitung verwendet werden könnte, um einzuschränken, welche Zeilen in der Join-Berechnung enthalten sind, sehe ich erst, wie Ihre {WHERE} -Klausel pro gesperrter Zeile ausgewertet werden kann, wenn der gesamte Satz von Joins vorliegt auch berechnet. Für unsere Analyse vermute ich jedoch, dass Sie Recht haben, dass wir den Verdacht haben sollten, dass die WHERE-Bedingung nach Abschluss der Abfrage ausgewertet wird. Dies führt mich jedoch zu der gleichen allgemeinen Schlussfolgerung, dass die Leistung aufgelöst werden muss und dann der scheinbare Grad der Parallelität proportional zunimmt.
JM Hicks

Beachten Sie, dass durch ordnungsgemäße Indizes möglicherweise alle vollständigen Tabellenscans in der Tabelle proc_warnings vermieden werden können. Damit dies geschieht, muss das Abfrageoptimierungsprogramm für uns gut funktionieren, und unsere Indizes, Abfragen und Daten müssen gut funktionieren. Die Parameterwerte müssen am Ende Zeilen in der Zieltabelle auswerten, die sich zwischen den beiden Abfragen nicht überschneiden. Die Indizes müssen dem Abfrageoptimierer eine Möglichkeit bieten, effizient nach diesen Zeilen zu suchen. Wir brauchen den Optimierer, um diese potenzielle Sucheffizienz zu erkennen und einen solchen Plan auszuwählen.
JM Hicks

Wenn zwischen Parameterwerten, Indizes, nicht überlappenden Ergebnissen in der Tabelle proc_warnings und der Auswahl des Optimierungsplans alles in Ordnung ist, können Sperren für die Dauer der Ausführung der Abfrage für jeden Thread generiert werden, wenn nicht Überlappung, wird nicht mit den Sperranforderungen der anderen Threads in Konflikt stehen.
JM Hicks

3

Ich kann sehen, wie READ_COMMITTED diese Situation verursachen kann.

READ_COMMITTED ermöglicht drei Dinge:

  • Sichtbarkeit festgeschriebener Änderungen durch andere Transaktionen mithilfe der Isolationsstufe READ_COMMITTED .
  • Nicht wiederholbare Lesevorgänge: Transaktion, die denselben Abruf mit der Möglichkeit durchführt, jedes Mal ein anderes Ergebnis zu erhalten.
  • Phantome: Bei Transaktionen werden möglicherweise Zeilen angezeigt, die zuvor nicht sichtbar waren.

Dies schafft ein internes Paradigma für die Transaktion selbst, da die Transaktion Kontakt halten muss mit:

  • InnoDB-Pufferpool (solange das Festschreiben noch nicht ausgeführt ist)
  • Primärschlüssel der Tabelle
  • Möglicherweise
    • der doppelte Schreibpuffer
    • Tablespace rückgängig machen
  • Bildliche Darstellung

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

    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;

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_locksSie in der Frage angegeben

| 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' |

Mit Ausnahme von lock_idist lock_trx_idder 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:


Ich habe über die Dinge gelesen, die Sie in MySQL-Dokumenten beschreiben. Für mich ist das Hauptmerkmal von READ COMMITTED, wie es mit Schlössern umgeht . Die Indexsperren nicht übereinstimmender Zeilen sollten freigegeben werden, dies ist jedoch nicht der Fall.
Vitalidze

Wenn nur eine einzelne SQL-Anweisung aufgrund eines Fehlers zurückgesetzt wird, bleiben möglicherweise einige der von der Anweisung festgelegten Sperren erhalten. Dies liegt daran, dass InnoDB Zeilensperren in einem Format speichert, dass es nicht wissen kann, welche Sperre durch welche Anweisung gesetzt wurde: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA

Bitte beachten Sie, dass ich die Möglichkeit erwähnt habe, dass zwei Zeilen auf derselben Seite zum Sperren vorhanden sind (siehe Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA

Informationen zum Zurücksetzen einer einzelnen Anweisung - Ich verstehe dies so, als ob eine einzelne Anweisung, die innerhalb einer einzelnen Transaktion fehlschlägt, möglicherweise immer noch die Sperren enthält. Das ist okay. Meine große Frage ist, warum nach erfolgreicher Verarbeitung der DELETEAnweisung keine nicht übereinstimmenden Zeilensperren freigegeben werden .
Vitalidze

Bei zwei abgeschlossenen Sperren muss eine zurückgesetzt werden. Es ist möglich, dass Schlösser verweilen. ARBEITSTHEORIE: Die zurückgesetzte Transaktion wird möglicherweise erneut versucht und stößt möglicherweise auf eine alte Sperre der vorherigen Transaktion, in der sie gespeichert war.
RolandoMySQLDBA

2

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:

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;

Das äquivalente SELECT lautet:

SELECT pw.id
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=16 AND dp.dirty_data=1;

Wenn Sie sich die Erklärung ansehen, werden Sie sehen, dass der Ausführungsplan mit der proc_warningsTabelle 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=1und in der proc_warningsTabelle verknüpfen .

Das heißt, Sie müssen einen der Indizes patchen:

ALTER TABLE `day_position`
 DROP INDEX `day_position__id`,
 ADD INDEX `day_position__id`
   USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);

und schreiben Sie die Löschabfrage neu:

DELETE pw
FROM (
  SELECT DISTINCT dp.transaction_id
  FROM ivehicle_days vd
  JOIN day_position dp ON dp.ivehicle_day_id = vd.id
  WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;

Leider hilft das nicht, dh Zeilen in werden proc_warningsimmer noch gesperrt. Danke trotzdem.
Vitalidze

2

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:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
    p.id = c.parent_id
WHERE p.id = 1;

Sie können überprüfen, in welcher Isolationsstufe Sie sich befinden, indem Sie abfragen

SELECT @@tx_isolation;

Das ist nicht wahr. Warum SET AUTOCOMMIT=0sollte 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ächste START TRANSACTIONoder BEGINAussage nicht notwendig. Mein Zweck beim Deaktivieren der automatischen Festschreibung besteht darin, die Transaktion nach der DELETEAusführung der Anweisung geöffnet zu lassen .
Vitalidze

1
@SqlKiwi Dies war der Weg, um diesen Beitrag zu bearbeiten, und dies war derjenige, den man kommentieren sollte
;-)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.