Zarządzanie współbieżnością przy użyciu wzorca SELECT-UPDATE

25

Załóżmy, że masz następujący kod (zignoruj, że jest okropny):

BEGIN TRAN;
DECLARE @id int
SELECT @id = id + 1 FROM TableA;
UPDATE TableA SET id = @id; --TableA must have only one row, apparently!
COMMIT TRAN;
-- @id is returned to the client or used somewhere else

Moim zdaniem NIE jest to właściwe zarządzanie współbieżnością. To, że masz transakcję, nie oznacza, że ​​ktoś nie przeczyta tej samej wartości, co ty, zanim przejdziesz do wyciągu z aktualizacji.

Teraz, pozostawiając kod bez zmian (zdaję sobie sprawę, że lepiej jest traktować go jako pojedynczą instrukcję, a nawet lepiej, używając kolumny autoinkrementacji / tożsamości), jakie są pewne sposoby na poprawne obsługiwanie współbieżności i zapobieganie warunkom wyścigu, które pozwalają dwóm klientom uzyskać to samo wartość id?

Jestem prawie pewien, że dodanie a WITH (UPDLOCK, HOLDLOCK)do SELECT załatwi sprawę. SERIALIZABLE poziom izolacji transakcji byłoby wydaje się działać jak dobrze, ponieważ zaprzecza ktoś czytać to, co zrobiłeś, aż tran jest skończona ( UPDATE :. Ta odpowiedź jest fałszywa Zobacz Martin). Czy to prawda? Czy oba będą działać równie dobrze? Czy jedno jest lepsze od drugiego?

Wyobraź sobie, że wykonujesz coś bardziej uzasadnionego niż aktualizacja identyfikatora - niektóre obliczenia na podstawie odczytu, który musisz zaktualizować. Może być zaangażowanych wiele tabel, z których niektóre napiszesz, a inne nie. Jaka jest tutaj najlepsza praktyka?

Po napisaniu tego pytania, myślę, że wskazówki blokujące są lepsze, ponieważ wtedy blokujesz tylko te tabele, których potrzebujesz, ale byłbym wdzięczny za wkład każdego.

PS I nie, nie znam najlepszej odpowiedzi i naprawdę chcę uzyskać lepsze zrozumienie! :)

ErikE
źródło
Tylko dla wyjaśnienia: czy chcesz uniemożliwić 2 klientom odczytanie tej samej wartości lub wydanie, updatektóre może być oparte na nieaktualnych danych? Jeśli to drugie, możesz użyć rowversionkolumny, aby sprawdzić, czy wiersz, który ma zostać zaktualizowany, nie został zmieniony od momentu jego odczytania.
a1ex07
Nie chcemy, aby drugi klient pobierał starą wartość identyfikatora przed zaktualizowaniem jej do nowej wartości przez pierwszego klienta. Powinien blokować.
ErikE

Odpowiedzi:

11

Wystarczy odnieść się do SERIALIZABLEaspektu poziomu izolacji. Tak, to zadziała, ale z ryzykiem zakleszczenia.

Dwie transakcje będą mogły jednocześnie odczytywać wiersz. Nie będą się wzajemnie blokować, ponieważ albo zabiorą Sblokadę obiektu, albo blokadę indeksu w RangeS-Szależności od struktury tabeli i te blokady są kompatybilne . Ale będą się wzajemnie blokować podczas próby uzyskania blokad potrzebnych do aktualizacji ( IXblokada obiektu lub indeksRangeS-U ), co doprowadzi do impasu.

Zastosowanie wyraźnej UPDLOCKpodpowiedzi zamiast tego spowoduje serializację odczytów, co pozwoli uniknąć ryzyka zakleszczenia.

Martin Smith
źródło
+1, ale: w przypadku tablic sterty nadal można uzyskać impas konwersji nawet przy blokadach aktualizacji: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/03/11/…
AK
Dziwaczne, @alex. Wyobrażam sobie, że ma to związek ze stanem wyścigowym silnika, próbującym znaleźć to, co zablokować, zanim faktycznie go URUCHOMIĘ ...
ErikE
@ErikE - Impas konwersji w artykule Alexa jest konwersja z IXaby Xna samej stercie. Co ciekawe, żadne rzędy się nie kwalifikują, więc nigdy nie zostaną usunięte blokady rzędu. Nie jestem pewien, dlaczego w ogóle bierze Xzamek.
Martin Smith
11

