Problemy ze współbieżnością są trudne w taki sam sposób, w jaki trudne jest programowanie wielowątkowe. O ile nie zostanie użyta izolacja serializowana, może być trudno zakodować transakcje T-SQL, które zawsze będą działać poprawnie, gdy inni użytkownicy w tym samym czasie dokonują zmian w bazie danych.
Potencjalne problemy mogą być nietrywialne, nawet jeśli dana „transakcja” jest prostym pojedynczym SELECT
oświadczenie. W przypadku złożonych transakcji zawierających wiele instrukcji, które odczytują i zapisują dane, możliwość wystąpienia nieoczekiwanych wyników i błędów przy wysokiej współbieżności może szybko stać się przytłaczająca. Próba rozwiązania subtelnych i trudnych do odtworzenia problemów ze współbieżnością poprzez zastosowanie losowych wskazówek dotyczących blokowania lub innych metod prób i błędów może być niezwykle frustrującym doświadczeniem.
Pod wieloma względami poziom izolacji migawki wydaje się idealnym rozwiązaniem tych problemów ze współbieżnością. Podstawową ideą jest to, że każda transakcja migawki zachowuje się tak, jakby była wykonywana na własnej prywatnej kopii zatwierdzonego stanu bazy danych, pobranej w momencie rozpoczęcia transakcji. Zapewnienie całej transakcji niezmiennego widoku zatwierdzonych danych oczywiście gwarantuje spójne wyniki dla operacji tylko do odczytu, ale co z transakcjami, które zmieniają dane?
Izolacja migawki obsługuje zmiany danych optymistycznie, zakładając, że konflikty między współbieżnymi osobami piszącymi będą stosunkowo rzadkie. W przypadku wystąpienia konfliktu zapisu, pierwszy zatwierdzający wygrywa, a przegrywająca transakcja jest wycofywana. Jest to oczywiście niefortunne w przypadku wycofania transakcji, ale jeśli jest to wystarczająco rzadkie, korzyści z izolacji migawki mogą z łatwością przewyższyć koszty sporadycznych awarii i ponownych prób.
Stosunkowo prosta i przejrzysta semantyka izolacji migawki (w porównaniu z alternatywami) może być znaczącą zaletą, szczególnie dla osób, które nie pracują wyłącznie w świecie baz danych i dlatego nie znają dobrze różnych poziomów izolacji. Nawet dla doświadczonych specjalistów od baz danych stosunkowo „intuicyjny” poziom izolacji może być mile widzianą ulgą.
Oczywiście rzadko wszystko jest tak proste, jak się wydaje, a izolacja migawek nie jest wyjątkiem. Oficjalna dokumentacja całkiem dobrze opisuje główne zalety i wady izolacji migawek, więc większość tego artykułu koncentruje się na zbadaniu niektórych mniej znanych i zaskakujących problemów, które możesz napotkać. Najpierw jednak przyjrzyj się logicznym właściwościom tego poziomu izolacji:
Właściwości ACID i izolacja migawek
Izolacja migawki nie jest jednym z poziomów izolacji zdefiniowanych w standardzie SQL, ale nadal często jest porównywana przy użyciu zdefiniowanych tam „zjawisk współbieżności”. Na przykład poniższa tabela porównawcza została odtworzona z artykułu technicznego dotyczącego SQL Server „SQL Server 2005 Row Versioning-Based Transaction Isolation” autorstwa Kimberly L. Tripp i Neal Graves:
Zapewniając widok z punktu w czasie zadeklarowanych danych , izolacja migawki zapewnia ochronę przed wszystkimi trzema przedstawionymi tam zjawiskami współbieżności. Brudne odczyty są blokowane, ponieważ widoczne są tylko zatwierdzone dane, a statyczny charakter migawki zapobiega napotkaniu zarówno niepowtarzalnych odczytów, jak i fantomów.
Jednak to porównanie (w szczególności wyróżniona sekcja) pokazuje tylko, że poziomy migawki i serializacji izolacji zapobiegają tym samym trzem określonym zjawiskom. Nie oznacza to, że są one równoważne pod każdym względem. Co ważne, standard SQL-92 nie definiuje serializowalnej izolacji wyłącznie w odniesieniu do tych trzech zjawisk. Sekcja 4.28 normy zawiera pełną definicję:
Gwarantuje się, że wykonanie równoczesnych transakcji SQL na poziomie izolacji SERIALIZABLE będzie możliwe do serializacji. Wykonanie możliwe do serializacji jest zdefiniowane jako wykonanie operacji równoczesnego wykonywania transakcji SQL, które daje taki sam efekt, jak niektóre seryjne wykonanie tych samych transakcji SQL. Wykonywanie szeregowe to takie, w którym każda transakcja SQL jest wykonywana do końca przed rozpoczęciem następnej transakcji SQL.
Często pomija się zakres i wagę dorozumianych gwarancji. Mówiąc prostym językiem:
Każda możliwa do serializacji transakcja, która jest wykonywana poprawnie, gdy jest uruchamiana samodzielnie, będzie nadal wykonywana poprawnie z dowolną kombinacją współbieżnych transakcji lub zostanie wycofana z komunikatem o błędzie (zazwyczaj zakleszczenie w implementacji SQL Server).
Poziomy izolacji, których nie można serializować, w tym izolacja migawki, nie zapewniają tak samo silnych gwarancji poprawności.
Nieaktualne dane
Izolacja migawki wydaje się niemal uwodzicielsko prosta. Odczyty zawsze pochodzą z zatwierdzonych danych od jednego punktu w czasie, a konflikty zapisu są automatycznie wykrywane i obsługiwane. Dlaczego nie jest to idealne rozwiązanie dla wszystkich problemów związanych ze współbieżnością?
Jednym z potencjalnych problemów jest to, że odczyty migawek niekoniecznie odzwierciedlają bieżący zatwierdzony stan bazy danych. Transakcja migawki całkowicie ignoruje wszelkie zatwierdzone zmiany wprowadzone przez inne współbieżne transakcje po rozpoczęciu transakcji migawki. Innym sposobem na określenie tego jest powiedzenie, że transakcja migawki widzi nieaktualne, nieaktualne dane. Chociaż to zachowanie może być dokładnie tym, co jest potrzebne do wygenerowania dokładnego raportu w określonym momencie, może nie być tak odpowiednie w innych okolicznościach (na przykład, gdy jest używane do wymuszania reguły w regule).
Pochylenie zapisu
Izolacja migawek jest również podatna na pokrewne zjawisko znane jako pochylenie zapisu. Czytanie przestarzałych danych odgrywa w tym pewną rolę, ale ten problem pomaga również wyjaśnić, co robi, a czego nie robi „wykrywanie konfliktu zapisu” migawki.
Pochylenie zapisu występuje, gdy dwie współbieżne transakcje odczytują dane, które modyfikuje druga transakcja. Nie występuje konflikt zapisu, ponieważ dwie transakcje modyfikują różne wiersze. Żadna z transakcji nie widzi zmian wprowadzonych przez drugą, ponieważ obie są odczytywane z punktu w czasie przed wprowadzeniem tych zmian.
Klasycznym przykładem skośnego zapisu jest problem białego i czarnego marmuru, ale chcę pokazać inny prosty przykład tutaj:
-- Create two empty tables CREATE TABLE A (x integer NOT NULL); CREATE TABLE B (x integer NOT NULL); -- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; INSERT A (x) SELECT COUNT_BIG(*) FROM B; -- Connection 2 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; INSERT B (x) SELECT COUNT_BIG(*) FROM A; COMMIT TRANSACTION; -- Connection 1 COMMIT TRANSACTION;
W przypadku izolacji migawki obie tabele w tym skrypcie kończą z jednym wierszem zawierającym wartość zerową. Jest to wynik poprawny, ale nie można go serializować:nie odpowiada żadnej możliwej kolejności wykonania transakcji seryjnych. W każdym naprawdę szeregowym harmonogramie jedna transakcja musi zakończyć się przed rozpoczęciem drugiej, więc druga transakcja liczy wiersz wstawiony przez pierwszą. Może to brzmieć jak kwestia techniczna, ale pamiętaj, że potężne gwarancje serializacji mają zastosowanie tylko wtedy, gdy transakcje są naprawdę możliwe do serializacji.
Subtelność wykrywania konfliktów
Konflikt zapisu migawki występuje, gdy transakcja migawki próbuje zmodyfikować wiersz, który został zmodyfikowany przez inną transakcję, która została zatwierdzona po rozpoczęciu transakcji migawki. Są tu dwie subtelności:
- Transakcje nie muszą się zmieniać dowolne wartości danych; i
- Transakcje nie muszą modyfikować żadnych wspólnych kolumn .
Poniższy skrypt demonstruje oba punkty:
-- Test table CREATE TABLE dbo.Conflict ( ID1 integer UNIQUE, Value1 integer NOT NULL, ID2 integer UNIQUE, Value2 integer NOT NULL ); -- Insert one row INSERT dbo.Conflict (ID1, ID2, Value1, Value2) VALUES (1, 1, 1, 1); -- Connection 1 BEGIN TRANSACTION; UPDATE dbo.Conflict SET Value1 = 1 WHERE ID1 = 1; -- Connection 2 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; UPDATE dbo.Conflict SET Value2 = 1 WHERE ID2 = 1; -- Connection 1 COMMIT TRANSACTION;
Zwróć uwagę na następujące:
- Każda transakcja lokalizuje ten sam wiersz przy użyciu innego indeksu
- Żadna aktualizacja nie powoduje zmiany już zapisanych danych
- Dwie transakcje „aktualizują” różne kolumny w wierszu.
Mimo wszystko, gdy pierwsza transakcja zostanie zatwierdzona, druga transakcja kończy się błędem konfliktu aktualizacji:
Podsumowanie:Wykrywanie konfliktów zawsze działa na poziomie całego wiersza, a „aktualizacja” nie musi faktycznie zmieniać żadnych danych. (Jeśli się zastanawiasz, zmiany w danych LOB lub SLOB poza wierszem również liczą się jako zmiany w wierszu w celu wykrywania konfliktów).
Problem z kluczem obcym
Wykrywanie konfliktów dotyczy również wiersza nadrzędnego w relacji klucza obcego. Podczas modyfikowania wiersza podrzędnego w izolacji migawki zmiana wiersza nadrzędnego w innej transakcji może wywołać konflikt. Tak jak poprzednio, ta logika dotyczy całego wiersza nadrzędnego – aktualizacja nadrzędna nie musi wpływać na samą kolumnę klucza obcego. Każda operacja na tabeli podrzędnej, która wymaga automatycznego sprawdzenia klucza obcego w planie wykonania, może spowodować nieoczekiwany konflikt.
Aby to zademonstrować, najpierw utwórz następujące tabele i przykładowe dane:
CREATE TABLE dbo.Dummy ( x integer NULL ); CREATE TABLE dbo.Parent ( ParentID integer PRIMARY KEY, ParentValue integer NOT NULL ); CREATE TABLE dbo.Child ( ChildID integer PRIMARY KEY, ChildValue integer NOT NULL, ParentID integer NULL FOREIGN KEY REFERENCES dbo.Parent ); INSERT dbo.Parent (ParentID, ParentValue) VALUES (1, 1); INSERT dbo.Child (ChildID, ChildValue, ParentID) VALUES (1, 1, 1);
Teraz wykonaj następujące czynności z dwóch oddzielnych połączeń, jak wskazano w komentarzach:
-- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT COUNT_BIG(*) FROM dbo.Dummy; -- Connection 2 (any isolation level) UPDATE dbo.Parent SET ParentValue = 1 WHERE ParentID = 1; -- Connection 1 UPDATE dbo.Child SET ParentID = NULL WHERE ChildID = 1; UPDATE dbo.Child SET ParentID = 1 WHERE ChildID = 1;
Odczyt z fikcyjnej tabeli jest tam, aby zapewnić, że transakcja migawki została oficjalnie rozpoczęta. Wystawienie BEGIN TRANSACTION
nie wystarczy, aby to zrobić; musimy wykonać jakiś rodzaj dostępu do danych w tabeli użytkowników.
Pierwsza aktualizacja tabeli Child nie powoduje konfliktu, ponieważ ustawienie kolumny odniesienia na NULL
nie wymaga sprawdzania tabeli nadrzędnej w planie wykonania (nie ma czego sprawdzać). Procesor zapytań nie dotyka wiersza nadrzędnego w planie wykonania, więc nie powstaje konflikt.
Druga aktualizacja tabeli Child powoduje konflikt, ponieważ sprawdzanie klucza obcego jest wykonywane automatycznie. Gdy procesor zapytań uzyskuje dostęp do wiersza nadrzędnego, jest on również sprawdzany pod kątem konfliktu aktualizacji. W tym przypadku zgłaszany jest błąd, ponieważ w odniesieniu do wiersza nadrzędnego wystąpiła zatwierdzona modyfikacja po rozpoczęciu transakcji migawki. Zauważ, że modyfikacja tabeli nadrzędnej nie wpłynęła na samą kolumnę klucza obcego.
Nieoczekiwany konflikt może również wystąpić, jeśli zmiana w tabeli Child odwołuje się do wiersza nadrzędnego, który został utworzony przez równoczesną transakcję (i ta transakcja zatwierdzona po rozpoczęciu transakcji migawki).
Podsumowanie:plan zapytania, który obejmuje automatyczne sprawdzanie klucza obcego, może generować błąd konfliktu, jeśli w odwoływanym wierszu wystąpiły jakiekolwiek modyfikacje (w tym utworzenie!) od momentu rozpoczęcia transakcji migawki.
Problem z obcinaniem tabeli
Transakcja migawkowa zakończy się niepowodzeniem z błędem, jeśli jakakolwiek tabela, do której uzyskuje dostęp, zostanie obcięta od momentu rozpoczęcia transakcji. Ma to zastosowanie nawet wtedy, gdy obcięta tabela nie zawiera wierszy na początku, jak pokazuje poniższy skrypt:
CREATE TABLE dbo.AccessMe ( x integer NULL ); CREATE TABLE dbo.TruncateMe ( x integer NULL ); -- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT COUNT_BIG(*) FROM dbo.AccessMe; -- Connection 2 TRUNCATE TABLE dbo.TruncateMe; -- Connection 1 SELECT COUNT_BIG(*) FROM dbo.TruncateMe;
Ostateczny WYBÓR kończy się niepowodzeniem z błędem:
To kolejny subtelny efekt uboczny, który należy sprawdzić przed włączeniem izolacji migawki w istniejącej bazie danych.
Następnym razem
Następny (i ostatni) post z tej serii będzie mówił o przeczytanym, niezatwierdzonym poziomie izolacji (czule nazywanym „nolock”).
[ Zobacz indeks dla całej serii ]