MySQL InnoDB blocca la chiave primaria durante l'eliminazione anche in READ COMMITTED


11

Prefazione

La nostra applicazione esegue diversi thread che eseguono DELETEquery in parallelo. Le query influiscono su dati isolati, ovvero non dovrebbe esserci alcuna possibilità che si DELETEverifichino contemporaneamente sulle stesse righe da thread separati. Tuttavia, per documentazione, MySQL utilizza il cosiddetto blocco "chiave successiva" per le DELETEistruzioni, che blocca sia la chiave corrispondente sia un po 'di spazio. Questa cosa porta a dead-lock e l'unica soluzione che abbiamo trovato è usare il READ COMMITTEDlivello di isolamento.

Il problema

Il problema sorge quando si eseguono DELETEistruzioni complesse con JOINs di tabelle enormi. In un caso particolare abbiamo una tabella con avvisi che ha solo due righe, ma la query deve eliminare tutti gli avvisi che appartengono ad alcune entità particolari da due INNER JOINtabelle ed separate . La query è la seguente:

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

Quando la tabella day_position è abbastanza grande (nel mio caso di test ci sono 1448 righe) qualsiasi transazione anche con la READ COMMITTEDmodalità di isolamento blocca l' intera proc_warnings tabella.

Il problema viene sempre riprodotto su questi dati di esempio: http://yadi.sk/d/QDuwBtpW1BxB9 sia in MySQL 5.1 (verificato su 5.1.59) che MySQL 5.5 (controllato su MySQL 5.5.24).

EDIT: i dati di esempio collegati contengono anche schemi e indici per le tabelle di query, riprodotti qui per comodità:

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`)
);

Le query per transazioni sono le seguenti:

  • Transazione 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;
  • Transazione 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;

Uno di questi fallisce sempre con l'errore "Blocca il timeout di attesa superato ...". Il information_schema.innodb_trxcontiene le seguenti righe:

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

Come posso vedere, entrambe le query richiedono un Xblocco esclusivo su una riga con chiave primaria = 53. Tuttavia, nessuna delle due deve eliminare le righe dalla proc_warningstabella. Non capisco perché l'indice sia bloccato. Inoltre, l'indice non è bloccato né quando la proc_warningstabella è vuota o la day_positiontabella contiene un numero minore di righe (ovvero cento righe).

Ulteriore indagine fu di esaminare EXPLAINla SELECTquery simile . Mostra che Query Optimizer non utilizza l'indice per eseguire query sulla proc_warningstabella e questa è l'unica ragione per cui posso immaginare perché blocca l'intero indice della chiave primaria.

Caso semplificato

Il problema può anche essere riprodotto in un caso più semplice quando ci sono solo due tabelle con un paio di record, ma la tabella figlio non ha un indice sulla colonna ref della tabella padre.

Crea parenttabella

CREATE TABLE `parent` (
  `id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

Crea childtabella

