5.1 Kwestia równoczesności

Zacznijmy od trochę odległej teorii.

Każdy system informacyjny (lub po prostu aplikacja), który tworzą programiści, składa się z kilku typowych bloków, z których każdy zapewnia część niezbędnej funkcjonalności. Na przykład pamięć podręczna służy do zapamiętywania wyniku operacji wymagającej dużych zasobów w celu zapewnienia szybszego odczytu danych przez klienta, narzędzia do przetwarzania strumieniowego umożliwiają wysyłanie komunikatów do innych komponentów w celu przetwarzania asynchronicznego, a narzędzia do przetwarzania wsadowego służą do „ rake” zgromadzone ilości danych z pewną częstotliwością. .

A w prawie każdej aplikacji bazy danych (DB) są w ten czy inny sposób zaangażowane, które zazwyczaj pełnią dwie funkcje: przechowują dane otrzymane od Ciebie, a następnie udostępniają je na żądanie. Rzadko kto myśli o stworzeniu własnej bazy danych, ponieważ gotowych rozwiązań jest już wiele. Ale jak wybrać odpowiedni do swojej aplikacji?

Wyobraźmy sobie więc, że napisałeś aplikację z interfejsem mobilnym, która umożliwia wczytanie wcześniej zapisanej listy zadań wokół domu – czyli odczytanie z bazy danych i uzupełnienie jej o nowe zadania, a także nadanie priorytetu każdemu konkretnemu zadanie - od 1 (najwyższa) do 3 (najniższa). Załóżmy, że z Twojej aplikacji mobilnej korzysta w danym momencie tylko jedna osoba. Ale teraz odważyłeś się powiedzieć swojej mamie o swoim stworzeniu, a teraz stała się drugim stałym użytkownikiem. Co się stanie, jeśli zdecydujesz się w tym samym czasie, dokładnie w tej samej milisekundie, nadać jakiemukolwiek zadaniu – „umyciu okien” – inny priorytet?

Z profesjonalnego punktu widzenia, zapytania do bazy danych twojej i matki można uznać za 2 procesy, które wykonały zapytanie do bazy danych. Proces to jednostka w programie komputerowym, która może działać na jednym lub wielu wątkach. Zwykle proces ma obraz kodu maszynowego, pamięć, kontekst i inne zasoby. Innymi słowy, proces można scharakteryzować jako wykonywanie instrukcji programu na procesorze. Kiedy Twoja aplikacja wysyła żądanie do bazy danych, mówimy o tym, że Twoja baza danych przetwarza żądanie otrzymane przez sieć z jednego procesu. Jeśli w aplikacji pracuje jednocześnie dwóch użytkowników, w dowolnym momencie mogą istnieć dwa procesy.

Gdy jakiś proces wysyła żądanie do bazy danych, znajduje je w określonym stanie. System stanowy to system, który zapamiętuje poprzednie zdarzenia i przechowuje pewne informacje, które nazywa się „stanem”. Zmienna zadeklarowana jako integermoże mieć stan 0, 1, 2 lub powiedzmy 42. Mutex (wzajemne wykluczanie) ma dwa stany: zablokowany lub odblokowany , podobnie jak semafor binarny („wymagany” vs. „zwolniony”) i ogólnie binarny (binarne) typy danych i zmienne, które mogą mieć tylko dwa stany - 1 lub 0.

W oparciu o koncepcję stanu opiera się kilka struktur matematycznych i inżynierskich, takich jak automat skończony — model, który ma jedno wejście i jedno wyjście oraz znajduje się w jednym ze skończonego zbioru stanów w każdym momencie czasu — oraz „stan ” wzorzec projektowy, w którym obiekt zmienia zachowanie w zależności od stanu wewnętrznego (na przykład w zależności od tego, jaka wartość jest przypisana do jednej lub drugiej zmiennej).

Tak więc większość obiektów w świecie maszyn ma pewien stan, który może zmieniać się w czasie: nasz potok, który przetwarza duży pakiet danych, zgłasza błąd i kończy się niepowodzeniem, lub właściwość obiektu Wallet, która przechowuje kwotę pieniędzy pozostawioną w portfelu użytkownika. konto, zmiany po kwitach płacowych.

Przejście („przejście”) z jednego stanu do drugiego — powiedzmy, z trwającego do zakończonego niepowodzeniem — nazywa się operacją. Chyba każdy zna operacje CRUD - create, read, update, deletelub podobne metody HTTP - POST, GET, PUT, DELETE. Ale programiści często nadają innym nazwom operacje w swoim kodzie, bo operacja może być bardziej złożona niż tylko odczytanie określonej wartości z bazy danych – może też sprawdzić dane, a potem nasza operacja, która przybrała formę funkcji, będzie nazwany np. A validate()kto wykonuje te operacje-funkcje? opisane już procesy.

