TL; DR: Poniższe pytanie sprowadza się do: Podczas wstawiania wiersza istnieje okno możliwości między wygenerowaniem nowej Identity
wartości a zablokowaniem odpowiedniego klucza wiersza w indeksie klastrowym, w którym zewnętrzny obserwator mógłby zobaczyć nowszą Identity
wartość wprowadzona przez równoległą transakcję? (W SQL Server.)
Wersja szczegółowa
Mam tabelę programu SQL Server z Identity
kolumną o nazwie CheckpointSequence
, która jest kluczem indeksu klastrowanego tabeli (który ma również szereg dodatkowych indeksów nieklastrowanych). Wiersze są wstawiane do tabeli przez kilka równoległych procesów i wątków (na poziomie izolacji READ COMMITTED
i bez IDENTITY_INSERT
). Jednocześnie istnieją procesy okresowo odczytujące wiersze z indeksu klastrowego, uporządkowane według tej CheckpointSequence
kolumny (również na poziomie izolacji READ COMMITTED
, z READ COMMITTED SNAPSHOT
wyłączoną opcją).
Obecnie polegam na tym, że procesy odczytu nigdy nie mogą „pominąć” punktu kontrolnego. Moje pytanie brzmi: czy mogę polegać na tej właściwości? A jeśli nie, co mogę zrobić, aby było to prawdą?
Przykład: po wstawieniu wierszy o wartościach tożsamości 1, 2, 3, 4 i 5 czytelnik nie może zobaczyć wiersza o wartości 5 przed zobaczeniem wiersza o wartości 4. Testy pokazują, że zapytanie zawierające ORDER BY CheckpointSequence
klauzulę ( i aWHERE CheckpointSequence > -1
klauzula), niezawodnie blokuje za każdym razem, gdy wiersz 4 ma zostać odczytany, ale jeszcze nie zatwierdzony, nawet jeśli wiersz 5 został już zatwierdzony.
Uważam, że przynajmniej teoretycznie mogą istnieć warunki rasowe, które mogą spowodować załamanie tego założenia. Niestety, dokumentacja Identity
nie mówi wiele o tym, jak Identity
działa w kontekście wielu równoczesnych transakcji, mówi tylko: „Każda nowa wartość jest generowana na podstawie bieżącego ziarna i przyrostu”. i „Każda nowa wartość dla określonej transakcji różni się od innych równoczesnych transakcji w tabeli”. ( MSDN )
Moje rozumowanie jest takie, że musi działać jakoś tak:
- Transakcja jest uruchamiana (jawnie lub niejawnie).
- Generowana jest wartość tożsamości (X).
- Odpowiednia blokada wiersza jest pobierana na indeks klastrowany na podstawie wartości tożsamości (chyba że uruchomi się eskalacja blokady, w którym to przypadku cała tabela jest blokowana).
- Wiersz został wstawiony.
- Transakcja zostaje zatwierdzona (być może sporo czasu później), więc blokada jest ponownie usuwana.
Myślę, że pomiędzy krokiem 2 a 3 jest bardzo małe okno, w którym
- równoczesna sesja może wygenerować następną wartość tożsamości (X + 1) i wykonać wszystkie pozostałe kroki,
- w ten sposób pozwalając czytelnikowi przyjść dokładnie w tym momencie, aby odczytać wartość X + 1, pomijając wartość X.
Oczywiście prawdopodobieństwo tego wydaje się bardzo niskie; ale nadal - może się zdarzyć. A może to?
(Jeśli interesuje Cię kontekst: jest to implementacja SQL Persistence Engine NEventStore. NEventStore implementuje magazyn zdarzeń tylko do dołączania, w którym każde zdarzenie otrzymuje nowy, rosnący numer sekwencji kontrolnej. Klienci odczytują zdarzenia z magazynu zdarzeń uporządkowane według punktu kontrolnego aby wykonać wszelkiego rodzaju obliczenia. Po przetworzeniu zdarzenia z punktem kontrolnym X klienci biorą pod uwagę tylko „nowsze” zdarzenia, tj. zdarzenia z punktem kontrolnym X + 1 i wyższym. Dlatego ważne jest, aby zdarzenia nigdy nie były pomijane, ponieważ nigdy więcej nie będą brane pod uwagę. Obecnie próbuję ustalić, czy Identity
implementacja punktu kontrolnego na podstawie tego warunku spełnia ten wymóg. Są to dokładnie używane instrukcje SQL : Schemat , zapytanie Writer ,Zapytanie czytelnika ).
Jeśli mam rację i może się zdarzyć sytuacja opisana powyżej, widzę tylko dwie opcje radzenia sobie z nimi, obie są niezadowalające:
- Gdy zobaczysz wartość sekwencji punktu kontrolnego X + 1 przed zobaczeniem X, odrzuć X + 1 i spróbuj ponownie później. Ponieważ jednak
Identity
może oczywiście tworzyć luki (np. Przy wycofywaniu transakcji), X może nigdy nie nadejść. - To samo podejście, ale zaakceptuj przerwę po n milisekundach. Jaką wartość n należy jednak przyjąć?
Jakieś lepsze pomysły?
źródło
Odpowiedzi:
Tak.
Alokacja wartości tożsamości jest niezależna od zawierającym transakcji użytkownika . Jest to jeden z powodów, dla których wartości tożsamości są konsumowane, nawet jeśli transakcja zostanie wycofana. Sama operacja przyrostowa jest chroniona przez zatrzask, aby zapobiec uszkodzeniu, ale taki jest zakres zabezpieczeń.
W szczególnych okolicznościach implementacji alokacja tożsamości (wezwanie do
CMEDSeqGen::GenerateNewValue
) jest wykonywana przed aktywacją transakcji użytkownika dla wstawki (a więc przed podjęciem jakichkolwiek blokad).Uruchamiając jednocześnie dwie wstawki z dołączonym debugerem, aby umożliwić mi zawieszenie jednego wątku tuż po zwiększeniu i przydzieleniu wartości tożsamości, byłem w stanie odtworzyć scenariusz, w którym:
Po kroku 3 zapytanie wykorzystujące numer wiersza w ramach zablokowanego odczytu zatwierdzonego zwróciło:
W Twojej implementacji spowodowałoby to niepoprawne pominięcie Checkpoint ID 3.
Okno niewłaściwej szansy jest stosunkowo małe, ale istnieje. Aby dać bardziej realistyczny scenariusz niż dołączenie debuggera: Wykonujący wątek zapytania może dać harmonogram po kroku 1 powyżej. Umożliwia to drugiemu wątkowi przydzielenie wartości tożsamości, wstawienie i zatwierdzenie, zanim oryginalny wątek wznowi wykonywanie jego wstawiania.
Dla jasności nie ma żadnych blokad ani innych obiektów synchronizujących chroniących wartość tożsamości po jej przydzieleniu i przed użyciem. Na przykład po kroku 1 powyżej w transakcji współbieżnej można zobaczyć nową wartość tożsamości za pomocą funkcji T-SQL, tak jak
IDENT_CURRENT
zanim wiersz istnieje w tabeli (nawet niezaangażowany).Zasadniczo nie ma więcej gwarancji dotyczących wartości tożsamości niż udokumentowane :
To jest naprawdę to.
Jeśli wymagane jest ścisłe transakcyjne przetwarzanie FIFO, prawdopodobnie nie masz innego wyboru niż ręczna serializacja. Jeśli aplikacja ma mniejsze wymagania, masz więcej opcji. Pytanie nie jest w 100% jasne w tym względzie. Niemniej jednak możesz znaleźć przydatne informacje w artykule Remusa Rusanu Używanie tabel jako kolejek .
źródło
Ponieważ Paul White odpowiedział absolutnie poprawnie, istnieje możliwość tymczasowego „pominięcia” wierszy tożsamości. Oto tylko mały fragment kodu do odtworzenia tego przypadku na własny użytek.
Utwórz bazę danych i tabelę testową:
Wykonuj współbieżne operacje wstawiania i wybierania w tej tabeli w programie konsoli C #:
Ta konsola wypisuje wiersz dla każdego przypadku, gdy jeden z czytanych wątków „pomija” wpis.
źródło
IDENTITY
powstałyby luki (takie jak wycofanie transakcji), drukowane linie rzeczywiście pokazują wartości „pominięte” (a przynajmniej tak się stało, gdy uruchomiłem i sprawdziłem to na moim komputerze). Bardzo ładna próbka repro!Najlepiej nie oczekiwać, że tożsamość będzie następować po sobie, ponieważ istnieje wiele scenariuszy, które mogą pozostawić luki. Lepiej jest traktować tożsamość jak liczbę abstrakcyjną i nie przypisywać do niej żadnego znaczenia biznesowego.
Zasadniczo luki mogą się zdarzyć, jeśli cofniesz operacje INSERT (lub jawnie usuniesz wiersze), a duplikaty mogą wystąpić, jeśli ustawisz właściwość tabeli IDENTITY_INSERT na ON.
Luki mogą wystąpić, gdy:
Właściwość tożsamości w kolumnie nigdy nie gwarantowała:
• wyjątkowość
• Kolejne wartości w ramach transakcji. Jeśli wartości muszą być następujące po sobie, transakcja powinna korzystać z wyłącznej blokady tabeli lub poziomu izolowania SERIALIZABLE.
• Kolejne wartości po ponownym uruchomieniu serwera.
• Ponowne wykorzystanie wartości.
Jeśli z tego powodu nie możesz użyć wartości tożsamości, utwórz osobną tabelę z aktualną wartością i zarządzaj dostępem do tabeli i przypisywania numerów w swojej aplikacji. Może to mieć wpływ na wydajność.
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx
źródło
ORDER BY CheckpointSequence
klauzulą (która jest czasem kolejnością indeksu klastrowanego). Myślę, że sprowadza się to do pytania, czy generowanie wartości tożsamości jest w jakikolwiek sposób powiązane z blokadami podjętymi przez instrukcję INSERT, czy też są to po prostu dwie niezwiązane ze sobą czynności wykonywane przez SQL Server jedna po drugiej.SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence
. Nie sądzę, by to zapytanie odczytało poza zablokowany wiersz 4, czy też nie? (W moich eksperymentach blokuje się, gdy zapytanie próbuje uzyskać blokadę klucza dla wiersza 4.)Podejrzewam, że czasami może to prowadzić do problemów, które nasilają się, gdy serwer jest obciążony. Rozważ dwie transakcje:
W powyższym scenariuszu Twój LAST_READ_ID wyniesie 6, więc 5 nigdy nie zostanie odczytany.
źródło
Uruchamianie tego skryptu:
Poniżej znajdują się blokady, które widzę, że zostały nabyte i zwolnione podczas przechwytywania przez sesję zdarzenia rozszerzonego:
Zwróć uwagę na blokadę RI_N KEY uzyskaną bezpośrednio przed blokadą X dla nowego tworzonego wiersza. Ta krótkotrwała blokada zakresu zapobiegnie uzyskaniu przez współbieżną wstawkę kolejnej blokady RI_N KEY, ponieważ blokady RI_N są niezgodne. Okno wspomniane między krokami 2 i 3 nie stanowi problemu, ponieważ blokada zakresu jest uzyskiwana przed blokadą wiersza na nowo wygenerowanym kluczu.
Tak długo, jak
SELECT...ORDER BY
rozpoczyna się skanowanie przed pożądanymi nowo wstawionymi wierszami, oczekiwałbym pożądanego zachowania na domyślnymREAD COMMITTED
poziomie izolacji, o ileREAD_COMMITTED_SNAPSHOT
opcja bazy danych jest wyłączona.źródło
RangeI_N
są kompatybilne , tzn. Nie blokują się nawzajem (blokada służy głównie do blokowania na istniejącym czytniku szeregowym).Z mojego zrozumienia programu SQL Server domyślnym zachowaniem jest to, że drugie zapytanie nie wyświetla żadnych wyników, dopóki pierwsze zapytanie nie zostanie zatwierdzone. Jeśli pierwsze zapytanie wykonuje ROLLBACK zamiast COMMIT, wówczas w kolumnie będzie brakować identyfikatora.
Podstawowa konfiguracja
Tabela bazy danych
Utworzyłem tabelę bazy danych o następującej strukturze:
Poziom izolacji bazy danych
Sprawdziłem poziom izolacji mojej bazy danych za pomocą następującej instrukcji:
Które zwróciło następujący wynik dla mojej bazy danych:
(Jest to ustawienie domyślne dla bazy danych w SQL Server 2012)
Skrypty testowe
Poniższe skrypty zostały wykonane przy użyciu standardowych ustawień klienta SQL Server SSMS i standardowych ustawień SQL Server.
Ustawienia połączeń klienckich
Klient został skonfigurowany do używania poziomu izolacji transakcji
READ COMMITTED
zgodnie z opcjami zapytania w SSMS.Zapytanie 1
Następujące zapytanie zostało wykonane w oknie zapytania o numerze SPID 57
Zapytanie 2
Następujące zapytanie zostało wykonane w oknie zapytania o numerze SPID 58
Zapytanie nie zostało zakończone i czeka na zwolnienie blokady eXclusive na STRONIE.
Skrypt określający blokowanie
Ten skrypt wyświetla blokowanie występujące na obiektach bazy danych dla dwóch transakcji:
A oto wyniki:
Wyniki pokazują, że okno zapytania nr 1 (SPID 57) ma blokadę współdzieloną (S) w bazie danych, blokadę zamierzonego eXlusive (IX) w OBJECT, blokadę zamierzonego eXlusive (IX) na STRONIE, do której chce wstawić, oraz wyłączność blokada (X) na KLUCZIE, do którego jest włożony, ale jeszcze nie zatwierdzony.
Z powodu nieprzydzielonych danych drugie zapytanie (SPID 58) ma blokadę współdzieloną (S) na poziomie DATABASE, blokadę zamierzonego współdzielenia (IS) w OBJECT, blokadę zamierzonego współdzielenia (IS) na stronie udostępnionej (S ) zablokuj KLUCZ ze statusem żądania CZEKAJ.
Podsumowanie
Zapytanie w pierwszym oknie zapytania jest wykonywane bez zatwierdzania. Ponieważ drugie zapytanie może zawierać tylko
READ COMMITTED
dane, czeka albo na przekroczenie limitu czasu, albo na zatwierdzenie transakcji w pierwszym zapytaniu.Z mojego zrozumienia wynika, że domyślne zachowanie Microsoft SQL Server.
Należy zauważyć, że identyfikator jest rzeczywiście w sekwencji dla kolejnych odczytów instrukcji SELECT, jeśli pierwsza instrukcja COMMITs.
Jeśli pierwsza instrukcja wykonuje ROLLBACK, w sekwencji znajdzie się brakujący identyfikator, ale nadal z identyfikatorem w kolejności rosnącej (pod warunkiem, że utworzono INDEKS z domyślną opcją ASC lub w kolumnie ID).
Aktualizacja:
(Tępo) Tak, możesz polegać na tym, że kolumna tożsamości działa poprawnie, dopóki nie napotkasz problemu. Jest tylko jeden HOTFIX dotyczący SQL Server 2000 i kolumny tożsamości w witrynie Microsoft.
Jeśli nie możesz polegać na poprawnej aktualizacji kolumny tożsamości, myślę, że w witrynie Microsoft byłoby więcej poprawek lub poprawek.
Jeśli masz umowę pomocy technicznej Microsoft, zawsze możesz otworzyć skrzynkę doradczą i poprosić o dodatkowe informacje.
źródło
Identity
wartości a nabyciem zamka KEY w wierszu (do którego mogłyby wpaść jednoczesne odczyty / zapisy). Nie sądzę, aby było to możliwe dzięki twoim obserwacjom, ponieważ nie można zatrzymać wykonywania zapytania i analizować blokad w tym bardzo krótkim okresie czasu.