Mysql
 sql >> Database >  >> RDS >> Mysql

Comprensione dei deadlock in MySQL e PostgreSQL

Lavorando con i database, il controllo della concorrenza è il concetto che garantisce che le transazioni del database vengano eseguite contemporaneamente senza violare l'integrità dei dati.

C'è molta teoria e approcci diversi attorno a questo concetto e su come realizzarlo, ma faremo brevemente riferimento al modo in cui PostgreSQL e MySQL (quando si usa InnoDB) lo gestiscono e un problema comune che può sorgere in sistemi altamente simultanei:deadlock.

Questi motori implementano il controllo della concorrenza utilizzando un metodo chiamato MVCC (Multiversion Concurrency Control). In questo metodo, durante l'aggiornamento di un elemento, le modifiche non sovrascriveranno i dati originali, ma verrà creata una nuova versione dell'elemento (con le modifiche). Pertanto avremo diverse versioni dell'elemento memorizzato.

Uno dei principali vantaggi di questo modello è che i lock acquisiti per interrogare (leggere) i dati non entrano in conflitto con i lock acquisiti per scrivere i dati, quindi la lettura non blocca mai la scrittura e la scrittura non blocca mai la lettura.

Ma, se vengono memorizzate più versioni dello stesso articolo, quale versione di esso verrà visualizzata da una transazione? Per rispondere a questa domanda dobbiamo rivedere il concetto di isolamento delle transazioni. Le transazioni specificano un livello di isolamento, che definisce il grado in cui una transazione deve essere isolata dalle modifiche di risorse o dati apportate da altre transazioni. Questo grado è direttamente correlato al blocco generato da una transazione e quindi, come può essere specificato a livello di transazione, può determinare l'impatto che una transazione in esecuzione può avere su altre transazioni in esecuzione.

Questo è un argomento molto interessante e lungo, anche se non entreremo nei dettagli in troppi dettagli in questo blog. Consigliamo la documentazione ufficiale di PostgreSQL e MySQL per ulteriori letture su questo argomento.

Quindi, perché affrontiamo gli argomenti di cui sopra quando ci occupiamo di deadlock? Perché i comandi sql acquisiranno automaticamente i blocchi per garantire il comportamento MVCC e il tipo di blocco acquisito dipende dall'isolamento della transazione definito.

Esistono diversi tipi di lock (ancora un altro argomento lungo e interessante da esaminare per PostgreSQL e MySQL), ma la cosa importante è come interagiscono (più esattamente, come entrano in conflitto) tra loro. Perché? Perché due transazioni non possono contenere contemporaneamente blocchi di modalità in conflitto sullo stesso oggetto. E un dettaglio non minore, una volta acquisito, un lock viene normalmente mantenuto fino al termine della transazione.

Questo è un esempio PostgreSQL di come i tipi di blocco sono in conflitto tra loro:

Conflitto di tipi di blocco PostgreSQL

E per MySQL:

Conflitto di tipi di blocco MySQL

X=blocco esclusivo         IX=blocco esclusivo dell'intenzione
S=blocco condiviso         IS=blocco condiviso per intenzione

Quindi cosa succede quando ho due transazioni in esecuzione che vogliono mantenere contemporaneamente blocchi in conflitto sullo stesso oggetto? Uno di loro otterrà il lucchetto e l'altro dovrà aspettare.

Quindi ora siamo in grado di capire veramente cosa sta succedendo durante una situazione di stallo.

Che cos'è uno stallo, allora? Come puoi immaginare, esistono diverse definizioni per un deadlock del database, ma mi piace la seguente per la sua semplicità.

Un deadlock del database è una situazione in cui due o più transazioni sono in attesa l'una dell'altra per rinunciare ai blocchi.

Quindi, ad esempio, la seguente situazione ci porterà a un punto morto:

Esempio di deadlock

Qui, l'applicazione A ottiene un blocco sulla tabella 1 riga 1 per effettuare un aggiornamento.

Allo stesso tempo, l'applicazione B ottiene un blocco sulla tabella 2 riga 2.

Ora l'applicazione A deve ottenere un blocco sulla tabella 2 riga 2, per continuare l'esecuzione e terminare la transazione, ma non può ottenere il blocco perché è trattenuta dall'applicazione B. L'applicazione A deve attendere che l'applicazione B la rilasci .

Ma l'applicazione B deve ottenere un blocco sulla tabella 1 riga 1, per continuare l'esecuzione e terminare la transazione, ma non può ottenere il blocco perché è trattenuto dall'applicazione A.