Jeszcze trochę, a zrozumiesz, dlaczego opisuję warunki tak szczegółowo!

Każda operacja - czy to funkcja, czy w systemach rozproszonych wysłanie żądania do innego serwera - ma 2 właściwości: czas wywołania i czas zakończenia (czas zakończenia) , który będzie ściśle większy niż czas wywołania (badacze z Jepsen wychodzić z teoretycznych założeń, że oba te znaczniki czasu będą miały wyimaginowane, w pełni zsynchronizowane, globalnie dostępne zegary).

Wyobraźmy sobie naszą aplikację z listą rzeczy do zrobienia. Wysyłasz żądanie do bazy danych przez interfejs mobilny w 14:00:00.014, a twoja mama w 13:59:59.678(czyli 336 milisekund wcześniej) aktualizowała listę rzeczy do zrobienia przez ten sam interfejs, dodając do niej zmywanie naczyń. Biorąc pod uwagę opóźnienie sieci i ewentualną kolejkę zadań dla Twojej bazy danych, jeśli oprócz Ciebie i Twojej mamy wszyscy znajomi Twojej mamy korzystają również z Twojej aplikacji, baza danych może wykonać żądanie matki po przetworzeniu Twojego. Innymi słowy, istnieje szansa, że ​​dwie Twoje prośby, a także prośby dziewczyn Twojej mamy, zostaną wysłane na te same dane w tym samym czasie (jednocześnie).

Doszliśmy więc do najważniejszego terminu w dziedzinie baz danych i aplikacji rozproszonych – współbieżności. Co dokładnie może oznaczać jednoczesność dwóch operacji? Jeśli dana jest jakaś operacja T1 i jakaś operacja T2, to:

  • T1 można rozpocząć przed czasem rozpoczęcia wykonania T2 i zakończyć między czasem rozpoczęcia i zakończenia T2
  • T2 można rozpocząć przed czasem rozpoczęcia T1 i zakończyć między początkiem a końcem T1
  • T1 można rozpocząć i zakończyć między czasem rozpoczęcia i zakończenia wykonania T1
  • oraz każdy inny scenariusz, w którym T1 i T2 mają wspólny czas wykonania

Oczywiste jest, że w ramach tego wykładu mówimy przede wszystkim o zapytaniach wchodzących do bazy danych io tym, jak system zarządzania bazą danych te zapytania postrzega, ale pojęcie współbieżności jest istotne np. w kontekście systemów operacyjnych. Nie będę zbytnio odbiegał od tematu tego artykułu, ale myślę, że należy wspomnieć, że współbieżność, o której tutaj mówimy, nie jest związana z dylematem współbieżności i współbieżności oraz ich różnicą, o której mowa w kontekście systemów operacyjnych i obliczeń o wysokiej wydajności. Równoległość to jeden ze sposobów osiągnięcia współbieżności w środowisku z wieloma rdzeniami, procesorami lub komputerami. Mówimy o współbieżności w sensie równoczesnego dostępu różnych procesów do wspólnych danych.

A co tak naprawdę może pójść nie tak, czysto teoretycznie?

Podczas pracy na udostępnionych danych mogą wystąpić liczne problemy związane ze współbieżnością, zwaną też „warunkami wyścigu”. Pierwszy problem pojawia się, gdy proces otrzymuje dane, których nie powinien był otrzymać: dane niekompletne, tymczasowe, anulowane lub w inny sposób „niepoprawne”. Drugi problem polega na tym, że proces otrzymuje nieaktualne dane, czyli dane, które nie odpowiadają ostatnio zapisanemu stanowi bazy danych. Powiedzmy, że jakaś aplikacja pobrała pieniądze z konta użytkownika z zerowym saldem, ponieważ baza danych zwróciła aplikacji stan konta, nie biorąc pod uwagę ostatniej wypłaty z niej pieniędzy, która miała miejsce zaledwie kilka milisekund temu. Sytuacja jest taka sobie, prawda?

5.2 Transakcje nas uratowały

W celu rozwiązania takich problemów pojawiło się pojęcie transakcji – pewnej grupy sekwencyjnych operacji (zmian stanów) z bazą danych, która jest logicznie pojedynczą operacją. Znów podam przykład z bankiem – i to nie przez przypadek, bo pojęcie transakcji pojawiło się najwyraźniej właśnie w kontekście pracy z pieniędzmi. Klasycznym przykładem transakcji jest przelew pieniędzy z jednego konta bankowego na drugie: najpierw należy wypłacić kwotę z konta źródłowego, a następnie wpłacić ją na konto docelowe.

