Instrukcja scalania sama się zakleszczyła

22

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ą)?

Obraz planu zapytania (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.
Sako73
źródło

Odpowiedzi:

12

OK, po kilkukrotnym przejrzeniu wszystkiego myślę, że twoje podstawowe założenie było prawidłowe. Prawdopodobnie dzieje się tutaj:

  1. Część MATCH części MERGE sprawdza indeks pod kątem dopasowań, blokując odczyt tych wierszy / stron w miarę upływu czasu.

  2. Gdy ma wiersz bez dopasowania, najpierw spróbuje wstawić nowy wiersz indeksu, aby poprosić o blokadę zapisu wiersza / strony ...

Ale jeśli inny użytkownik również przejdzie do kroku 1 w tym samym wierszu / stronie, pierwszy użytkownik zostanie zablokowany z powodu aktualizacji i ...

Jeśli drugi użytkownik również musi wstawić na tej samej stronie, oznacza to, że jest w impasie.

AFAIK, jest tylko jeden (prosty) sposób, aby być w 100% pewnym, że nie można uzyskać impasu dzięki tej procedurze i byłoby dodanie wskazówki TABLOCKX do MERGE, ale prawdopodobnie miałoby to naprawdę zły wpływ na wydajność.

Jest możliwe , że dodanie podpowiedź TABLOCK zamiast wystarczy, by rozwiązać problem bez konieczności Big wpływ na wydajność.

Na koniec możesz także spróbować dodać PAGLOCK, XLOCK lub oba PAGLOCK i XLOCK. Znów może to działać, a wydajność może nie być zbyt okropna. Musisz spróbować, aby zobaczyć.

RBarryYoung
źródło
Czy uważasz, że poziom izolacji migawek (wersjonowanie wierszy) może być tutaj pomocny?
Mikael Eriksson
Może. Lub może zmienić wyjątki impasu w wyjątki współbieżności.
RBarryYoung
2
Określenie wskazówki TABLOCK w tabeli, która jest celem instrukcji INSERT, ma taki sam efekt, jak określenie wskazówki TABLOCKX. (Źródło: msdn.microsoft.com/en-us/library/bb510625.aspx )
tutaj
31

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):

Istniejący plan

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 TYPEzmiennej tabeli w celu włączenia klastra PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

Plan wykonania pokazuje teraz skan indeksu klastrowanego, a gwarancja unikatowości oznacza, że ​​optymalizator jest w stanie bezpiecznie usunąć bufor bufora:

Z kluczem podstawowym

W testach z 5000 iteracjami MERGEinstrukcji 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:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Pozwala to również wyeliminować bufor (i spójność rzędu wierszy) kosztem wprowadzenia jawnego sortowania:

Sortuj plan

Ten plan nie spowodował również zakleszczenia przy użyciu tego samego testu. Skrypt reprodukcji poniżej:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Paul White mówi GoFundMonica
źródło
8

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:

  1. 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ć.

  2. 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ść.

  3. 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.

AK
źródło
-1

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.

Ed Green
źródło