W poprzednim poście z tej serii pokazano, w jaki sposób instrukcja T-SQL uruchomiona w izolacji zatwierdzonej migawki odczytu (RCSI ) zwykle widzi widok migawki zatwierdzonego stanu bazy danych, tak jak w momencie rozpoczęcia wykonywania instrukcji. To dobry opis tego, jak to działa w przypadku instrukcji odczytujących dane, ale istnieją istotne różnice dla instrukcji działających w ramach RCSI, które modyfikują istniejące wiersze .
Podkreślam modyfikacje istniejących wierszy powyżej, ponieważ poniższe uwagi dotyczą tylko UPDATE
i DELETE
operacje (i odpowiadające akcje MERGE
oświadczenie). Aby było jasne, INSERT
oświadczenia są w szczególności wykluczone z zachowania, które zamierzam opisać, ponieważ wstawki nie modyfikują istniejących dane.
Aktualizuj blokady i wersje wierszy
Pierwsza różnica polega na tym, że instrukcje aktualizacji i usuwania nie odczytują wersji wierszy pod RCSI podczas wyszukiwania wierszy źródłowych do modyfikacji. Aktualizuj i usuwaj instrukcje w ramach RCSI zamiast tego uzyskaj blokady aktualizacji podczas wyszukiwania kwalifikujących się wierszy. Korzystanie z blokad aktualizacji zapewnia, że operacja wyszukiwania znajdzie wiersze do zmodyfikowania przy użyciu najnowszych zatwierdzonych danych .
Bez blokad aktualizacji wyszukiwanie byłoby oparte na prawdopodobnie nieaktualnej wersji zestawu danych (zatwierdzone dane, takie jak w momencie uruchomienia instrukcji modyfikacji danych). Może to przypominać przykład wyzwalacza, który widzieliśmy ostatnio, w którym READCOMMITTEDLOCK
wskazówka została użyta do powrotu z RCSI do implementacji blokowania odczytu popełnionej izolacji. Ta wskazówka była wymagana w tym przykładzie, aby uniknąć opierania ważnej akcji na nieaktualnych informacjach. Zastosowano tutaj ten sam rodzaj rozumowania. Jedną z różnic jest to, że READCOMMITTEDLOCK
wskazówka uzyskuje blokady współdzielone zamiast blokad aktualizacji. Ponadto SQL Server automatycznie uzyskuje blokady aktualizacji, aby chronić modyfikacje danych w ramach RCSI, bez konieczności dodawania wyraźnej wskazówki.
Blokowanie aktualizacji zapewnia również, że instrukcja aktualizacji lub usunięcia blokuje jeśli napotka niekompatybilną blokadę, na przykład blokadę na wyłączność chroniącą modyfikację danych podczas lotu wykonywaną przez inną równoczesną transakcję.
Dodatkową komplikacją jest to, że zmodyfikowane zachowanie ma zastosowanie tylko do tabeli, która jest celem operacji aktualizacji lub usunięcia. Inne stoły w tym samym usuń lub zaktualizuj oświadczenie, w tym dodatkowe odniesienia do tabeli docelowej, nadal używaj wersji wierszy .
Pewne przykłady są prawdopodobnie wymagane, aby te mylące zachowania były nieco jaśniejsze…
Konfiguracja testowa
Poniższy skrypt zapewnia, że wszyscy jesteśmy przygotowani do korzystania z RCSI, tworzy prostą tabelę i dodaje do niej dwa przykładowe wiersze:
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Test ( RowID integer PRIMARY KEY, Data integer NOT NULL ); GO INSERT dbo.Test (RowID, Data) VALUES (1, 1234), (2, 2345);
Następny krok musi być wykonany w oddzielnej sesji . Rozpoczyna transakcję i usuwa oba wiersze z tabeli testowej (wydaje się dziwne, ale to wszystko wkrótce nabierze sensu):
BEGIN TRANSACTION; DELETE dbo.Test WHERE RowID IN (1, 2);
Zwróć uwagę, że transakcja jest celowo pozostawiona otwarta . Utrzymuje to blokady na wyłączność w obu usuwanych wierszach (wraz ze zwykłymi blokadami na wyłączność na stronie zawierającej i samej tabeli), ponieważ poniższe zapytanie może pokazać:
SELECT resource_type, resource_description, resource_associated_entity_id, request_mode, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID;
Test wyboru
Powrót do pierwotnej sesji , pierwszą rzeczą, którą chcę pokazać, jest to, że zwykłe instrukcje select używające RCSI nadal widzą usuwane dwa wiersze. Poniższe zapytanie wybierające używa wersji wierszy do zwrócenia najnowszych zatwierdzonych danych w momencie rozpoczęcia instrukcji:
SELECT * FROM dbo.Test;
Jeśli wydaje się to zaskakujące, pamiętaj, że pokazywanie wierszy jako usuniętych oznaczałoby wyświetlanie niezatwierdzonego widoku danych, co nie jest dozwolone w przypadku odczytu zatwierdzonej izolacji.
Test usuwania
Pomimo sukcesu testu wyboru, próba usunięcia te same wiersze z bieżącej sesji zostaną zablokowane. Możesz sobie wyobrazić, że to blokowanie występuje, gdy operacja próbuje uzyskać wyłączność zamki, ale tak nie jest.
Usuń nie używa wersji wierszy zlokalizować wiersze do usunięcia; zamiast tego próbuje uzyskać blokady aktualizacji. Blokady aktualizacji są niekompatybilne z wyłącznymi blokadami wierszy utrzymywanymi przez sesję z otwartą transakcją, więc zapytanie blokuje:
DELETE dbo.Test WHERE RowID IN (1, 2);
Szacowany plan zapytania dla tej instrukcji pokazuje, że wiersze do usunięcia są identyfikowane przez zwykłą operację wyszukiwania, zanim oddzielny operator wykona faktyczne usunięcie:
Blokady utrzymywane na tym etapie możemy zobaczyć, uruchamiając to samo zapytanie blokujące co poprzednio (z innej sesji), pamiętając o zmianie odwołania SPID na używane przez blokowane zapytanie. Wyniki wyglądają tak:
Nasze zapytanie usuwające jest zablokowane u operatora Clustered Index Seek, który czeka na uzyskanie blokady aktualizacji w celu odczytu dane. Pokazuje to, że lokalizowanie wierszy do usunięcia w ramach RCSI powoduje uzyskanie blokad aktualizacji, a nie odczytywanie potencjalnie nieaktualnych wersjonowanych danych. Pokazuje również, że blokowanie nie jest spowodowane usunięciem części operacji oczekującej na uzyskanie blokady na wyłączność.
Test aktualizacji
Anuluj zablokowane zapytanie i zamiast tego wypróbuj następującą aktualizację:
UPDATE dbo.Test SET Data = Data + 1000 WHERE RowID IN (1, 2);
Szacowany plan wykonania jest podobny do tego widocznego w teście usuwania:
Skalar obliczeniowy służy do określenia wyniku dodania 1000 do bieżącej wartości kolumny Dane w każdym wierszu, która jest odczytywana przez Clustered Index Seek. To stwierdzenie również blokuje po wykonaniu, ze względu na blokadę aktualizacji żądaną przez operację odczytu. Poniższy zrzut ekranu pokazuje blokady utrzymywane, gdy zapytanie blokuje:
Tak jak poprzednio, zapytanie jest blokowane podczas wyszukiwania, czekając na zwolnienie niekompatybilnej blokady na wyłączność, aby można było uzyskać blokadę aktualizacji.
Test wstawiania
Następny test zawiera instrukcję, która wstawia nowy wiersz do naszej tabeli testowej, używając wartości kolumny Dane z istniejącego wiersza z ID 1 w tabeli. Przypomnij sobie, że ten wiersz jest nadal zablokowany wyłącznie przez sesję z otwartą transakcją:
INSERT dbo.Test (RowID, Data) SELECT 3, Data FROM dbo.Test WHERE RowID = 1;
Plan wykonania jest ponownie podobny do poprzednich testów:
Tym razem zapytanie nie jest zablokowane . Pokazuje to, że blokady aktualizacji nie zostały nabyte podczas odczytu dane do wkładki. To zapytanie zamiast tego używało wersji wiersza, aby uzyskać wartość kolumny danych dla nowo wstawionego wiersza. Nie udało się uzyskać blokad aktualizacji, ponieważ w tym stwierdzeniu nie znaleziono żadnych wierszy do modyfikowania , tylko odczytuje dane do użycia we wstawce.
Możemy zobaczyć ten nowy wiersz w tabeli za pomocą zapytania testowego wyboru z poprzedniego:
Pamiętaj, że jesteśmy w stanie zaktualizować i usunąć nowy wiersz (co będzie wymagało aktualizacji blokad), ponieważ nie ma konfliktu blokady na wyłączność. Sesja z otwartą transakcją ma wyłączne blokady tylko w wierszach 1 i 2:
-- Update the new row UPDATE dbo.Test SET Data = 9999 WHERE RowID = 3; -- Show the data SELECT * FROM dbo.Test; -- Delete the new row DELETE dbo.Test WHERE RowID = 3;
Ten test potwierdza, że instrukcje insert nie uzyskują blokad aktualizacji podczas czytania , ponieważ w przeciwieństwie do aktualizacji i usuwania nie modyfikują istniejący wiersz. Część do czytania wkładki instrukcja używa normalnego zachowania wersji RCSI.
Test wielu referencji
Wspomniałem wcześniej, że tylko pojedyncze odwołanie do tabeli używane do lokalizowania wierszy do modyfikacji uzyskuje blokady aktualizacji; inne tabele w tej samej instrukcji aktualizacji lub usunięcia nadal odczytują wersje wierszy. W szczególnym przypadku tej ogólnej zasady oświadczenie o modyfikacji danych z wieloma odniesieniami do tej samej tabeli stosuje blokady aktualizacji tylko w jednej instancji służy do lokalizowania wierszy do modyfikacji. Ten końcowy test ilustruje to bardziej złożone zachowanie, krok po kroku.
Pierwszą rzeczą, jakiej będziemy potrzebować, jest nowy trzeci wiersz dla naszej tabeli testowej, tym razem z zerem w kolumnie Dane:
INSERT dbo.Test (RowID, Data) VALUES (3, 0);
Zgodnie z oczekiwaniami ta wstawka przebiega bez blokowania, czego wynikiem jest tabela, która wygląda tak:
Pamiętaj, że druga sesja nadal jest na wyłączność w tym momencie blokuje się w rzędach 1 i 2. Jeśli zajdzie taka potrzeba, możemy nabyć zamki w trzecim rzędzie. Poniższe zapytanie jest tym, którego użyjemy do pokazania zachowania z wieloma odniesieniami do tabeli docelowej:
-- Multi-reference update test UPDATE WriteRef SET Data = ReadRef.Data * 2 OUTPUT ReadRef.RowID, ReadRef.Data, INSERTED.RowID AS UpdatedRowID, INSERTED.Data AS NewDataValue FROM dbo.Test AS ReadRef JOIN dbo.Test AS WriteRef ON WriteRef.RowID = ReadRef.RowID + 2 WHERE ReadRef.RowID = 1;
Jest to bardziej złożone zapytanie, ale jego obsługa jest stosunkowo prosta. Istnieją dwa odniesienia do tabeli testowej, jedno, które nazwałem aliasem ReadRef, a drugie jako WriteRef. Chodzi o to, aby czytać z wiersza 1 (przy użyciu wersji wiersza) przez ReadRef i do aktualizacji trzeci wiersz (który będzie wymagał blokady aktualizacji) za pomocą WriteRef.
Zapytanie określa wiersz 1 jawnie w klauzuli WHERE dla odwołania do tabeli odczytu. Łączy się z zapisem odniesienia do tej samej tabeli dodając 2 do tego RowID (w ten sposób identyfikując wiersz 3). Instrukcja aktualizacji używa również klauzuli wyjściowej, aby zwrócić zestaw wyników pokazujący wartości odczytane z tabeli źródłowej i wynikowe zmiany wprowadzone w wierszu 3.
Szacowany plan zapytań dla tej instrukcji jest następujący:
Właściwości wyszukiwania oznaczone jako (1) pokaż, że to wyszukiwanie znajduje się w ReadRef alias, odczytywanie danych z wiersza z RowID 1:
Ta operacja wyszukiwania nie lokalizuje wiersza, który zostanie zaktualizowany, więc blokady aktualizacji nie zajęty; odczyt jest wykonywany przy użyciu danych wersjonowanych. Odczyt nie jest blokowany przez wyłączne blokady utrzymywane przez drugą sesję.
Skalar obliczeniowy oznaczony (2) definiuje wyrażenie oznaczone etykietą 1004, które oblicza zaktualizowaną wartość kolumny danych. Wyrażenie 1009 oblicza identyfikator wiersza do aktualizacji (1 + 2 =identyfikator wiersza 3):
Drugie wyszukiwanie to odwołanie do tej samej tabeli (3). To wyszukiwanie lokalizuje wiersz, który zostanie zaktualizowany (wiersz 3) za pomocą wyrażenia 1009:
Ponieważ to wyszukiwanie lokalizuje wiersz do zmiany, blokada aktualizacji jest pobierana zamiast używania wersji wierszowych. Nie ma sprzecznej blokady na wyłączność w wierszu o identyfikatorze 3, więc żądanie blokady jest udzielane natychmiast.
Ostatni wyróżniony operator (4) to sama operacja aktualizacji. Blokada aktualizacji w wierszu 3 została uaktualniona do wyłączności zablokować w tym momencie, tuż przed faktycznym wykonaniem modyfikacji. Ten operator zwraca również dane określone w klauzuli output oświadczenia o aktualizacji:
Wynik instrukcji aktualizacji (generowany przez klauzulę output) jest pokazany poniżej:
Ostateczny stan tabeli jest taki, jak pokazano poniżej:
Możemy potwierdzić blokady podjęte podczas wykonywania za pomocą śladu Profiler:
To pokazuje, że tylko jedna aktualizacja blokada klucza rzędu jest nabyta. Gdy ten wiersz dotrze do operatora aktualizacji, blokada zostanie przekonwertowana na wyłączną Zamek. Na końcu oświadczenia blokada zostaje zwolniona.
Możesz zobaczyć na podstawie danych wyjściowych śledzenia, że wartość skrótu blokady dla wiersza zablokowanego przez aktualizację to (98ec012aa510) w mojej testowej bazie danych. Poniższe zapytanie pokazuje, że ten skrót blokady jest rzeczywiście powiązany z RowID 3 w indeksie klastrowym:
SELECT RowID, %%LockRes%% FROM dbo.Test;
Zwróć uwagę, że blokady aktualizacji zastosowane w tych przykładach są krótsze niż blokady aktualizacji, jeśli określimy UPDLOCK
wskazówka. Te wewnętrzne blokady aktualizacji są zwalniane na końcu instrukcji, podczas gdy UPDLOCK
blokady są utrzymywane do końca transakcji.
Na tym kończy się demonstracja przypadków, w których RCSI uzyskuje blokady aktualizacji, aby odczytać bieżące zatwierdzone dane zamiast używać wersji wierszy.
Blokady współdzielone i klucze w ramach RCSI
Istnieje wiele innych scenariuszy, w których aparat bazy danych może nadal uzyskiwać blokady w ramach RCSI. Wszystkie te sytuacje odnoszą się do potrzeby zachowania poprawności, która byłaby zagrożona przez poleganie na potencjalnie nieaktualnych wersjonowanych danych.
Współdzielone blokady brane do walidacji klucza obcego
W przypadku dwóch tabel w prostej relacji klucza obcego aparat bazy danych musi podjąć kroki w celu zapewnienia, że ograniczenia nie zostaną naruszone, polegając na potencjalnie nieaktualnych odczytach w wersji. Obecna implementacja robi to, przełączając się na blokowanie odczytu zatwierdzonego podczas uzyskiwania dostępu do danych w ramach automatycznego sprawdzania klucza obcego.
Przyjmowanie współdzielonych blokad zapewnia, że kontrola integralności odczytuje najnowsze zatwierdzone dane (nie starą wersję) lub bloki z powodu równoczesnej modyfikacji podczas lotu. Przełączenie na blokowanie odczytu zatwierdzonego dotyczy tylko konkretnej metody dostępu używanej do sprawdzania danych klucza obcego; inny dostęp do danych w tym samym wyciągu nadal korzysta z wersji wierszy.
To zachowanie dotyczy tylko instrukcji zmieniających dane, w przypadku których zmiana bezpośrednio wpływa na relację klucza obcego. W przypadku modyfikacji tabeli, do której się odwołuje (nadrzędnej), oznacza to aktualizacje, które wpływają na wartość, do której się odwołuje (chyba że jest ustawiona na NULL
) i wszystkie usunięcia. W przypadku tabeli referencyjnej (podrzędnej) oznacza to wszystkie wstawienia i aktualizacje (ponownie, chyba że odwołanie do klucza to NULL
). Te same uwagi dotyczą efektów składowych MERGE
.
Przykładowy plan wykonania pokazujący wyszukiwanie klucza obcego, który pobiera blokady współdzielone, jest pokazany poniżej:
Możliwość serializacji w celu kaskadowania kluczy obcych
Tam, gdzie relacja klucza obcego ma akcję kaskadową, poprawność wymaga lokalnej eskalacji do semantyki izolacji możliwej do serializacji. Oznacza to, że zobaczysz blokady zakresu kluczy, które zostały zastosowane do kaskadowego działania referencyjnego. Podobnie jak w przypadku blokad aktualizacji, które widzieliśmy wcześniej, te blokady zakresu kluczy dotyczą instrukcji, a nie transakcji. Przykładowy plan wykonania pokazujący, gdzie wewnętrzne serializowane blokady są pobierane w ramach RCSI, pokazano poniżej:
Inne scenariusze
Istnieje wiele innych konkretnych przypadków, w których silnik automatycznie wydłuża żywotność blokad lub lokalnie eskaluje poziom izolacji, aby zapewnić poprawność. Obejmują one semantykę serializowaną używaną podczas obsługi powiązanego widoku indeksowanego lub podczas obsługi indeksu, który ma IGNORE_DUP_KEY
zestaw opcji.
Komunikat na wynos jest taki, że RCSI zmniejsza ilość blokowania, ale nie zawsze może go całkowicie wyeliminować.
Następnym razem
Następny post z tej serii dotyczy poziomu izolacji migawki.
[ Zobacz indeks dla całej serii ]