Quindi eccoci in una situazione di stallo. L'applicazione A è in attesa della risorsa trattenuta dall'applicazione B per terminare e l'applicazione B è in attesa della risorsa trattenuta dall'applicazione A. Quindi, come continuare? Il motore di database rileverà il deadlock e ucciderà una delle transazioni, sbloccando l'altra e generando un errore di deadlock su quella terminata.

Esaminiamo alcuni esempi di deadlock di PostgreSQL e MySQL:

PostgreSQL

Supponiamo di avere un database di test con informazioni dai paesi del mondo.

world=# SELECT code,region,population FROM country WHERE code IN ('NLD','AUS');
code |          region           | population
------+---------------------------+------------
NLD  | Western Europe            |   15864000
AUS  | Australia and New Zealand |   18886000
(2 rows)

Abbiamo due sessioni che vogliono apportare modifiche al database.

La prima sessione modificherà il campo regione per il codice NLD e il campo popolazione per il codice AUS.

La seconda sessione modificherà il campo regione per il codice AUS e il campo popolazione per il codice NLD.

Dati della tabella:

code: NLD
region: Western Europe
population: 15864000
code: AUS
region: Australia and New Zealand
population: 18886000

Sessione 1:

world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Europe' WHERE code='NLD';
UPDATE 1

Sessione 2:

world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Oceania' WHERE code='AUS';
UPDATE 1
world=# UPDATE country SET population=15864001 WHERE code='NLD';

La sessione 2 si bloccherà in attesa del termine della sessione 1.

Sessione 1:

world=# UPDATE country SET population=18886001 WHERE code='AUS';

ERROR:  deadlock detected
DETAIL:  Process 1181 waits for ShareLock on transaction 579; blocked by process 1148.
Process 1148 waits for ShareLock on transaction 578; blocked by process 1181.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,15) in relation "country"

Qui abbiamo il nostro punto morto. Il sistema ha rilevato il deadlock e ha terminato la sessione 1.

Sessione 2:

world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Oceania' WHERE code='AUS';
UPDATE 1
world=# UPDATE country SET population=15864001 WHERE code='NLD';
UPDATE 1

E possiamo verificare che la seconda sessione sia terminata correttamente dopo che è stato rilevato il deadlock e che la sessione 1 è stata uccisa (quindi, il blocco è stato rilasciato).

Per avere maggiori dettagli possiamo vedere il log nel nostro server PostgreSQL:

2018-05-16 12:56:38.520 -03 [1181] ERROR:  deadlock detected
2018-05-16 12:56:38.520 -03 [1181] DETAIL:  Process 1181 waits for ShareLock on transaction 579; blocked by process 1148.
       Process 1148 waits for ShareLock on transaction 578; blocked by process 1181.
       Process 1181: UPDATE country SET population=18886001 WHERE code='AUS';
       Process 1148: UPDATE country SET population=15864001 WHERE code='NLD';
2018-05-16 12:56:38.520 -03 [1181] HINT:  See server log for query details.
2018-05-16 12:56:38.520 -03 [1181] CONTEXT:  while updating tuple (0,15) in relation "country"
2018-05-16 12:56:38.520 -03 [1181] STATEMENT:  UPDATE country SET population=18886001 WHERE code='AUS';
2018-05-16 12:59:50.568 -03 [1181] ERROR:  current transaction is aborted, commands ignored until end of transaction block

Qui saremo in grado di vedere i comandi effettivi che sono stati rilevati in deadlock.

Scarica il whitepaper oggi Gestione e automazione di PostgreSQL con ClusterControlScopri cosa devi sapere per distribuire, monitorare, gestire e ridimensionare PostgreSQLScarica il whitepaper

MySQL

Per simulare un deadlock in MySQL possiamo fare quanto segue.

Come con PostgreSQL, supponiamo di avere un database di prova con informazioni su attori e film, tra le altre cose.

mysql> SELECT first_name,last_name FROM actor WHERE actor_id IN (1,7);
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| PENELOPE   | GUINESS   |
| GRACE      | MOSTEL    |
+------------+-----------+
2 rows in set (0.00 sec)

Abbiamo due processi che vogliono apportare modifiche al database.

Il primo processo modificherà il campo first_name per attore_id 1 e il campo last_name per attore_id 7.

Il secondo processo modificherà il campo first_name per attore_id 7 e il campo last_name per attore_id 1.

