Mam następującą procedurę (SQL Server 2008 R2):
create procedure usp_SaveCompanyUserData
@companyId bigint,
@userId bigint,
@dataTable tt_CoUserdata readonly
as
begin
set nocount, xact_abort on;
merge CompanyUser with (holdlock) as r
using (
select
@companyId as CompanyId,
@userId as UserId,
MyKey,
MyValue
from @dataTable) as newData
on r.CompanyId = newData.CompanyId
and r.UserId = newData.UserId
and r.MyKey = newData.MyKey
when not matched then
insert (CompanyId, UserId, MyKey, MyValue) values
(@companyId, @userId, newData.MyKey, newData.MyValue);
end;
CompanyId, UserId, MyKey tworzą klucz złożony dla tabeli docelowej. CompanyId to klucz obcy do tabeli nadrzędnej. Ponadto istnieje indeks nieklastrowany CompanyId asc, UserId asc
.
Jest wywoływany z wielu różnych wątków i konsekwentnie dostaję zakleszczenia między różnymi procesami wywołującymi tę samą instrukcję. Zrozumiałem, że „z blokadą” było konieczne, aby zapobiec błędom wstawiania / aktualizacji warunków wyścigu.
Zakładam, że dwa różne wątki blokują wiersze (lub strony) w różnych porządkach podczas sprawdzania poprawności ograniczeń, a tym samym blokują się.
Czy to prawidłowe założenie?
Jaki jest najlepszy sposób rozwiązania tej sytuacji (tj. Brak impasu, minimalny wpływ na wydajność wielowątkową)?
(Jeśli obraz jest wyświetlany w nowej karcie, jest czytelny. Przepraszamy za mały rozmiar.)
- W @datatable znajduje się maksymalnie 28 wierszy.
- Prześledziłem kod i nigdzie nie widzę, że tutaj rozpoczynamy transakcję.
- Klucz obcy jest skonfigurowany do kaskadowania tylko podczas usuwania i nie było żadnych usunięć z tabeli nadrzędnej.
Nie byłoby problemu, gdyby zmienna tabeli zawierała tylko jedną wartość. Z wieloma rzędami istnieje nowa możliwość impasu. Załóżmy, że dwa równoległe procesy (A i B) działają ze zmiennymi tabel zawierającymi (1, 2) i (2, 1) dla tej samej firmy.
Proces A odczytuje miejsce docelowe, nie znajduje wiersza i wstawia wartość „1”. Posiada wyłączną blokadę wierszy dla wartości „1”. Proces B odczytuje miejsce docelowe, nie znajduje wiersza i wstawia wartość „2”. Posiada wyłączną blokadę wierszy dla wartości „2”.
Teraz proces A musi przetworzyć wiersz 2, a proces B musi przetworzyć wiersz 1. Żaden proces nie może zrobić postępu, ponieważ wymaga blokady, która jest niezgodna z blokadą wyłączną posiadaną przez inny proces.
Aby uniknąć zakleszczeń z wieloma wierszami, wiersze muszą być przetwarzane (i dostęp do tabel) za każdym razem w tej samej kolejności . Zmienna tabeli w planie wykonania pokazanym w pytaniu jest stertą, więc wiersze nie mają wewnętrznej kolejności (istnieje duże prawdopodobieństwo, że zostaną odczytane w kolejności wstawiania, choć nie jest to gwarantowane):
Brak spójnej kolejności przetwarzania wierszy prowadzi bezpośrednio do impasu. Drugą kwestią jest to, że brak kluczowej gwarancji unikalności oznacza, że szpula stołowa jest niezbędna do zapewnienia właściwej ochrony na Halloween. Szpula jest chętną szpulą, co oznacza, że wszystkie wiersze są zapisywane w stole roboczym tempdb, zanim zostaną ponownie odczytane i odtworzone dla operatora Insert.
Ponowne zdefiniowanie
TYPE
zmiennej tabeli w celu włączenia klastraPRIMARY KEY
:Plan wykonania pokazuje teraz skan indeksu klastrowanego, a gwarancja unikatowości oznacza, że optymalizator jest w stanie bezpiecznie usunąć bufor bufora:
W testach z 5000 iteracjami
MERGE
instrukcji dla 128 wątków nie wystąpiły zakleszczenia ze zmienną tabeli klastrowej. Powinienem podkreślić, że dzieje się tak tylko na podstawie obserwacji; zmienna tabeli klastrowej może również ( technicznie ) wytwarzać swoje wiersze w różnych rzędach, ale szanse na spójne zamówienie są znacznie zwiększone. Obserwowane zachowanie należy oczywiście przetestować ponownie dla każdej nowej aktualizacji zbiorczej, dodatku Service Pack lub nowej wersji SQL Server.W przypadku, gdy nie można zmienić definicji zmiennej tabeli, istnieje inna alternatywa:
Pozwala to również wyeliminować bufor (i spójność rzędu wierszy) kosztem wprowadzenia jawnego sortowania:
Ten plan nie spowodował również zakleszczenia przy użyciu tego samego testu. Skrypt reprodukcji poniżej:
źródło
Myślę, że SQL_Kiwi dostarczył bardzo dobrą analizę. Jeśli chcesz rozwiązać problem w bazie danych, postępuj zgodnie z jego sugestią. Oczywiście musisz ponownie sprawdzić, czy nadal działa dla Ciebie przy każdej aktualizacji, zastosowaniu dodatku Service Pack lub dodaniu / zmianie indeksu lub widoku indeksowanego.
Istnieją trzy inne alternatywy:
Możesz serializować swoje wstawki, aby się nie kolidowały: możesz wywołać sp_getapplock na początku transakcji i uzyskać wyłączną blokadę przed wykonaniem MERGE. Oczywiście nadal musisz to przetestować.
Możesz mieć jeden wątek obsługujący wszystkie wstawki, dzięki czemu Twój serwer aplikacji będzie obsługiwał współbieżność.
Możesz automatycznie ponowić próbę po zakleszczeniu - może to być najwolniejsze podejście, jeśli współbieżność jest wysoka.
Tak czy inaczej, tylko Ty możesz określić wpływ swojego rozwiązania na wydajność.
Zazwyczaj nie mamy w ogóle impasu w naszym systemie, chociaż mamy duży potencjał, aby je mieć. W 2011 roku popełniliśmy błąd w jednym wdrożeniu i mieliśmy kilka impasów, które wystąpiły w ciągu kilku godzin, wszystkie zgodnie z tym samym scenariuszem. Naprawiłem to wkrótce i to były wszystkie impasy w tym roku.
W naszym systemie najczęściej stosujemy podejście 1. Działa nam naprawdę dobrze.
źródło
Jeszcze jedno możliwe podejście - znalazłem opcję Łączenie, która czasami powoduje problemy z blokowaniem i wydajnością - warto grać z opcją zapytania Opcja (MaxDop x)
W ciemnej i odległej przeszłości SQL Server miał opcję Wstaw blokowanie na poziomie wiersza - ale wydaje się, że to umarła śmierć, jednak klastrowana PK z tożsamością powinna sprawić, że wstawki będą czyste.
źródło