När du arbetar med databaser är samtidighetskontroll konceptet som säkerställer att databastransaktioner utförs samtidigt utan att kränka dataintegriteten.
Det finns mycket teori och olika tillvägagångssätt kring detta koncept och hur man uppnår det, men vi kommer kort att hänvisa till hur PostgreSQL och MySQL (när man använder InnoDB) hanterar det, och ett vanligt problem som kan uppstå i mycket samtidiga system:låsningar.
Dessa motorer implementerar samtidighetskontroll genom att använda en metod som kallas MVCC (Multiversion Concurrency Control). I den här metoden, när ett objekt uppdateras, kommer ändringarna inte att skriva över originaldata, utan istället skapas en ny version av objektet (med ändringarna). Således kommer vi att ha flera versioner av objektet lagrade.
En av de största fördelarna med denna modell är att lås som förvärvats för att fråga (läsa) data inte kommer i konflikt med lås som förvärvats för att skriva data, och därför blockerar läsning aldrig skrivning och skrivning blockerar aldrig läsning.
Men om flera versioner av samma objekt lagras, vilken version av det kommer en transaktion att se? För att svara på den frågan måste vi se över konceptet med transaktionsisolering. Transaktioner anger en isoleringsnivå som definierar i vilken grad en transaktion måste isoleras från resurs- eller dataändringar gjorda av andra transaktioner. Denna grad är direkt relaterad till låsningen som genereras av en transaktion, och så, eftersom den kan specificeras på transaktionsnivå, kan den bestämma effekten som en löpande transaktion kan ha över andra pågående transaktioner.
Det här är ett mycket intressant och långt ämne, även om vi inte kommer att gå in på för många detaljer i den här bloggen. Vi rekommenderar den officiella PostgreSQL- och MySQL-dokumentationen för vidare läsning om detta ämne.
Så varför går vi in på ovanstående ämnen när vi har att göra med dödlägen? Eftersom sql-kommandon automatiskt skaffar lås för att säkerställa MVCC-beteendet, och låstypen som förvärvas beror på den definierade transaktionsisoleringen.
Det finns flera typer av lås (återigen ett långt och intressant ämne att granska för PostgreSQL och MySQL), men det viktiga med dem är hur de interagerar (mest exakt hur de konflikter) med varandra. Varför är det så? Eftersom två transaktioner inte kan hålla lås av motstridiga lägen på samma objekt samtidigt. Och en icke-mindre detalj, när den väl har förvärvats, hålls ett lås normalt till slutet av transaktionen.
Det här är ett PostgreSQL-exempel på hur låsningstyper kommer i konflikt med varandra:
PostgreSQL Låsningstyper konfliktOch för MySQL:
MySQL-låsningstyper konfliktX=exklusivt lås IX=intention exklusivt lås
S=delat lås IS=avsikt delat lås
Så vad händer när jag har två pågående transaktioner som vill ha motstridiga lås på samma objekt samtidigt? En av dem kommer att få låset och den andra får vänta.
Så nu är vi i en position att verkligen förstå vad som händer under ett dödläge.
Vad är ett dödläge då? Som du kan föreställa dig finns det flera definitioner för ett låst databas, men jag gillar följande för dess enkelhet.
Ett låst databas är en situation där två eller flera transaktioner väntar på att varandra ska ge upp låsningar.
Så till exempel kommer följande situation att leda oss till ett dödläge:
exempel på dödlägeHär får applikation A ett lås på tabell 1 rad 1 för att göra en uppdatering.
Samtidigt får applikation B ett lås på tabell 2 rad 2.
Nu behöver applikation A få ett lås på tabell 2 rad 2 för att kunna fortsätta exekveringen och slutföra transaktionen, men den kan inte få låset eftersom det hålls av applikation B. Applikation A måste vänta på att applikation B släpper det .
Men applikation B behöver få ett lås på tabell 1 rad 1 för att kunna fortsätta exekveringen och slutföra transaktionen, men den kan inte få låset eftersom det hålls av applikation A.
Så här är vi i ett dödläge. Applikation A väntar på resursen som innehas av applikation B för att slutföras och applikation B väntar på resursen som innehas av applikation A. Så, hur fortsätter man? Databasmotorn kommer att upptäcka dödläget och döda en av transaktionerna, avblockera den andra och skapa ett dödlägesfel på den dödade.
Låt oss titta på några exempel på PostgreSQL och MySQL dödläge:
PostgreSQL
Anta att vi har en testdatabas med information från världens länder.
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)
Vi har två sessioner som vill göra ändringar i databasen.
Den första sessionen kommer att ändra regionfältet för NLD-koden och befolkningsfältet för AUS-koden.
Den andra sessionen kommer att ändra regionfältet för AUS-koden och befolkningsfältet för NLD-koden.
Tabelldata:
code: NLD
region: Western Europe
population: 15864000
code: AUS
region: Australia and New Zealand
population: 18886000
Session 1:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Europe' WHERE code='NLD';
UPDATE 1
Session 2:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Oceania' WHERE code='AUS';
UPDATE 1
world=# UPDATE country SET population=15864001 WHERE code='NLD';
Session 2 kommer att hänga i väntan på att Session 1 ska slutföras.
Session 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"
Här har vi vårt dödläge. Systemet upptäckte dödläget och dödade session 1.
Session 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
Och vi kan kontrollera att den andra sessionen avslutades korrekt efter att dödläget upptäcktes och session 1 dödades (därmed släpptes låset).
För att få mer information kan vi se loggen i vår PostgreSQL-server:
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
Här kommer vi att kunna se de faktiska kommandon som upptäcktes vid dödläge.
Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda WhitepaperMySQL
För att simulera ett dödläge i MySQL kan vi göra följande.
Som med PostgreSQL, anta att vi har en testdatabas med information om bland annat skådespelare och filmer.
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)
Vi har två processer som vill göra ändringar i databasen.
Den första processen kommer att ändra fältet förnamn för actor_id 1 och fältet efternamn för actor_id 7.
Den andra processen kommer att ändra fältet first_name för actor_id 7 och fältet last_name för actor_id 1.
Tabelldata:
actor_id: 1
first_name: PENELOPE
last_name: GUINESS
actor_id: 7
first_name: GRACE
last_name: MOSTEL
Session 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
Session 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';
Session 2 kommer att hänga i väntan på att Session 1 ska slutföras.
Session 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
Här har vi vårt dödläge. Systemet upptäckte dödläget och dödade session 1.
Session 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
Som vi kan se i felet, som vi såg för PostgreSQL, finns det ett dödläge mellan båda processerna.
För mer information kan vi använda kommandot 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)
Under rubriken "SENAST DETECTED DEADLOCK" kan vi se detaljer om vårt dödläge.
För att se detaljerna om dödläget i mysql-felloggen måste vi aktivera alternativet innodb_print_all_deadlocks i vår databas.
mysql> set global innodb_print_all_deadlocks=1;
Query OK, 0 rows affected (0.00 sec)
MySQL-loggfel:
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)
Med hänsyn till vad vi har lärt oss ovan om varför dödlägen inträffar, kan du se att det inte finns mycket vi kan göra på databassidan för att undvika dem. Hur som helst, som DBA:er är det vår plikt att faktiskt fånga dem, analysera dem och ge feedback till utvecklarna.
Verkligheten är att dessa fel är specifika för varje applikation, så du måste kontrollera dem en efter en och det finns ingen guide som berättar hur du felsöker detta. Med detta i åtanke finns det några saker du kan leta efter.
Tips för att undersöka och undvika dödlägen
Sök efter långvariga transaktioner. Eftersom låsen vanligtvis hålls till slutet av en transaktion, ju längre transaktionen är, desto längre låser resurserna. Om det är möjligt, försök att dela upp långvariga transaktioner i mindre/snabbare.
Ibland är det inte möjligt att faktiskt dela upp transaktionerna, så arbetet bör fokusera på att försöka utföra dessa operationer i en konsekvent ordning varje gång, så att transaktioner bildar väldefinierade köer och inte låser sig.
En lösning som du också kan föreslå är att lägga till logik för omförsök i applikationen (naturligtvis, försök att lösa det underliggande problemet först) på ett sätt som, om ett dödläge inträffar, kommer applikationen att köra samma kommandon igen.
Kontrollera vilka isoleringsnivåer som används, ibland försöker du genom att ändra dem. Leta efter kommandon som SELECT FOR UPDATE och SELECT FOR SHARE, eftersom de genererar explicita lås, och utvärdera om de verkligen behövs eller så kan du arbeta med en äldre ögonblicksbild av data. En sak du kan prova om du inte kan ta bort dessa kommandon är att använda en lägre isoleringsnivå, som t.ex. READ COMMITTED.
Lägg naturligtvis alltid till väl valda index i dina tabeller. Då behöver dina frågor skanna färre indexposter och följaktligen ställa in färre lås.
På en högre nivå kan du som DBA vidta några försiktighetsåtgärder för att minimera låsning i allmänhet. För att nämna ett exempel, i det här fallet för PostgreSQL, kan du undvika att lägga till ett standardvärde i samma kommando som du lägger till en kolumn. Att ändra en tabell kommer att få ett riktigt aggressivt lås, och att ställa in ett standardvärde för den kommer faktiskt att uppdatera de befintliga raderna som har nollvärden, vilket gör att denna operation tar väldigt lång tid. Så om du delar upp den här operationen i flera kommandon, lägger till kolumnen, lägger till standard, uppdaterar nollvärdena, kommer du att minimera låseffekten.
Naturligtvis finns det massor av tips som detta som DBA:erna får med övningen (att skapa index samtidigt, skapa pk-indexet separat innan de lägger till pk, och så vidare), men det viktiga är att lära sig och förstå detta "sätt att tänkande" och alltid för att minimera låseffekten av de operationer vi gör.
Sammanfattning
Förhoppningsvis har den här bloggen försett dig med användbar information om låsta databaser och hur du kan övervinna dem. Eftersom det inte finns ett säkert sätt att undvika dödlägen kan det hjälpa dig att fånga dem innan de skadar dina databasinstanser genom att veta hur de fungerar. Programvarulösningar som ClusterControl kan hjälpa dig att säkerställa att dina databaser alltid håller sig i form. ClusterControl har redan hjälpt hundratals företag - kommer ditt nästa? Ladda ner din kostnadsfria testversion av ClusterControl idag för att se om den passar dina databasbehov.