Myślę, że najlepszym rozwiązaniem byłoby wystawienie modułu na wysoką współbieżność i przekonanie się sam. Czasami wystarczy sam UPDLOCK, a HOLDLOCK nie jest potrzebny. Czasami sp_getapplock działa bardzo dobrze. Nie wydałbym tutaj żadnej instrukcji ogólnej - czasem dodanie jeszcze jednego indeksu, wyzwalacza lub widoku indeksowanego zmienia wynik. Musimy poddać kod testowi warunków skrajnych i przekonać się osobno dla każdego przypadku.

Pisałem kilka przykładów testów warunków skrajnych tutaj

Edytować: aby lepiej poznać elementy wewnętrzne, możesz przeczytać książki Kalena Delaneya. Książki mogą się jednak nie synchronizować, tak jak każda inna dokumentacja. Poza tym istnieje zbyt wiele kombinacji do rozważenia: sześć poziomów izolacji, wiele rodzajów blokad, indeksy klastrowane / nieklastrowane i kto wie, co jeszcze. To dużo kombinacji. Ponadto SQL Server jest zamkniętym źródłem, więc nie możemy pobierać kodu źródłowego, debugować go i tak dalej - byłoby to ostateczne źródło wiedzy. Wszystko inne może być niekompletne lub nieaktualne po następnej wersji lub dodatku Service Pack.

Dlatego nie powinieneś decydować, co działa dla twojego systemu bez własnych testów warunków skrajnych. Cokolwiek przeczytałeś, może ci pomóc zrozumieć, co się dzieje, ale musisz udowodnić, że porady, które przeczytałeś, działają dla ciebie. Nie sądzę, żeby ktokolwiek mógł to dla ciebie zrobić.

AK
źródło
9

W tym konkretnym przypadku dodanie UPDLOCKblokady do SELECTrzeczywiście uniemożliwiłoby anomalie. Dodanie HOLDLOCKnie jest konieczne, ponieważ blokada aktualizacji jest utrzymywana na czas trwania transakcji, ale przyznaję się do włączenia jej jako (prawdopodobnie złego) nawyku w przeszłości.

Wyobraź sobie, że wykonujesz coś bardziej uzasadnionego niż aktualizacja identyfikatora, jakieś obliczenia oparte na odczycie, które musisz zaktualizować. Może być zaangażowanych wiele tabel, z których niektóre napiszesz, a inne nie. Jaka jest tutaj najlepsza praktyka?

Nie ma najlepszej praktyki. Wybór kontroli współbieżności musi być oparty na wymaganiach aplikacji. Niektóre aplikacje / transakcje muszą być wykonywane tak, jakby posiadały wyłączną własność bazy danych, unikając anomalii i niedokładności za wszelką cenę. Inne aplikacje / transakcje mogą tolerować pewien stopień wzajemnych zakłóceń.

  • Pobieranie pasmowego poziomu zapasów (<5, 10+, 50+, 100+) dla produktu w sklepie internetowym = brudny odczyt (niedokładne nie ma znaczenia).
  • Sprawdzanie i zmniejszanie poziomu zapasów w kasie sklepu internetowego = powtarzalny odczyt (MUSIMY mieć zapas zanim sprzedamy, NIE MUSIMY skończyć z ujemnym poziomem zapasów).
  • Przenoszenie środków pieniężnych między moim rachunkiem bieżącym a oszczędnościowym w banku = możliwe do serializacji (nie przeliczaj ani nie zgub moich środków pieniężnych!).

Edycja: Komentarz @ AlexKuznetsova skłonił mnie do ponownego przeczytania pytania i usunięcia bardzo oczywistego błędu w mojej odpowiedzi. Uwaga do siebie w późnych godzinach nocnych.

Mark Storey-Smith
źródło