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?