CREATE TABLE `child` (
  `id` int(10) unsigned NOT NULL,
  `parent_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

Riempi le tabelle

INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);

Test in due transazioni parallele:

  • Transazione 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;
  • Transazione 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;

La parte comune in entrambi i casi è che MySQL non utilizza gli indici. Credo che sia la ragione del blocco dell'intero tavolo.

La nostra soluzione

L'unica soluzione che possiamo vedere per ora è aumentare il timeout di attesa del blocco predefinito da 50 secondi a 500 secondi per consentire la pulizia del filo. Quindi incrocia le dita.

Qualsiasi aiuto apprezzato.


Ho una domanda: hai eseguito COMMIT in una delle transazioni?
RolandoMySQLDBA,

Ovviamente. Il problema è che tutte le altre transazioni devono attendere fino a quando una di esse commette modifiche. Il semplice test case non contiene un'istruzione commit per mostrare come riprodurre il problema. Se esegui il commit o il rollback in una transazione non in attesa, rilascia simultaneamente il blocco e la transazione in attesa completa il suo funzionamento.
vitalidze,

Quando dici che MySQL non usa gli indici in entrambi i casi, è perché non ce ne sono nel vero scenario? Se ci sono indici potresti fornire il codice per loro? È possibile provare uno dei seguenti suggerimenti di indice pubblicati? Se non ci sono indici e non è possibile provare ad aggiungerne altri, MySQL non può limitare il set di dati elaborato da ciascun thread. In tal caso, N thread moltiplicherebbe semplicemente il carico di lavoro del server per N volte e sarebbe più efficiente consentire l'esecuzione di un solo thread con un elenco di parametri come {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
JM Hicks,

Ok, ho trovato gli indici nascosti nel file di dati di esempio collegato.
JM Hicks,

Altre due domande: 1) quante righe day_positioncontiene normalmente la tabella, quando inizia a funzionare così lentamente che devi superare il limite di timeout a 500 sec? 2) Quanto tempo ci vuole per avere solo i dati di esempio?
JM Hicks,

Risposte:


3

NUOVA RISPOSTA (SQL dinamico in stile MySQL): Ok, questo affronta il problema nel modo in cui è descritto uno degli altri poster - invertendo l'ordine in cui vengono acquisiti i blocchi esclusivi reciprocamente incompatibili in modo che indipendentemente da quanti si verifichino, si verificano solo per il minor tempo alla fine dell'esecuzione della transazione.

Ciò si ottiene separando la parte letta dell'istruzione nella propria istruzione select e generando dinamicamente un'istruzione di eliminazione che verrà forzata a eseguire per ultima a causa dell'ordine di visualizzazione dell'istruzione e che interesserà solo la tabella proc_warnings.

Una demo è disponibile su sql fiddle:

Questo collegamento mostra lo schema con i dati di esempio e una semplice query per le righe corrispondenti ivehicle_id=2. Ne risultano 2 righe, poiché nessuna di esse è stata eliminata.

Questo collegamento mostra lo stesso schema, dati di esempio, ma passa un valore 2 al programma memorizzato DeleteEntries, indicando a SP di eliminare le proc_warningsvoci per ivehicle_id=2. La semplice query per le righe non restituisce risultati poiché sono state tutte eliminate correttamente. I collegamenti demo dimostrano solo che il codice funziona come previsto per l'eliminazione. L'utente con l'ambiente di test corretto può commentare se ciò risolve il problema del thread bloccato.

Ecco anche il codice per comodità:

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;

Questa è la sintassi per chiamare il programma all'interno di una transazione:

CALL DeleteEntries(2);

RISPOSTA ORIGINALE (penso ancora che non sia troppo malandato) Sembra 2 problemi: 1) query lenta 2) comportamento di blocco imprevisto

Per quanto riguarda il problema n. 1, le query lente sono spesso risolte con le stesse due tecniche nella semplificazione dell'istruzione di query in tandem e aggiunte o modifiche utili agli indici. Tu stesso hai già effettuato la connessione agli indici - senza di essi l'ottimizzatore non può cercare un insieme limitato di righe da elaborare e ogni riga da ogni tabella moltiplicando per ogni riga aggiuntiva ha scansionato la quantità di lavoro extra che deve essere fatto.

RIVEDUTO DOPO VEDERE POST DI SCHEMA E INDICI: Ma immagino che otterrai il massimo beneficio in termini di prestazioni dalla tua query assicurandoti di avere una buona configurazione dell'indice. Per fare ciò, puoi optare per una migliore prestazione di eliminazione, e forse anche una migliore prestazione di eliminazione, con trade off di indici più grandi e forse prestazioni di inserzione notevolmente più lente sulle stesse tabelle a cui viene aggiunta una struttura di indice aggiuntiva.

QUALCOSA MIGLIORE:

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`)
);

RIVEDUTO QUI TROPPO: Dal momento che impiega tutto il tempo necessario per eseguire, lascerei lo dirty_data nell'indice e lo ho anche sbagliato di sicuro quando l'ho inserito dopo l'ivehicle_day_id nell'ordine dell'indice - dovrebbe essere il primo.