Aby transakcja została zrealizowana, aplikacja będzie musiała wykonać kilka czynności w bazie danych: sprawdzenie salda nadawcy, zablokowanie kwoty na koncie nadawcy, dodanie kwoty na konto odbiorcy oraz pobranie kwoty od nadawcy. Będzie kilka wymagań dotyczących takiej transakcji. Np. aplikacja nie może otrzymać nieaktualnej lub błędnej informacji o saldzie – np. jeśli w tym samym czasie transakcja równoległa zakończyła się błędem w połowie, a środki nie zostały pobrane z konta – a nasza aplikacja otrzymała już informację że środki zostały umorzone.

Aby rozwiązać ten problem, odwołano się do takiej właściwości transakcji, jak „izolacja”: nasza transakcja jest wykonywana tak, jakby w tym samym momencie nie były wykonywane żadne inne transakcje. Nasza baza danych wykonuje współbieżne operacje tak, jakby wykonywała je jedna po drugiej, sekwencyjnie - w rzeczywistości najwyższy poziom izolacji nazywa się Strict Serializable . Tak, najwyższy, co oznacza, że ​​jest kilka poziomów.

„Przestań” – mówisz. Wstrzymaj konie, panie.

Przypomnijmy sobie, jak opisałem, że każda operacja ma swój czas wywołania i czas wykonania. Dla wygody możesz rozważyć wywołanie i wykonanie jako 2 akcje. Wtedy posortowaną listę wszystkich akcji wywołań i wykonań można nazwać historią bazy danych. Wtedy poziom izolacji transakcji jest zbiorem historii. Używamy poziomów izolacji, aby określić, które historie są „dobre”. Kiedy mówimy, że historia „łamie serializowalność” lub „nie nadaje się do serializacji”, mamy na myśli, że historia nie należy do zbioru historii możliwych do serializacji.

Aby było jasne, o jakich historiach mówimy, podam przykłady. Na przykład istnieje taki rodzaj historii - czytanie pośrednie . Występuje, gdy transakcja A może odczytać dane z wiersza, który został zmodyfikowany przez inną działającą transakcję B i nie został jeszcze zatwierdzony („niezatwierdzony”) - czyli w rzeczywistości zmiany nie zostały jeszcze ostatecznie zatwierdzone przez transakcję B i może w każdej chwili je anulować. I na przykład przerwany odczyt to tylko nasz przykład z anulowaną transakcją wypłaty

Istnieje kilka możliwych anomalii. Oznacza to, że anomalie to pewnego rodzaju niepożądany stan danych, który może wystąpić podczas konkurencyjnego dostępu do bazy danych. Aby uniknąć pewnych niepożądanych stanów, bazy danych stosują różne poziomy izolacji — to znaczy różne poziomy ochrony danych przed niepożądanymi stanami. Poziomy te (4 sztuki) zostały wymienione w standardzie ANSI SQL-92.

Opis tych poziomów wydaje się niektórym badaczom niejasny i proponują oni własne, bardziej szczegółowe klasyfikacje. Radzę zwrócić uwagę na wspomniany już Jepsen, a także projekt Hermitage, który ma na celu doprecyzowanie, jakie dokładnie poziomy izolacji oferują konkretne DBMS, takie jak MySQL czy PostgreSQL. Jeśli otworzysz pliki z tego repozytorium, możesz zobaczyć, jakiej sekwencji poleceń SQL używają do testowania bazy danych pod kątem pewnych anomalii i możesz zrobić coś podobnego dla interesujących Cię baz danych). Oto jeden przykład z repozytorium, aby Cię zainteresować:

-- Database: MySQL

-- Setup before test
create table test (id int primary key, value int) engine=innodb;
insert into test (id, value) values (1, 10), (2, 20);

-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomaly
set session transaction isolation level read uncommitted; begin; -- T1
set session transaction isolation level read uncommitted; begin; -- T2
update test set value = 101 where id = 1; -- T1
select * from test; -- T2. Shows 1 => 101
update test set value = 11 where id = 1; -- T1
commit; -- T1
select * from test; -- T2. Now shows 1 => 11
commit; -- T2

-- Result: doesn't prevent G1b

