Podczas pracy z bazami danych kontrola współbieżności to koncepcja, która zapewnia, że transakcje w bazie danych są wykonywane jednocześnie bez naruszania integralności danych.
Istnieje wiele teorii i różnych podejść wokół tej koncepcji i sposobów jej realizacji, ale pokrótce odniesiemy się do sposobu, w jaki PostgreSQL i MySQL (w przypadku korzystania z InnoDB) radzą sobie z tym, oraz częsty problem, który może pojawić się w wysoce współbieżnych systemach:zakleszczenia.
Te aparaty implementują kontrolę współbieżności przy użyciu metody o nazwie MVCC (Multiversion Concurrency Control). W tej metodzie, gdy element jest aktualizowany, zmiany nie zastąpią oryginalnych danych, ale zamiast tego zostanie utworzona nowa wersja elementu (ze zmianami). W ten sposób będziemy mieć kilka wersji tego elementu.
Jedną z głównych zalet tego modelu jest to, że blokady nabyte w celu odpytywania (odczytu) danych nie kolidują z blokadami nabytymi do zapisywania danych, więc czytanie nigdy nie blokuje zapisu, a pisanie nigdy nie blokuje czytania.
Ale jeśli przechowywanych jest kilka wersji tego samego elementu, którą wersję zobaczy transakcja? Aby odpowiedzieć na to pytanie, musimy przyjrzeć się koncepcji izolacji transakcji. Transakcje określają poziom izolacji, który określa stopień, w jakim jedna transakcja musi być odizolowana od modyfikacji zasobów lub danych dokonywanych przez inne transakcje. Ten stopień jest bezpośrednio związany z blokowaniem generowanym przez transakcję, a zatem, ponieważ można go określić na poziomie transakcji, może określić wpływ, jaki działająca transakcja może mieć na inne działające transakcje.
To bardzo ciekawy i długi temat, choć na tym blogu nie będziemy się zagłębiać w zbyt wiele szczegółów. Do dalszego czytania na ten temat zalecamy oficjalną dokumentację PostgreSQL i MySQL.
Dlaczego więc zajmujemy się powyższymi tematami, gdy mamy do czynienia z impasami? Ponieważ polecenia sql automatycznie uzyskają blokady, aby zapewnić zachowanie MVCC, a uzyskany typ blokady zależy od zdefiniowanej izolacji transakcji.
Istnieje kilka rodzajów blokad (to kolejny długi i interesujący temat do przejrzenia dla PostgreSQL i MySQL), ale najważniejszą rzeczą w nich jest to, w jaki sposób wchodzą ze sobą w interakcje (a najdokładniej, w jaki sposób są w konflikcie). Dlaczego? Ponieważ dwie transakcje nie mogą jednocześnie blokować sprzecznych trybów na tym samym obiekcie. A nie drobny szczegół, po nabyciu, blokada jest zwykle utrzymywana do końca transakcji.
Oto przykład PostgreSQL pokazujący konflikt typów blokowania:
Konflikt typów blokowania PostgreSQLA dla MySQL:
Konflikt typów blokowania MySQLX=blokada na wyłączność IX=cel blokady na wyłączność
S=wspólna blokada IS=intencja wspólna blokada
Więc co się stanie, gdy mam dwie uruchomione transakcje, które chcą jednocześnie utrzymywać sprzeczne blokady na tym samym obiekcie? Jeden z nich dostanie zamek, a drugi będzie musiał poczekać.
Więc teraz jesteśmy w stanie naprawdę zrozumieć, co dzieje się podczas impasu.
Czym zatem jest impas? Jak możesz sobie wyobrazić, istnieje kilka definicji impasu w bazie danych, ale ze względu na prostotę podoba mi się poniższe.
Zakleszczenie bazy danych to sytuacja, w której dwie lub więcej transakcji czeka na siebie na zwolnienie blokad.
Na przykład następująca sytuacja doprowadzi nas do impasu:
Przykład zakleszczeniaTutaj aplikacja A zostaje zablokowana w tabeli 1 wiersz 1 w celu dokonania aktualizacji.
W tym samym czasie aplikacja B blokuje się w tabeli 2 wiersz 2.
Teraz aplikacja A musi uzyskać blokadę w tabeli 2 wiersz 2, aby kontynuować wykonywanie i zakończyć transakcję, ale nie może uzyskać blokady, ponieważ jest ona utrzymywana przez aplikację B. Aplikacja A musi poczekać, aż aplikacja B ją zwolni .
Jednak aplikacja B musi uzyskać blokadę w tabeli 1 wiersz 1, aby kontynuować wykonywanie i zakończyć transakcję, ale nie może uzyskać blokady, ponieważ jest ona utrzymywana przez aplikację A.
Więc tutaj znajdujemy się w sytuacji impasu. Aplikacja A czeka na zasób posiadany przez aplikację B, aby zakończyć, a aplikacja B czeka na zasób posiadany przez aplikację A. Jak więc kontynuować? Silnik bazy danych wykryje zakleszczenie i zabije jedną z transakcji, odblokuje drugą i zgłosi błąd zakleszczenia w zabitej transakcji.
Sprawdźmy kilka przykładów zakleszczeń w PostgreSQL i MySQL:
PostgreSQL
Załóżmy, że mamy testową bazę danych zawierającą informacje z krajów świata.
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)
Mamy dwie sesje, które chcą wprowadzić zmiany w bazie danych.
Pierwsza sesja zmodyfikuje pole regionu dla kodu NLD i pole populacji dla kodu AUS.
Druga sesja zmodyfikuje pole regionu dla kodu AUS i pole populacji dla kodu NLD.
Dane tabeli:
code: NLD
region: Western Europe
population: 15864000
code: AUS
region: Australia and New Zealand
population: 18886000
Sesja 1:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Europe' WHERE code='NLD';
UPDATE 1
Sesja 2:
world=# BEGIN;
BEGIN
world=# UPDATE country SET region='Oceania' WHERE code='AUS';
UPDATE 1
world=# UPDATE country SET population=15864001 WHERE code='NLD';
Sesja 2 zawiesza się w oczekiwaniu na zakończenie Sesji 1.
Sesja 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"
Tutaj mamy nasz impas. System wykrył zakleszczenie i zabitą sesję 1.
Sesja 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
I możemy sprawdzić, czy druga sesja zakończyła się poprawnie po wykryciu zakleszczenia i zabiciu sesji 1 (w ten sposób blokada została zwolniona).
Aby uzyskać więcej informacji, możemy zobaczyć dziennik na naszym serwerze 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
Tutaj będziemy mogli zobaczyć rzeczywiste polecenia wykryte w momencie zakleszczenia.
Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokumentMySQL
Aby zasymulować zakleszczenie w MySQL, możemy wykonać następujące czynności.
Podobnie jak w przypadku PostgreSQL, załóżmy, że mamy testową bazę danych zawierającą między innymi informacje o aktorach i filmach.
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)
Mamy dwa procesy, które chcą wprowadzić zmiany w bazie danych.
Pierwszy proces zmodyfikuje pole first_name dla actor_id 1 i last_name dla actor_id 7.
Drugi proces zmodyfikuje pole first_name dla actor_id 7 i last_name dla actor_id 1.
Dane tabeli:
actor_id: 1
first_name: PENELOPE
last_name: GUINESS
actor_id: 7
first_name: GRACE
last_name: MOSTEL
Sesja 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
Sesja 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';
Sesja 2 zawiesza się w oczekiwaniu na zakończenie Sesji 1.
Sesja 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
Tutaj mamy nasz impas. System wykrył zakleszczenie i zabitą sesję 1.
Sesja 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
Jak widać w błędzie, jak widzieliśmy w przypadku PostgreSQL, istnieje impas między obydwoma procesami.
Aby uzyskać więcej informacji, możemy użyć polecenia 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)
Pod tytułem „OSTATNI WYKRYTY BLOK” możemy zobaczyć szczegóły naszego impasu.
Aby zobaczyć szczegóły zakleszczenia w dzienniku błędów mysql, musimy włączyć opcję innodb_print_all_deadlocks w naszej bazie danych.
mysql> set global innodb_print_all_deadlocks=1;
Query OK, 0 rows affected (0.00 sec)
Błąd dziennika 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)
Biorąc pod uwagę to, czego nauczyliśmy się powyżej o przyczynach występowania zakleszczeń, widać, że po stronie bazy danych niewiele możemy zrobić, aby ich uniknąć. W każdym razie, jako administratorzy baz danych, naszym obowiązkiem jest ich wyłapywanie, analizowanie i przekazywanie opinii programistom.
W rzeczywistości te błędy są specyficzne dla każdej aplikacji, więc będziesz musiał je sprawdzić jeden po drugim i nie ma przewodnika, który by ci powiedział, jak rozwiązać ten problem. Mając to na uwadze, jest kilka rzeczy, których możesz szukać.
Wskazówki dotyczące badania i unikania zakleszczeń
Szukaj długotrwałych transakcji. Ponieważ blokady są zwykle utrzymywane do końca transakcji, im dłuższa transakcja, tym dłuższe blokady zasobów. Jeśli to możliwe, spróbuj podzielić długoterminowe transakcje na mniejsze/szybsze.
Czasami nie jest możliwe faktyczne rozdzielenie transakcji, więc praca powinna koncentrować się na próbie wykonania tych operacji za każdym razem w spójnej kolejności, aby transakcje tworzyły dobrze zdefiniowane kolejki i nie blokowały się.
Jednym z obejść, które możesz również zaproponować, jest dodanie logiki ponawiania prób do aplikacji (oczywiście najpierw spróbuj rozwiązać podstawowy problem) w taki sposób, aby w przypadku zakleszczenia aplikacja ponownie uruchomiła te same polecenia.
Sprawdź używane poziomy izolacji, czasami próbujesz je zmienić. Poszukaj poleceń, takich jak SELECT FOR UPDATE i SELECT FOR SHARE, ponieważ generują jawne blokady i oceń, czy są naprawdę potrzebne, czy też możesz pracować ze starszą migawką danych. Jedną z rzeczy, które możesz spróbować, jeśli nie możesz usunąć tych poleceń, jest użycie niższego poziomu izolacji, takiego jak READ COMMITTED.
Oczywiście zawsze dodawaj do swoich tabel dobrze dobrane indeksy. Wtedy twoje zapytania muszą skanować mniej rekordów indeksu, a w konsekwencji ustawić mniej blokad.
Na wyższym poziomie, jako DBA, możesz podjąć pewne środki ostrożności, aby ogólnie zminimalizować blokowanie. Aby wymienić jeden przykład, w tym przypadku PostgreSQL, możesz uniknąć dodawania wartości domyślnej w tym samym poleceniu, w którym dodasz kolumnę. Zmiana tabeli spowoduje naprawdę agresywną blokadę, a ustawienie jej wartości domyślnej faktycznie zaktualizuje istniejące wiersze, które mają wartości null, przez co ta operacja zajmie naprawdę dużo czasu. Więc jeśli podzielisz tę operację na kilka poleceń, dodając kolumnę, dodając wartość domyślną, aktualizując wartości null, zminimalizujesz wpływ blokowania.
Oczywiście istnieje mnóstwo takich wskazówek, które administratorzy baz danych uzyskują z praktyką (równoczesne tworzenie indeksów, oddzielne tworzenie indeksu pk przed dodaniem pk itd.), ale ważne jest, aby nauczyć się i zrozumieć ten „sposób myślenie” i zawsze w celu zminimalizowania wpływu blokowania operacji, które wykonujemy.
Podsumowanie
Mamy nadzieję, że ten blog dostarczył przydatnych informacji na temat impasów w bazie danych i sposobów ich przezwyciężenia. Ponieważ nie ma niezawodnego sposobu na uniknięcie zakleszczeń, wiedza o tym, jak działają, może pomóc Ci je złapać, zanim zaszkodzą instancjom bazy danych. Rozwiązania programowe, takie jak ClusterControl, mogą pomóc w zapewnieniu, że Twoje bazy danych będą zawsze w dobrym stanie. ClusterControl pomógł już setkom przedsiębiorstw – czy Twoje będzie następne? Pobierz bezpłatną wersję próbną ClusterControl już dziś, aby sprawdzić, czy jest ona odpowiednia dla potrzeb Twojej bazy danych.