Вирішення тупикової ситуації з 2 таблиць пов'язане лише через індексований вигляд


17

У мене ситуація, коли я отримую тупики, і я думаю, що я звузив винуватців, але я не зовсім впевнений, що можу зробити, щоб виправити це.

Це у виробничому середовищі під управлінням SQL Server 2008 R2.

Щоб дати вам трохи спрощений погляд на ситуацію:


У мене є 3 таблиці, як визначено нижче:

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,
    ...
)

У member_activityтаблиці є складний первинний ключ, визначений як member_id, activity_id, тому що мені потрібно коли-небудь шукати дані в цій таблиці таким чином.

Я також маю некластеризований індекс на follow:

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

Крім того, у мене є схема, пов'язана network_activityзі схемою, яка визначається наступним чином:

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

Який також має унікальний кластерний індекс:

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

Тепер у мене є дві збережені процедури, що зберігаються в тупику. Вони проходять наступний процес:

-- 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 (...)

Обидві ці процедури виконуються в ЧИТАЙТЕ ІЗОБРАЖЕНО. Мені вдалося запитати вихід 1222 розширених подій і інтерпретував таке щодо тупиків:

SP1 очікує RangeS-Sблокування ключа в IX_follow_member_id_includesіндексі, тоді як SP2 тримає конфліктуючий (X) замок

SP2 очікує Sввімкнення блокування режиму, PK_member_activity а SP1 блокує конфлікт (X)

Виходить, що тупик відбувається в останньому рядку кожного запиту (вставки). Мені незрозуміло, чому SP1 хоче заблокувати IX_follow-member_id_includesіндекс. Мені здається, що це єдине посилання з цього індексованого погляду, і саме тому я включив його.

Що було б для мене найкращим способом запобігти тому, щоб ці тупики не траплялися? Будь-яка допомога буде дуже вдячна. Я не маю великого досвіду у вирішенні проблем з тупиком.

Будь ласка, дайте мені знати, чи є ще якась інформація, яку я можу надати, яка могла б допомогти!

Заздалегідь спасибі.


Редагувати 1: Додавання додаткової інформації на запит.

Ось 1222 вихід із цього глухого кута:

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

В цьому випадку,

пов’язанийObjectId 72057594098679808 відповідає member_activity, PK_member_activity

relatedObjectId відповідає 72057594104905728 follow, IX_follow_member_id_includes

Крім того, ось більш точна картина того, що роблять SP1 та 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)

також 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)

Редагувати 2: Після перечитування коментарів я подумав додати трохи інформації про те, які стовпці є іноземними ключами ...

  • member_activity.member_id- іноземний ключ до memberстолу
  • member_activity.activity_idє іноземним ключем до activityстолу
  • follow.member_id- іноземний ключ до memberстолу
  • follow.follower_id- іноземний ключ до memberстолу

Оновлення 1:

Я вніс кілька змін, які, на мою думку, можуть допомогти запобігти тупиковість, не пощастивши.

Зміни, які я вніс, були такими:

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

і з 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

З цими двома змінами я все ще здаюсь тупиком.

Якщо я можу надати щось інше, будь ласка, повідомте мене про це. Спасибі.


читання, здійснене, не приймає блокування ключового діапазону, лише серіалізація. Якщо глухий кут насправді показує, що прочитане зроблено (2), то, на мою думку, ви отримуєте доступ до зміни закордонного ключа, який під обкладинками буде перетворений на серіалізацію (хоча все ще кажуть, що читання здійснено). Нам б чесно потрібен цілий DDL і sp, щоб допомогти в подальшому.
Шон Галларді

@SeanGallardy, спасибі Я відредагував, щоб включити вихід 1222 у випадку, якщо я інтерпретував неправильно, і я додав більше деталей про те, що роблять SP. Чи допомагає це?
Leland Richardson

2
@SeanGallardy Частина плану запитів, яка підтримує індексований вигляд, працює внутрішньо на SERIALIZABLE(є трохи більше, ніж це, але це коментар, а не відповідь :)
Пол Білий 9

@PaulWhite Дякую за розуміння, я цього не знав! Зробивши швидкий тест, я точно можу отримати режими серіалізаційного блокування з індексованим видом під час вставки у ваші збережені процедури (RangeI-N, RangeS-S, RangeS-U). Схоже, що тупик відбувається від несумісних режимів блокування, які потрапляють у потрібний час один проти одного під час вставок у ваші збережені процедури, коли вони потрапляють всередину меж блокування (наприклад, у зону, яку тримає замок діапазону). Думаю, і про терміни, і про зіткнення вхідних даних.
Шон Галларді

Питання: Якщо я додав натяк HOLDLOCK на оператори SELECT, чи не завадить це зробити блокування на вставці?
Leland Richardson

Відповіді:


5

Конфлікт зводиться до network_activityіндексованого перегляду, який потрібно підтримувати (внутрішньо) в операторах DML. Це найімовірніше, чому SP1 хоче заблокувати IX_follow-member_id_includesіндекс, як це, ймовірно, використовується View (це здається, що це індекс покриття для View).

Два можливі варіанти:

  1. Розгляньте можливість скидання індексу кластеру на вигляд, щоб він більше не був індексованим видом. Чи користь від того, що вона має, перевищує вартість обслуговування? Ви вибираєте з нього досить часто чи варто підвищити продуктивність, якщо вона індексується? Якщо ви запускаєте ці програми досить часто, то, можливо, вартість вище, ніж вигода?

  2. Якщо вигода від індексації View переважає вартість, то розгляньте можливість ізоляції DML-операцій від базових таблиць цього представлення. Це можна зробити за допомогою програми Lock Lock (див. Sp_getapplock та sp_releaseapplock ). Блокування програм дозволяє створювати замки навколо довільних понять. Це означає, що ви можете визначити @Resourceяк "network_activity" в обох зберіганих програмах, що змусить їх чекати своєї черги. Кожен прок буде дотримуватися тієї ж структури:

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

    Вам потрібно керувати помилками / ROLLBACKсамостійно (як зазначено в пов'язаній документації MSDN), так що вводиться у звичайне TRY...CATCH. Але це дозволяє вам керувати ситуацією.
    Зверніть увагу: sp_getapplock / sp_releaseapplockслід використовувати економно; Блокування додатків, безумовно, може бути дуже зручним (як, наприклад, у подібних випадках), але використовувати їх слід лише в разі крайньої необхідності.


Дякую за допомогу. Я збираюся прочитати трохи більше на варіант №2 і побачити, чи працює це для нас. Вигляд читається зовсім небагато, а кластерний індекс - досить велика допомога ... тому я краще не видаляю його ще. Я повернусь оновлення, як тільки я спробую це зробити.
Leland Richardson

Я думаю, що використання sp_getapplock буде працювати. Я ще не міг спробувати це в нашому виробничому середовищі, але я хотів переконатися, що ви отримали нагороду ще до її закінчення. Я оновлю тут, коли зможу підтвердити, що це працює!
Leland Richardson

Спасибі. Одним із приємних речей щодо Lock Application є те, що ви можете змінити рівень деталізації об'єднань у щось подібне member_idдо @Resourceзначення. Це, мабуть, не стосується даної конкретної ситуації, але я бачив, що він використовується таким чином, і це дуже зручно, особливо в системі з багатьма орендарями, де ви хочете обмежити процес одним потоком на основі кожного клієнта, але все ще має бути багатопоточним для клієнтів.
Соломон Руцький

Я хотів дати оновлення і сказати, що це все- таки працює в нашому виробничому середовищі. :)
Leland Richardson
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.