Dati della tabella:

actor_id: 1
first_name: PENELOPE
last_name: GUINESS
actor_id: 7
first_name: GRACE
last_name: MOSTEL

Sessione 1:

mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE actor SET first_name='GUINESS' WHERE actor_id='1';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Sessione 2:

mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE actor SET first_name='MOSTEL' WHERE actor_id='7';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1';

La sessione 2 si bloccherà in attesa del termine della sessione 1.

Sessione 1:

mysql> UPDATE actor SET last_name='GRACE' WHERE actor_id='7';

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Qui abbiamo il nostro punto morto. Il sistema ha rilevato il deadlock e ha terminato la sessione 1.

Sessione 2:

mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE actor SET first_name='MOSTEL' WHERE actor_id='7';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1';
Query OK, 1 row affected (8.52 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Come possiamo vedere nell'errore, come abbiamo visto per PostgreSQL, c'è un deadlock tra entrambi i processi.

Per maggiori dettagli possiamo usare il comando SHOW ENGINE INNODB STATUS\G:

mysql> SHOW ENGINE INNODB STATUS\G
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-05-16 18:55:46 0x7f4c34128700
*** (1) TRANSACTION:
TRANSACTION 1456, ACTIVE 33 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 54, OS thread handle 139965388506880, query id 15876 localhost root updating
UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1456 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc   ;;
1: len 6; hex 0000000005af; asc       ;;
2: len 7; hex 2d000001690110; asc -   i  ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afca8b3; asc Z   ;;

*** (2) TRANSACTION:
TRANSACTION 1455, ACTIVE 47 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 53, OS thread handle 139965267871488, query id 16013 localhost root updating
UPDATE actor SET last_name='GRACE' WHERE actor_id='7'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1455 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc   ;;
1: len 6; hex 0000000005af; asc       ;;
2: len 7; hex 2d000001690110; asc -   i  ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afca8b3; asc Z   ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1455 lock_mode X locks rec but not gap waiting
Record lock, heap no 202 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0007; asc   ;;
1: len 6; hex 0000000005b0; asc       ;;
2: len 7; hex 2e0000016a0110; asc .   j  ;;
3: len 6; hex 4d4f5354454c; asc MOSTEL;;
4: len 6; hex 4d4f5354454c; asc MOSTEL;;
5: len 4; hex 5afca8c1; asc Z   ;;

*** WE ROLL BACK TRANSACTION (2)

Sotto il titolo "ULTIMO DEADLOCK RILEVATO", possiamo vedere i dettagli del nostro deadlock.

Per vedere i dettagli del deadlock nel log degli errori di MySQL, dobbiamo abilitare l'opzione innodb_print_all_deadlocks nel nostro database.

mysql> set global innodb_print_all_deadlocks=1;
Query OK, 0 rows affected (0.00 sec)

Errore di registro MySQL:

2018-05-17T18:36:58.341835Z 12 [Note] InnoDB: Transactions deadlock detected, dumping detailed information.
2018-05-17T18:36:58.341869Z 12 [Note] InnoDB:
*** (1) TRANSACTION:
 
TRANSACTION 1812, ACTIVE 42 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 11, OS thread handle 140515492943616, query id 8467 localhost root updating
UPDATE actor SET last_name='PENELOPE' WHERE actor_id='1'
2018-05-17T18:36:58.341945Z 12 [Note] InnoDB: *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
 
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1812 lock_mode X locks rec but not gap waiting
Record lock, heap no 204 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc   ;;
1: len 6; hex 000000000713; asc       ;;
2: len 7; hex 330000016b0110; asc 3   k  ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afdcb89; asc Z   ;;
 
2018-05-17T18:36:58.342347Z 12 [Note] InnoDB: *** (2) TRANSACTION:
 
TRANSACTION 1811, ACTIVE 65 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 12, OS thread handle 140515492677376, query id 9075 localhost root updating
UPDATE actor SET last_name='GRACE' WHERE actor_id='7'
2018-05-17T18:36:58.342409Z 12 [Note] InnoDB: *** (2) HOLDS THE LOCK(S):
 
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1811 lock_mode X locks rec but not gap
Record lock, heap no 204 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0001; asc   ;;
1: len 6; hex 000000000713; asc       ;;
2: len 7; hex 330000016b0110; asc 3   k  ;;
3: len 7; hex 4755494e455353; asc GUINESS;;
4: len 7; hex 4755494e455353; asc GUINESS;;
5: len 4; hex 5afdcb89; asc Z   ;;
 
2018-05-17T18:36:58.342793Z 12 [Note] InnoDB: *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
 
RECORD LOCKS space id 23 page no 3 n bits 272 index PRIMARY of table `sakila`.`actor` trx id 1811 lock_mode X locks rec but not gap waiting
Record lock, heap no 205 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 2; hex 0007; asc   ;;
1: len 6; hex 000000000714; asc       ;;
2: len 7; hex 340000016c0110; asc 4   l  ;;
3: len 6; hex 4d4f5354454c; asc MOSTEL;;
4: len 6; hex 4d4f5354454c; asc MOSTEL;;
5: len 4; hex 5afdcba0; asc Z   ;;
 
2018-05-17T18:36:58.343105Z 12 [Note] InnoDB: *** WE ROLL BACK TRANSACTION (2)

Tenendo conto di ciò che abbiamo appreso in precedenza sul motivo per cui si verificano i deadlock, puoi vedere che non c'è molto che possiamo fare sul lato del database per evitarli. Ad ogni modo, come DBA è nostro dovere catturarli, analizzarli e fornire feedback agli sviluppatori.

La realtà è che questi errori sono specifici di ogni applicazione, quindi dovrai controllarli uno per uno e non c'è una guida che ti dica come risolverli. Tenendo presente questo, ci sono alcune cose che puoi cercare.

Suggerimenti per indagare ed evitare deadlock

Cerca le transazioni di lunga durata. Poiché i blocchi vengono generalmente mantenuti fino alla fine di una transazione, più lunga è la transazione, più lunghi saranno i blocchi sulle risorse. Se è possibile, prova a dividere le transazioni di lunga durata in quelle più piccole/più veloci.

A volte non è possibile dividere effettivamente le transazioni, quindi il lavoro dovrebbe concentrarsi sul tentativo di eseguire tali operazioni ogni volta in un ordine coerente, in modo che le transazioni formino code ben definite e non si blocchino.

Una soluzione alternativa che puoi anche proporre consiste nell'aggiungere la logica dei tentativi nell'applicazione (ovviamente, provare prima a risolvere il problema sottostante) in modo che, se si verifica un deadlock, l'applicazione eseguirà nuovamente gli stessi comandi.

Controlla i livelli di isolamento utilizzati, a volte provi a modificarli. Cerca comandi come SELECT FOR UPDATE e SELECT FOR SHARE, poiché generano blocchi espliciti e valuta se sono davvero necessari o puoi lavorare con uno snapshot precedente dei dati. Una cosa che puoi provare se non riesci a rimuovere questi comandi è usare un livello di isolamento inferiore come READ COMMITTED.

Naturalmente, aggiungi sempre indici ben scelti alle tue tabelle. Quindi le tue query devono scansionare meno record di indice e di conseguenza impostare meno blocchi.

A un livello superiore, come DBA puoi prendere alcune precauzioni per ridurre al minimo il blocco in generale. Per fare un esempio, in questo caso per PostgreSQL, puoi evitare di aggiungere un valore predefinito nello stesso comando in cui aggiungerai una colonna. La modifica di una tabella otterrà un blocco davvero aggressivo e l'impostazione di un valore predefinito per essa aggiornerà effettivamente le righe esistenti che hanno valori nulli, rendendo questa operazione molto lunga. Quindi, se dividi questa operazione in più comandi, aggiungendo la colonna, aggiungendo il valore predefinito, aggiornando i valori null, ridurrai al minimo l'impatto del blocco.

Naturalmente, ci sono un sacco di suggerimenti come questo che i DBA ottengono con la pratica (creare indici contemporaneamente, creare l'indice pk separatamente prima di aggiungere il pk e così via), ma l'importante è imparare e capire questo "modo di pensando" e sempre per ridurre al minimo l'impatto del blocco delle operazioni che stiamo facendo.

Riepilogo

Si spera che questo blog ti abbia fornito informazioni utili sui deadlock del database e su come superarli. Dal momento che non esiste un modo infallibile per evitare i deadlock, sapere come funzionano può aiutarti a catturarli prima che danneggino le istanze del database. Le soluzioni software come ClusterControl possono aiutarti a garantire che i tuoi database rimangano sempre in forma. ClusterControl ha già aiutato centinaia di aziende:la tua sarà la prossima? Scarica oggi la tua prova gratuita di ClusterControl per vedere se è la soluzione giusta per le tue esigenze di database.