Rozwiązanie impasu z 2 tabel dotyczy tylko widoku indeksowanego

17

Mam sytuację, w której dostaję impasu i myślę, że zawęziłem sprawców, ale nie jestem pewien, co mogę zrobić, aby to naprawić.

Jest to w środowisku produkcyjnym z programem SQL Server 2008 R2.

Aby nieco uprościć widok sytuacji:


Mam 3 tabele zdefiniowane poniżej:

TABLE activity (
    id, -- PK
    ...
)

TABLE member_activity (
    member_id, -- PK col 1
    activity_id, -- PK col 2
    ...
)

TABLE follow (
    id, -- PK
    follower_id,
    member_id,
    ...
)

The member_activityTabela ma klucz podstawowy związek zdefiniowany jak member_id, activity_id, bo ja zawsze tylko trzeba spojrzeć w górę dane dotyczące tej tabeli tej drodze.

Mam również indeks nieklastrowany follow :

CREATE NONCLUSTERED INDEX [IX_follow_member_id_includes] 
ON follow ( member_id ASC ) INCLUDE ( follower_id )

Dodatkowo mam widok związany ze schematem network_activity który jest zdefiniowany w następujący sposób:

CREATE VIEW network_activity
WITH SCHEMABINDING
AS

SELECT
    follow.follower_id as member_id,
    member_activity.activity_id as activity_id,
    COUNT_BIG(*) AS cb
FROM member_activity
INNER JOIN follow ON follow.member_id = member_activity.member_id
INNER JOIN activity ON activity.id = member_activity.activity_id
GROUP BY follow.follower_id, member_activity.activity_id

Który ma również unikalny indeks klastrowany:

CREATE UNIQUE CLUSTERED INDEX [IX_network_activity_unique_member_id_activity_id] 
ON network_activity
(
    member_id ASC,
    activity_id ASC
)

Teraz mam dwie zakleszczone procedury składowane. Przechodzą przez następujący proces:

-- SP1: insert activity
-----------------------
INSERT INTO activity (...)
SELECT ... FROM member_activity WHERE member_id = @a AND activity_id = @b
INSERT INTO member_activity (...)


-- SP2: insert follow
---------------------
SELECT follow WHERE member_id = @x AND follower_id = @y
INSERT INTO follow (...)

Te dwie procedury działają w izolacji PRZECZYTAJ ZOBOWIĄZANIE. Udało mi się przesłać zapytanie do 1222 danych wyjściowych zdarzeń rozszerzonych i zinterpretowałem następujące kwestie w odniesieniu do impasu:

Dodatek SP1 czeka na RangeS-S blokadę klucza w IX_follow_member_id_includesindeksie, podczas gdy SP2 utrzymuje blokadę powodującą konflikt (X)

Dodatek SP2 czeka na Swłączenie blokady trybuPK_member_activity Dodatek podczas gdy dodatek SP1 utrzymuje powodującą konflikt (X)

Wygląda na to, że impas występuje w ostatnim wierszu każdego zapytania (wstawek). Nie jest dla mnie jasne, dlaczego dodatek SP1 chce zablokowaćIX_follow-member_id_includes indeks. Wydaje mi się, że jedynym linkiem jest ten indeksowany widok i dlatego go umieściłem.

Jaki byłby najlepszy sposób, aby zapobiec tym impasom? Każda pomoc będzie mile widziana. Nie mam dużego doświadczenia w rozwiązywaniu problemów z impasem.

Daj mi znać, jeśli mogę podać dodatkowe informacje, które mogą pomóc!

Z góry dziękuję.


Edycja 1: Dodanie dodatkowych informacji na żądanie.

Oto wyjście 1222 z tego impasu:

<deadlock>
    <victim-list>
        <victimProcess id="process4c6672748" />
    </victim-list>
    <process-list>
        <process id="process4c6672748" taskpriority="0" logused="332" waitresource="KEY: 8:72057594104905728 (25014f77eaba)" waittime="581" ownerId="474698706" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.287" XDES="0x298487970" lockMode="RangeS-S" schedulerid="1" kpid="972" status="suspended" spid="79" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T10:25:00.283" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698706" currentdb="8" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
            <executionStack>
                <frame procname="" line="7" stmtstart="1194" stmtend="1434" sqlhandle="0x02000000a26bb72a2b220406876cad09c22242e5265c82e6" />
                <frame procname="" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 1 --> </inputbuf>
        </process>
        <process id="process6cddc5b88" taskpriority="0" logused="456" waitresource="KEY: 8:72057594098679808 (89013169fc76)" waittime="567" ownerId="474698698" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.283" XDES="0x30c459970" lockMode="S" schedulerid="4" kpid="4204" status="suspended" spid="70" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T15:04:55.870" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698698" currentdb="8" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
            <executionStack>
                <frame procname="" line="18" stmtstart="942" stmtend="1250" sqlhandle="0x03000800ca458d315ee9130100a300000100000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 2 --> </inputbuf>
        </process>
    </process-list>
    <resource-list>
        <keylock hobtid="72057594104905728" dbid="8" objectname="" indexname="" id="lock33299fc00" mode="X" associatedObjectId="72057594104905728">
            <owner-list>
                <owner id="process6cddc5b88" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process4c6672748" mode="RangeS-S" requestType="wait" />
            </waiter-list>
        </keylock>
        <keylock hobtid="72057594098679808" dbid="8" objectname="" indexname="" id="lockb7e2ba80" mode="X" associatedObjectId="72057594098679808">
            <owner-list>
                <owner id="process4c6672748" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process6cddc5b88" mode="S" requestType="wait" />
            </waiter-list>
        </keylock>
    </resource-list>
</deadlock>

W tym przypadku,

relatedObjectId 72057594098679808 odpowiada member_activity, PK_member_activity

relatedObjectId 72057594104905728 odpowiada follow, IX_follow_member_id_includes

Ponadto, tutaj jest bardziej precyzyjny obraz tego, co robią SP1 i SP2

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m1 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m1, @activityId, @field1)

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m2 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m2, @activityId, @field1)

także SP2:

-- SP2: insert follow
---------------------

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

Edycja 2: Po ponownym przeczytaniu komentarzy pomyślałem, że dodam także informacje o tym, które kolumny są kluczami obcymi ...

  • member_activity.member_idjest kluczem obcym do membertabeli
  • member_activity.activity_idjest kluczem obcym do activitytabeli
  • follow.member_idjest kluczem obcym do membertabeli
  • follow.follower_idjest kluczem obcym do membertabeli

Aktualizacja 1:

Wprowadziłem kilka zmian, które moim zdaniem mogłyby pomóc w zapobieganiu impasu, bez powodzenia.

Wprowadzone przeze mnie zmiany były następujące:

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m1 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m2 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

oraz z dodatkiem SP2:

-- SP2: insert follow
---------------------

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow WITH ( UPDLOCK )
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

COMMIT

Po tych dwóch zmianach wydaje mi się, że wciąż mam impas.

Jeśli mogę coś jeszcze podać, proszę dać mi znać. Dzięki.

Leland Richardson
źródło
zatwierdzony odczyt nie przyjmuje blokad zakresu klucza, tylko serializacja. Jeśli zakleszczenie faktycznie pokazuje, że dokonano odczytu (2), to zgaduję, że masz dostęp do zmiany klucza obcego, który zostanie przekształcony w szereg pod seriami (choć nadal powiedz, że odczyt został zatwierdzony). Szczerze mówiąc potrzebowalibyśmy całego ddl i sp, aby dalej pomagać.
Sean Gallardy
@SeanGallardy, dzięki. Zredagowałem, aby uwzględnić wyjście 1222 na wypadek, gdy źle interpretuję, i dodałem więcej szczegółów na temat tego, co robią SP. czy to pomaga?
Leland Richardson
2
@SeanGallardy Część planu zapytań, która utrzymuje widok indeksowany, działa wewnętrznie na SERIALIZABLE(jest w tym nieco więcej, ale to komentarz, a nie odpowiedź :)
Paul White 9
@PaulWhite Dziękuję za wgląd, nie wiedziałem o tym! Wykonując szybki test, zdecydowanie mogę uzyskać szeregowalne tryby blokowania z widokiem indeksowanym podczas wstawiania w procedurach przechowywanych (RangeI-N, RangeS-S, RangeS-U). Wygląda na to, że impas zachodzi od niekompatybilnych trybów blokady uderzających we właściwym czasie o siebie podczas wstawiania w procedurach przechowywanych, gdy wchodzą one w granice blokady (na przykład w obszarze utrzymywanym przez blokadę zasięgu). Myślę, że zarówno czas, jak i kolizja danych wejściowych.
Sean Gallardy,
Pytanie: Jeśli dodam wskazówkę HOLDLOCK do instrukcji SELECT, czy to zapobiegnie blokadzie przy wstawianiu?
Leland Richardson

