Тупик при оновленні різних рядків з некластеризованим індексом


13

Я вирішую проблему з глухим блоком, коли помічаю, що поведінка блокування відрізняється, коли я використовую кластерний та некластеризований індекс у полі id. Проблема з тупиковою ситуацією, схоже, вирішується, якщо до поля id застосовано кластеризований індекс або первинний ключ.

У мене є різні транзакції, роблячи одне або кілька оновлень для різних рядків, наприклад транзакція A оновлює лише рядок з ID = a, tx B буде торкатися лише рядка з ID = b тощо.

І я зрозумів, що без індексу оновлення придбає блокування оновлення для всіх рядків і при необхідності приховає до ексклюзивного блокування, що в підсумку призведе до тупикової ситуації. Але я не можу з’ясувати, чому з некластеризованим індексом тупик все ще є (хоча швидкість звернення здається зниженою)

Таблиця даних:

CREATE TABLE [dbo].[user](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [userName] [nvarchar](255) NULL,
    [name] [nvarchar](255) NULL,
    [phone] [nvarchar](255) NULL,
    [password] [nvarchar](255) NULL,
    [ip] [nvarchar](30) NULL,
    [email] [nvarchar](255) NULL,
    [pubDate] [datetime] NULL,
    [todoOrder] [text] NULL
)

Тупиковий слід

deadlock-list
deadlock victim=process4152ca8
process-list
process id=process4152ca8 taskpriority=0 logused=0 waitresource=RID: 5:1:388:29 waittime=3308 ownerId=252354 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.947 XDES=0xb0bf180 lockMode=U schedulerid=3 kpid=11392 status=suspended spid=57 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.953 lastbatchcompleted=2014-04-11T00:15:30.950 lastattention=1900-01-01T00:00:00.950 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252354 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=62 sqlhandle=0x0200000062f45209ccf17a0e76c2389eb409d7d970b0f89e00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(2)<c/>@owner int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
process id=process4153468 taskpriority=0 logused=4652 waitresource=KEY: 5:72057594042187776 (3fc56173665b) waittime=3303 ownerId=252344 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.920 XDES=0x4184b78 lockMode=U schedulerid=3 kpid=7272 status=suspended spid=58 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.960 lastbatchcompleted=2014-04-11T00:15:30.960 lastattention=1900-01-01T00:00:00.960 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252344 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=60 sqlhandle=0x02000000d4616f250747930a4cd34716b610a8113cb92fbc00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(61)<c/>@uid int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
resource-list
ridlock fileid=1 pageid=388 dbid=5 objectname=SQL2012_707688_webows.dbo.user id=lock3f7af780 mode=X associatedObjectId=72057594042122240
owner-list
owner id=process4153468 mode=X
waiter-list
waiter id=process4152ca8 mode=U requestType=wait
keylock hobtid=72057594042187776 dbid=5 objectname=SQL2012_707688_webows.dbo.user indexname=10 id=lock3f7ad700 mode=U associatedObjectId=72057594042187776
owner-list
owner id=process4152ca8 mode=U
waiter-list
waiter id=process4153468 mode=U requestType=wait

Також цікавим та можливим пов'язаним висновком є ​​те, що кластерний та некластеризований індекс, схоже, має різні способи блокування

При використанні кластерного індексу є ексклюзивне блокування ключа, а також ексклюзивне блокування на RID під час оновлення, яке очікується; в той час, як є два ексклюзивних блокування на двох різних RID, якщо використовується некластеризований індекс, що мене бентежить.

Буде корисно, якщо хтось може пояснити, чому саме на цьому.

Тестовий SQL:

use SQL2012_707688_webows;
begin transaction;
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
exec sp_lock;
commit;

Ідентифікатор як індекс кластеру:

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   1   KEY (b1a92fe5eed4)                      X   GRANT
53  5   917578307   1   PAG 1:879                               IX  GRANT
53  5   917578307   1   PAG 1:1928                              IX  GRANT
53  5   917578307   1   RID 1:879:7                             X   GRANT

Ідентифікатор як некластеризований індекс

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   0   PAG 1:879                               IX  GRANT
53  5   917578307   0   PAG 1:1928                              IX  GRANT
53  5   917578307   0   RID 1:879:7                             X   GRANT
53  5   917578307   0   RID 1:1928:18                           X   GRANT

EDIT1: Подробиці тупикового зв'язку без будь-якого індексу
Скажіть, що у мене є два tx A і B, кожен з яких має два заяви про оновлення, різний рядок, звичайно,
tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63502
update [user] with (rowlock) set todoOrder='{4}' where id = 63502

{1} і {4} мали б шанс зайти в тупик

у {1} ​​для рядка 63502 запитується U блокування, оскільки йому потрібно виконати сканування таблиці, а X-замок міг утримуватися у рядку 63501, оскільки він відповідає умові

у {4} для рядка 63501 запитується U блокування, а блокування X вже утримується для 63502

Таким чином, у нас txA утримує 63501 і чекає 63502, а txB утримує 63502 в очікуванні 63501, що є тупиком

EDIT2: Виявляється помилка мого тестового випадку, тут ситуація різниться. Вибачте за плутанину, але помилка робить ситуацію різною і, здається, з часом може призвести до тупикової ситуації.

Оскільки аналіз Павла справді мені допоміг у цій справі, тому я прийму це як відповідь.

Через помилку мого тестового випадку дві транзакції txA і txB можуть оновити ту саму рядок, як нижче:

tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63501

{2} і {3} матимуть шанс зайти в тупик, коли:

txA вимагає блокування U ключа на ключі, утримуючи X блокування на RID (завдяки оновленню {1}) txB запитує U блокування RID, утримуючи U блокування клавіші