Ma se avessi messo le mani su di esso, a questo punto, dal momento che ci doveva essere una buona quantità di dati per far sì che ci mettesse così tanto tempo, mi limiterei a cercare tutti gli indici di copertura solo per assicurarmi di ottenere l'indicizzazione migliore che il mio tempo per la risoluzione dei problemi potrebbe comprare, se non altro per escludere quella parte del problema.

INDICI MIGLIORI / DI COPERTURA:

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`)
);

Esistono due obiettivi di ottimizzazione delle prestazioni ricercati dagli ultimi due suggerimenti di modifica:
1) Se le chiavi di ricerca per le tabelle a cui si accede successivamente non sono uguali ai risultati della chiave in cluster restituiti per la tabella a cui si accede attualmente, eliminiamo ciò che sarebbe stato necessario apportare una seconda serie di operazioni di ricerca dell'indice con scansione sull'indice cluster
2) Se quest'ultimo non è il caso, esiste almeno la possibilità che l'ottimizzatore possa selezionare un algoritmo di join più efficiente poiché gli indici manterranno il chiavi di join richieste in ordine ordinato.

La tua query sembra semplificata al massimo (può essere copiata qui nel caso venga modificata in seguito):

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;

A meno che ovviamente non ci sia qualcosa nell'ordine di join scritto che influisce sul modo in cui procede Query Optimizer, nel qual caso è possibile provare alcuni dei suggerimenti di riscrittura forniti da altri, incluso forse questo con suggerimenti per l'indice (facoltativo):

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 = ?
    )
);

Per quanto riguarda il n. 2, comportamento di blocco imprevisto.

Come posso vedere, entrambe le query vogliono un esclusivo blocco X su una riga con chiave primaria = 53. Tuttavia, nessuno dei due deve eliminare le righe dalla tabella proc_warnings. Non capisco perché l'indice sia bloccato.

Immagino che sarebbe l'indice bloccato perché la riga di dati da bloccare si trova in un indice cluster, ovvero la singola riga di dati stessa risiede nell'indice.

Sarebbe bloccato, perché:
1) secondo http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html

... un DELETE generalmente imposta blocchi di record su ogni record di indice che viene scansionato nell'elaborazione dell'istruzione SQL. Non importa se ci sono condizioni WHERE nell'istruzione che escluderebbero la riga. InnoDB non ricorda l'esatta condizione WHERE, ma conosce solo quali intervalli di indici sono stati scansionati.

Hai anche menzionato sopra:

... per quanto mi riguarda la caratteristica principale di READ COMMITTED è il modo in cui gestisce i blocchi. Dovrebbe rilasciare i blocchi di indice delle righe non corrispondenti, ma non lo fa.

e fornito il seguente riferimento per questo:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed

Che afferma lo stesso di te, tranne che secondo lo stesso riferimento c'è una condizione su cui deve essere rilasciato un lucchetto:

Inoltre, i blocchi dei record per le righe non corrispondenti vengono rilasciati dopo che MySQL ha valutato la condizione WHERE.

Che è ribadito anche in questa pagina di manuale http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html

Ci sono anche altri effetti dell'utilizzo del livello di isolamento READ COMMITTED o dell'abilitazione di innodb_locks_unsafe_for_binlog: i blocchi dei record per le righe non corrispondenti vengono rilasciati dopo che MySQL ha valutato la condizione WHERE.

Quindi, ci viene detto che la condizione WHERE deve essere valutata prima di poter rilasciare il blocco. Sfortunatamente non ci viene detto quando viene valutata la condizione WHERE e probabilmente qualcosa potrebbe cambiare da un piano all'altro creato dall'ottimizzatore. Ma ci dice che il rilascio del blocco dipende in qualche modo dalle prestazioni dell'esecuzione della query, l'ottimizzazione di cui, come discusso in precedenza, dipende dall'attenta scrittura dell'istruzione e dall'uso giudizioso degli indici. Può anche essere migliorato da un miglior design del tavolo, ma sarebbe probabilmente meglio lasciarlo a una domanda separata.

Inoltre, l'indice non è bloccato neanche quando la tabella proc_warnings è vuota

Il database non può bloccare i record all'interno dell'indice se non ce ne sono.

Inoltre, l'indice non è bloccato quando ... la tabella day_position contiene un numero minore di righe (ovvero cento righe).

Ciò potrebbe significare numerose cose come, ma probabilmente non limitato a: un piano di esecuzione diverso a causa di un cambiamento nelle statistiche, un blocco troppo breve per essere osservato a causa di un'esecuzione molto più veloce a causa di un set di dati molto più piccolo / operazione di partecipazione.


La WHEREcondizione viene valutata al completamento della query. No? Ho pensato che il blocco venisse rilasciato subito dopo l'esecuzione di alcune query simultanee. Questo è il comportamento naturale. Tuttavia, ciò non accade. Nessuna delle query suggerite in questo thread consente di evitare il blocco dell'indice cluster nella proc_warningstabella. Penso che presenterò un bug su MySQL. Grazie per l'aiuto.
vitalidze,

Non mi aspetterei nemmeno che evitino il comportamento di blocco. Mi aspetterei che si blocchi perché penso che la documentazione affermi che è quello che ci si aspetta, indipendentemente dal modo in cui vorremmo che elaborasse la query. Mi aspetterei solo che sbarazzarsi del problema di prestazioni manterrà il blocco della query simultanea per un periodo di tempo così evidentemente (oltre 500 secondi).
JM Hicks,

Sebbene la tua {WHERE} possa essere utilizzata durante l'elaborazione del join per limitare le righe incluse nel calcolo del join, non vedo come la tua clausola {WHERE} possa essere valutata per riga bloccata fino a quando l'intero set di join è calcolato anche. Detto questo, per la nostra analisi, sospetto che tu abbia ragione nel sospettare che "La condizione WHERE viene valutata al termine della query". Tuttavia, ciò mi porta alla stessa conclusione generale, che le prestazioni devono essere risolte e quindi il grado apparente di concorrenza aumenterà in modo proporzionale.
JM Hicks,

Ricordare che gli indici corretti possono potenzialmente eliminare qualsiasi scansione della tabella completa che si verifica nella tabella proc_warnings. Affinché ciò accada, abbiamo bisogno di Query Optimizer che funzioni bene per noi, e abbiamo bisogno che i nostri indici, query e dati funzionino bene con esso. I valori dei parametri devono valutare alla fine le righe nella tabella di destinazione che non si sovrappongono tra le due query. Gli indici devono fornire allo Strumento per ottimizzare le query un mezzo per cercare in modo efficiente quelle righe. Abbiamo bisogno dell'ottimizzatore per realizzare quella potenziale efficienza di ricerca e selezionare un tale piano.
JM Hicks,

Se tutto va bene tra i valori dei parametri, gli indici, i risultati non sovrapposti nella tabella proc_warnings e la selezione del piano di ottimizzazione, anche se i blocchi possono essere generati per la durata del tempo necessario per eseguire la query per ciascun thread, quei blocchi, se no sovrapposti, non entreranno in conflitto con le richieste di blocco degli altri thread.
JM Hicks,

3

Vedo come READ_COMMITTED può causare questa situazione.

READ_COMMITTED consente tre cose:

  • Visibilità delle modifiche apportate da altre transazioni utilizzando il livello di isolamento READ_COMMITTED .
  • Letture non ripetibili: transazione che esegue lo stesso recupero con la possibilità di ottenere ogni volta un risultato diverso.
  • Fantasmi: è possibile che vengano visualizzate righe nelle transazioni che prima non erano visibili.

Questo crea un paradigma interno per la transazione stessa perché la transazione deve mantenere il contatto con:

  • Pool di buffer InnoDB (mentre il commit è ancora non scaricato)
  • Chiave primaria della tabella
  • Possibilmente
    • il doppio buffer di scrittura
    • Annulla tablespace
  • Rappresentazione pittorica

Se due distinte transazioni READ_COMMITTED accedono alle stesse tabelle / righe che vengono aggiornate allo stesso modo, sii pronto ad aspettarti non un blocco della tabella, ma un blocco esclusivo all'interno di gen_clust_index (aka Clustered Index) . Date le domande dal tuo caso semplificato:

  • Transazione 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;
  • Transazione 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;

Stai bloccando la stessa posizione in gen_clust_index. Si potrebbe dire "ma ogni transazione ha una chiave primaria diversa". Sfortunatamente, questo non è il caso agli occhi di InnoDB. Accade solo che id 1 e id 2 risiedano sulla stessa pagina.

Guarda indietro che information_schema.innodb_lockshai fornito nella domanda

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

Ad eccezione di lock_id, lock_trx_idil resto della descrizione del blocco è identico. Poiché le transazioni sono sullo stesso piano di parità (stesso isolamento delle transazioni), ciò dovrebbe effettivamente accadere .

Credetemi, ho già affrontato questo tipo di situazione prima. Ecco i miei post precedenti su questo:


Ho letto delle cose che descrivi nei documenti MySQL. Ma per quanto mi riguarda la caratteristica principale di READ COMMITTED è il modo in cui gestisce i blocchi . Dovrebbe rilasciare i blocchi di indice delle righe non corrispondenti, ma non lo fa.
vitalidze,

Se viene eseguito il rollback di una sola istruzione SQL a seguito di un errore, alcuni dei blocchi impostati dall'istruzione potrebbero essere conservati. Ciò accade perché InnoDB archivia i blocchi di riga in un formato tale da non poter sapere in seguito quale blocco è stato impostato da quale istruzione: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA

Si prega di notare che ho menzionato la possibilità di due file esistenti nella stessa pagina per il blocco (Vedi Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA

Informazioni sul rollback di una singola istruzione: lo capisco come se una singola istruzione fallisce all'interno di una singola transazione, potrebbe comunque contenere i blocchi. Va bene. La mia grande domanda è perché non rilascia blocchi di riga non corrispondenti dopo aver elaborato correttamente l' DELETEistruzione.
vitalidze,

Con due blocchi di completamento, uno deve essere ripristinato. È possibile che i blocchi possano persistere. TEORIA DI LAVORO: la transazione che è stata ripristinata potrebbe riprovare e potrebbe riscontrare un vecchio blocco rispetto alla transazione precedente che la deteneva.
RolandoMySQLDBA,

2

Ho guardato la domanda e la spiegazione. Non sono sicuro, ma ho la sensazione che il problema sia il seguente. Diamo un'occhiata alla query:

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;

Il SELECT equivalente è:

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;

Se guardi la sua spiegazione, vedrai che il piano di esecuzione inizia con la proc_warningstabella. Ciò significa che MySQL esegue la scansione della chiave primaria nella tabella e per ogni riga verifica se la condizione è vera e, in caso affermativo, la riga viene eliminata. Cioè MySQL deve bloccare l'intera chiave primaria.

Ciò di cui hai bisogno è invertire l'ordine JOIN, ovvero trovare tutti gli ID transazione vd.ivehicle_id=16 AND dp.dirty_data=1 e unirli sul proc_warningstavolo.

Cioè dovrai correggere uno degli indici:

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

e riscrivi la query di eliminazione:

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;

Purtroppo questo non aiuta, vale a dire che le righe proc_warningsrimangono bloccate. Grazie comunque.
vitalidze,

2

Quando imposti il ​​livello di transazione senza il modo in cui lo fai, applica il commit di lettura solo alla transazione successiva, quindi (imposta il commit automatico). Questo significa che dopo autocommit = 0, non sei più in Read Committed. Lo scriverei in questo modo:

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;

Puoi verificare a quale livello di isolamento ti trovi eseguendo una query

SELECT @@tx_isolation;

Non è vero. Perché SET AUTOCOMMIT=0dovrebbe ripristinare il livello di isolamento per la prossima transazione? Credo che inizi una nuova transazione se nessuna è stata avviata prima (che è il mio caso). Quindi, per essere più precisi, non è necessario il prossimo START TRANSACTIONo l' BEGINaffermazione. Il mio scopo di disabilitare autocommit è lasciare la transazione aperta dopo l' DELETEesecuzione dell'istruzione.
vitalidze,

1
@SqlKiwi questo era il modo di modificare questo post e questo era quello su cui commentare ;-)
jcolebrand
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.