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

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;

С идентификатором Clustered Index:

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} блокировка U запрашивается для строки 63502, поскольку ей необходимо выполнить сканирование таблицы, и блокировка X могла быть удержана в строке 63501, поскольку она соответствует условию

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

таким образом, у нас есть txA, удерживающий 63501 и ожидающий 63502, в то время как txB держит 63502, ожидая 63501, что является тупиком

РЕДАКТИРОВАТЬ 2: Оказывается, ошибка в моем тестовом примере имеет значение ситуации Извините за путаницу, но ошибка создает ситуацию и, похоже, в конечном итоге вызывает тупик.

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

Из-за ошибки в моем тестовом примере две транзакции 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-блокировку на ключе

Bood
источник
1
Я не могу понять, почему транзакция должна обновить одну и ту же строку дважды.
ypercubeᵀᴹ
@ypercube Хорошо, это то, что я должен улучшить. Но в этом случае я просто хочу лучше понять поведение блокировки
Bood
@ypercube после долгих раздумий, я думаю, что возможно, приложению со сложной логикой нужно обновить одну и ту же строку дважды в одной и той же
передаче,

Ответы:

16

... почему с кластерным индексом тупик все еще существует (хотя скорость попадания, кажется, снижается)

Вопрос не совсем ясен (например, сколько обновлений и к каким idзначениям в каждой транзакции), но возникает один очевидный сценарий взаимоблокировки с несколькими однострочными обновлениями в одной транзакции, где есть перекрытие [id]значений, и идентификаторы обновляется в другом [id]порядке:

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

Последовательность взаимоблокировки: Т1 (и2), Т2 (u1), Т1 (u1) ожидание , Т2 (и2) ожидание .

Эту последовательность взаимоблокировок можно избежать, если выполнять строгое обновление идентификатора в каждой транзакции (получение блокировок в том же порядке на том же пути).

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

При включенном уникальном кластеризованном индексе для idключа кластеризации устанавливается исключительная блокировка для защиты записей в данных в строке. Отдельная RIDмонопольная блокировка требуется для защиты записи в textстолбец больших объектов , который по умолчанию хранится на отдельной странице данных.

Когда таблица представляет собой кучу с включенным только некластеризованным индексом id, происходят две вещи. Во-первых, одна RIDисключительная блокировка относится к данным в куче, а другая - это блокировка данных больших объектов, как и раньше. Второй эффект заключается в том, что требуется более сложный план выполнения.

С помощью кластеризованного индекса и простого обновления предиката равенства с одним значением процессор запросов может применить оптимизацию, которая выполняет обновление (чтение и запись) в одном операторе, используя один путь:

Обновление с одним оператором

Строка находится и обновляется в одной операции поиска, требующей только исключительных блокировок (блоки обновлений не требуются). Пример последовательности блокировки с использованием вашей таблицы образцов:

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 

Обратите внимание, что данные больших объектов читаются и записываются в итераторе обновления таблицы. Более сложный план и несколько путей чтения и записи увеличивают вероятность тупиковой ситуации.

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

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

Дополнительные материалы:
общие схемы
взаимоблокировок и взаимоблокировок Bart Duncan.

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

Пол Уайт 9
источник
Отличный ответ. Как вы выводите журналы / трассировку с сообщениями «получение ... блокировки» и «освобождение ссылки блокировки»?
Санджив Дживан