1
Я не можу зрозуміти, чому транзакції потрібно оновлювати один і той же рядок двічі.
ypercubeᵀᴹ

@ypercube Хороший момент, це щось, що я повинен вдосконалити. Але в цьому випадку я просто хочу краще розуміти поведінку блокування
Bood

@ypercube після більшої думки. Я думаю, що можливо, що програма зі складною логікою повинна двічі оновлювати один і той же рядок в одному і тому ж tx, можуть бути різні стовпці, наприклад
Bood

Відповіді:


16

... чому з кластерним індексом тупик все ще існує (хоча швидкість звернення здається зниженою)

Питання не є ясним (наприклад, скільки оновлень та яких idзначень є в кожній транзакції), але виникає один очевидний сценарій тупикового зв'язку з декількома оновленнями ряду одно рядків у межах однієї транзакції, де є перекриття [id]значень, і ідентифікатори оновлено в іншому [id]порядку:

[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;

Послідовність тупикової ситуації: T1 (u2), T2 (u1), T1 (u1) чекати , T2 (u2) чекати .

Цю послідовність тупикових ситуацій можна уникнути шляхом суворого оновлення в порядку ідентифікації в межах кожної транзакції (придбання блокувань у тому ж порядку на тому ж шляху).

При використанні кластерного індексу є ексклюзивне блокування ключа, а також ексклюзивне блокування на RID під час оновлення, яке очікується; в той час, як є два ексклюзивних блокування на двох різних RID, якщо використовується некластеризований індекс, що мене бентежить.

Якщо унікальний кластерний індекс увімкнено id, клавіш кластера робиться ексклюзивним блокуванням для захисту запису в рядкові дані. Для RIDзахисту запису в textстовпець LOB , який зберігається на окремій сторінці даних за замовчуванням, потрібен окремий ексклюзивний замок .

Коли таблиця - це купа з лише некластеризованим індексом id, відбувається дві речі. По-перше, один RIDексклюзивний замок стосується купи в рядкових даних, а другий - замок на даних LOB, як і раніше. Другий ефект полягає в тому, що потрібен складніший план виконання.

За допомогою кластерного індексу та простого оновлення предиката однозначного рівності, процесор запитів може застосувати оптимізацію, яка виконує оновлення (читання та запис) в одному операторі, використовуючи єдиний шлях:

Оновлення для одного оператора

Рядок розміщується та оновлюється в рамках однієї операції пошуку, вимагаючи лише виключних блокувань (блокування оновлення не потрібно). Приклад послідовності блокування за допомогою зразкової таблиці:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IX lock on PAGE: 6:1:59104 -- INROW
acquiring X lock on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
acquiring IX lock on PAGE: 6:1:59091 -- LOB
acquiring X lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 -- LOB
releasing lock reference on RID: 6:1:59091:1 -- LOB
releasing lock reference on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
releasing lock reference on PAGE: 6:1:59104 -- INROW

Маючи лише некластеризований індекс, таку ж оптимізацію неможливо застосувати, оскільки нам потрібно читати з однієї структури b-дерева та писати іншу. План багатодоріжок має окремі фази читання і запису:

Оновлення багато ітераторів

Це отримує блокування оновлень під час читання, перетворюючись на ексклюзивні блокування, якщо рядок кваліфікується. Приклад послідовності блокування із заданою схемою:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IU lock on PAGE: 6:1:59105 -- NC INDEX
acquiring U lock on KEY: 6:72057594233749504 (61a06abd401c) -- NC INDEX
acquiring IU lock on PAGE: 6:1:59104 -- HEAP
acquiring U lock on RID: 6:1:59104:1 -- HEAP
acquiring IX lock on PAGE: 6:1:59104 -- HEAP convert to X
acquiring X lock on RID: 6:1:59104:1 -- HEAP convert to X
acquiring IU lock on PAGE: 6:1:59091 -- LOB
acquiring U lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 
releasing lock reference on RID: 6:1:59091:1
releasing lock reference on RID: 6:1:59104:1
releasing lock reference on PAGE: 6:1:59104 
releasing lock on KEY: 6:72057594233749504 (61a06abd401c)
releasing lock on PAGE: 6:1:59105 

Зверніть увагу, що дані LOB читаються та записуються в ітераторі оновлення таблиці. Складніший план та кілька шляхів читання та запису збільшують шанси на тупик.

Нарешті, я не можу не помітити типи даних, що використовуються у визначенні таблиці. Не слід використовувати застарілий textтип даних для нової роботи; альтернативою, якщо вам дійсно потрібна можливість зберігання до 2 Гб даних у цьому стовпці, є varchar(max). Одна важлива відмінність між textі varchar(max)полягає в тому, що textдані зберігаються поза рядком за замовчуванням, тоді як varchar(max)зберігаються в рядку за замовчуванням.

Використовуйте типи Unicode лише в тому випадку, якщо вам потрібна така гнучкість (наприклад, важко зрозуміти, чому для IP-адреси знадобиться Unicode). Крім того, виберіть відповідні обмеження довжини для своїх атрибутів - 255 всюди здається навряд чи правильним.

Додаткове читання: серія усунення неполадок Барта Данкана із
завмерлої проблемою та широким діапазоном

Відстеження замків можна проводити різними способами. SQL Server Express з розширеними послугами ( лише для SP1 2014 та 2012 ) містить інструмент Profiler , який підтримує спосіб перегляду деталей придбання та випуску блокування.


Відмінна відповідь. Як ви виводите журнали / сліди, що містять повідомлення "придбання ... блокування" та "звільнення довідок блокування"?
Санжив Дживан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.