Ważne jest, aby zrozumieć, że dla tej samej bazy danych z reguły można wybrać jeden z kilku rodzajów izolacji. Dlaczego nie wybrać najmocniejszej izolacji? Ponieważ, jak wszystko w informatyce, wybrany poziom izolacji powinien odpowiadać kompromisowi, na który jesteśmy gotowi - w tym przypadku kompromisowi w szybkości wykonywania: im wyższy poziom izolacji, tym wolniejsze będą żądania obrobiony. Aby zrozumieć, jakiego poziomu izolacji potrzebujesz, musisz zrozumieć wymagania dla swojej aplikacji, a aby zrozumieć, czy wybrana baza danych oferuje ten poziom, będziesz musiał zajrzeć do dokumentacji - dla większości aplikacji to wystarczy, ale jeśli masz jakieś szczególnie napięte wymagania, lepiej umówić się na test, taki jak robią faceci z projektu Hermitage.

5.3 „I” i inne litery w ACID

Izolacja jest zasadniczo tym, co ludzie mają na myśli, kiedy ogólnie mówią o ACID. I właśnie z tego powodu zacząłem analizę tego akronimu od izolacji i nie poszedłem w kolejności, jak zwykle robią to ci, którzy próbują wyjaśnić to pojęcie. Teraz spójrzmy na pozostałe trzy litery.

Przypomnijmy jeszcze raz nasz przykład z przelewem bankowym. Transakcja przelewu środków z jednego rachunku na drugi obejmuje operację wypłaty z pierwszego konta i operację uzupełnienia na drugim. Jeśli operacja uzupełnienia drugiego konta nie powiodła się, prawdopodobnie nie chcesz, aby wystąpiła operacja wypłaty z pierwszego konta. Innymi słowy, albo transakcja zakończy się całkowitym sukcesem, albo w ogóle nie nastąpi, ale nie może być dokonana tylko w jakiejś części. Ta właściwość nazywa się „atomowość” i jest to „A” w ACID.

Kiedy nasza transakcja jest wykonywana, to, jak każda operacja, przenosi bazę danych z jednego ważnego stanu do drugiego. Niektóre bazy danych oferują tak zwane ograniczenia , czyli reguły, które mają zastosowanie do przechowywanych danych, na przykład dotyczące kluczy podstawowych lub drugorzędnych, indeksów, wartości domyślnych, typów kolumn itp. Dokonując więc transakcji, musimy mieć pewność, że wszystkie te ograniczenia zostaną spełnione.

Ta gwarancja nazywa się „spójność” i litera Cw ACID (nie mylić ze spójnością ze świata aplikacji rozproszonych, o której porozmawiamy później). Podam jasny przykład spójności w sensie ACID: aplikacja dla sklepu internetowego chce dodać orderswiersz do tabeli, a identyfikator z tabeli product_idzostanie wskazany w kolumnie - typowy .productsforeign key

Jeśli produkt, powiedzmy, został usunięty z asortymentu, a zatem z bazy danych, wówczas operacja wstawiania wiersza nie powinna się wydarzyć i otrzymamy błąd. Ta gwarancja, w porównaniu z innymi, jest moim zdaniem trochę naciągana - choćby dlatego, że aktywne korzystanie z ograniczeń z bazy danych oznacza przerzucanie odpowiedzialności za dane (a także częściowe przerzucanie logiki biznesowej, jeśli mówimy o takie ograniczenie jak CHECK ) z aplikacji do bazy danych, która, jak się teraz mówi, właśnie tak jest.

I wreszcie pozostaje D- „odporność” (trwałość). Awaria systemu lub jakakolwiek inna awaria nie powinna prowadzić do utraty wyników transakcji lub zawartości bazy danych. Oznacza to, że jeśli baza danych odpowiedziała, że ​​transakcja się powiodła, oznacza to, że dane zostały zapisane w pamięci nieulotnej - na przykład na dysku twardym. Nawiasem mówiąc, nie oznacza to, że natychmiast zobaczysz dane przy następnym żądaniu odczytu.

Niedawno pracowałem z DynamoDB z AWS (Amazon Web Services) i wysłałem trochę danych do zapisania, a po otrzymaniu odpowiedzi ( HTTP 200OK) czy coś w tym stylu postanowiłem to sprawdzić - i nie widziałem tego dane w bazie danych przez następne 10 sekund. Oznacza to, że DynamoDB zatwierdził moje dane, ale nie wszystkie węzły natychmiast zsynchronizowały się, aby uzyskać najnowszą kopię danych (chociaż mogły znajdować się w pamięci podręcznej). Tutaj ponownie wspięliśmy się na terytorium spójności w kontekście systemów rozproszonych, ale czas, aby o tym mówić, jeszcze nie nadszedł.

Więc teraz wiemy, czym są gwarancje ACID. I nawet wiemy, dlaczego są przydatne. Ale czy naprawdę potrzebujemy ich w każdym zastosowaniu? A jeśli nie, to kiedy dokładnie? Czy wszystkie bazy danych oferują te gwarancje, a jeśli nie, co oferują w zamian?