Odpowiedzi:

5

Konflikt sprowadza się do tego, network_activityże jest widokiem indeksowanym, który należy zachować (wewnętrznie) w instrukcjach DML. Najprawdopodobniej SP1 chce zablokowaćIX_follow-member_id_includes indeksu, ponieważ jest on prawdopodobnie używany przez widok (wygląda na to, że indeks obejmuje indeks).

Dwie możliwe opcje:

  1. Rozważ upuszczenie indeksu klastrowanego w widoku, aby nie był on już widokiem indeksowanym. Czy korzyść płynąca z posiadania go przewyższa koszty utrzymania? Czy wybierasz z niego wystarczająco często, czy warto zwiększyć wydajność indeksowania? Jeśli uruchamiasz te procesy dość często, to może koszt jest wyższy niż korzyść?

  2. Jeśli korzyść z indeksowania Widoku przewyższa koszt, rozważ odizolowanie operacji DML od tabel podstawowych tego Widoku. Można tego dokonać za pomocą blokad aplikacji (patrz sp_getapplock i sp_releaseapplock ). Blokady aplikacji umożliwiają tworzenie blokad wokół dowolnych koncepcji. Oznacza to, że możesz zdefiniować @Resource„aktywność_sieciową” w obu przechowywanych procesach, co zmusi ich do oczekiwania na swoją kolej. Każdy proces miałby tę samą strukturę:

    BEGIN TRANSACTION;
    EXEC sp_getapplock @Resource = 'network_activity', @LockMode = 'Exclusive';
    ...current proc code...
    EXEC sp_releaseapplock @Resource = 'network_activity';
    COMMIT TRANSACTION;

    Musisz samodzielnie zarządzać błędami ROLLBACK(jak podano w dołączonej dokumentacji MSDN), więc włącz to TRY...CATCH. Pozwala to jednak zarządzać sytuacją.
    Uwaga: sp_getapplock / sp_releaseapplocknależy stosować oszczędnie; Blokady aplikacji mogą być bardzo przydatne (na przykład w takich przypadkach), ale należy ich używać tylko wtedy, gdy jest to absolutnie konieczne.

Solomon Rutzky
źródło
Dzięki za pomoc. Przeczytam trochę więcej na temat opcji nr 2 i zobaczę, czy to nam odpowiada. Widok jest odczytywany z całkiem sporo, a indeks klastrowy jest dość dużą pomocą ... więc wolałbym go jeszcze nie usuwać. Wrócę aktualizacji, kiedy dam temu szansę.
Leland Richardson
Myślę, że użycie sp_getapplock będzie działać. Nie byłem jeszcze w stanie wypróbować go w naszym środowisku produkcyjnym, ale chciałem się upewnić, że otrzymałeś nagrodę przed jej wygaśnięciem. Zaktualizuję tutaj, kiedy mogę potwierdzić, że działa!
Leland Richardson,
Dzięki. Jedną fajną rzeczą w Blokadach aplikacji jest to, że możesz zmienić poziom szczegółowości łączenia w coś podobnego member_iddo @Resourcewartości. Nie wydaje się, aby miało to zastosowanie do tej konkretnej sytuacji, ale widziałem, że jest tak używane i jest to całkiem przydatne, szczególnie w systemie z wieloma dzierżawcami, w którym chcesz ograniczyć proces do jednego wątku na klienta, ale wciąż jest wielowątkowy wśród klientów.
Solomon Rutzky
Chciałem dać aktualizacji i powiedzieć, że to nie skończy się pracy w naszym środowisku produkcyjnym. :)
Leland Richardson