2013-12-21 4 views
4

У меня есть таблица ImportSourceMetadata, которую я использую для управления процессом импорта. Он содержит столбец PK SourceId и столбец данных LastCheckpoint. Процесс пакетного импорта читает LastCheckpoint для данного SourceId, выполняет некоторую логику (на других таблицах), затем обновляет LastCheckpoint для этого SourceIdили вставляет его, если он еще не существует.Почему код SQLERT SQL Server иногда не блокируется?

Несколько экземпляров процесса запускаются в одно и то же время, обычно с отключением SourceIds, и для этих случаев мне нужна высокая параллельность. Однако может случиться, что для одного и того же SourceId запускаются два процесса; в этом случае мне нужны экземпляры, чтобы блокировать друг друга.

Поэтому мой код выглядит следующим образом:

BEGIN TRAN 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED 
SELECT LastCheckpoint FROM ImportSourceMetadata WITH (UPDLOCK) WHERE SourceId = 'Source' 

-- Perform some processing 

-- UPSERT: if the SELECT above yielded no value, then 
INSERT INTO ImportSourceMetadata(SourceId, LastCheckpoint) VALUES ('Source', '2013-12-21') 
-- otherwise, we'd do this: UPDATE ImportSourceMetadata SET LastCheckpoint = '2013-12-21' WHERE SourceId = 'Source' 

COMMIT TRAN 

Я использую транзакцию для достижения атомарности, но я могу использовать только READ COMMITTED уровня изоляции (из-за требования к параллельности в «Выполнить некоторую обработку «блок»). Поэтому (и чтобы избежать взаимоблокировок), я включаю подсказку UPDLOCK с инструкцией SELECT для достижения «критического раздела», параметризованного по значению SourceId.

Теперь это работает довольно хорошо в большинстве случаев, но мне удалось вызвать ошибки нарушения первичного ключа с помощью инструкции INSERT при запуске множества параллельных процессов для того же SourceId с пустой базой данных. Однако я не могу достоверно воспроизвести это, и я не понимаю , почему он не работает.

Я нашел намеки в Интернете (например, here и here, in a comment), что мне нужно указать WITH (UPDLOCK,HOLDLOCK) (соотв. WITH (UPDLOCK,SERIALIZABLE)), а не просто принимать UPDLOCK на SELECT, но я действительно не понимаю, почему это , MSDN документы say,

UPDLOCK
Указывает, что блокировки обновления должны быть приняты и проведены, пока транзакция не будет завершена.

замок обновление, которое принимается и удерживается до завершения транзакции должна быть достаточно, чтобы блокировать последующее INSERT, а на самом деле, когда я пытаюсь его в SQL Server Management Studio, она действительно блокирует мою вставку. Однако в некоторых редких случаях он, похоже, внезапно не работает.

Итак, почему именно UPDLOCK недостаточно, и почему этого достаточно в 99% моих тестовых прогонов (и при имитации его в SQL Server Management Studio)?

Update: Я теперь нашел, что я могу воспроизвести Неблокирующая поведение надежно, выполнив код, указанный выше в двух разных окнах среды SQL Server Management Studio, одновременно до как раз перед INSERT, но только первый после создания базы данных. После этого (хотя я удалил содержимое таблицы ImportSourceMetadata), SELECT WITH (UPDLOCK) действительно блокирует и код больше не будет работать. Действительно, в sys.dm_tran_locks я вижу U-блокировку, даже если строка не существует при последующих тестовых прогонах, но не в первом запуске после создания таблицы.

Это полный пример, чтобы показать разницу в замках между «вновь созданной таблицы» и «старый стол»:

DROP TABLE ImportSourceMetadata 
CREATE TABLE ImportSourceMetadata(SourceId nvarchar(50) PRIMARY KEY, LastCheckpoint datetime) 

BEGIN TRAN 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED 
SELECT LastCheckpoint FROM ImportSourceMetadata WITH (UPDLOCK) WHERE SourceId='Source' 

SELECT * 
FROM sys.dm_tran_locks l 
JOIN sys.partitions p 
ON l.resource_associated_entity_id = p.hobt_id JOIN sys.objects o 
ON p.object_id = o.object_id 

INSERT INTO ImportSourceMetadata VALUES('Source', '2013-12-21') 
ROLLBACK TRAN 

BEGIN TRAN 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED 
SELECT LastCheckpoint FROM ImportSourceMetadata WITH (UPDLOCK) WHERE SourceId='Source' 

SELECT * 
FROM sys.dm_tran_locks l 
JOIN sys.partitions p 
ON l.resource_associated_entity_id = p.hobt_id JOIN sys.objects o 
ON p.object_id = o.object_id 

ROLLBACK TRAN 

В моей системе (с SQL Server 2012), первый запрос показывает нет замков на ImportSourceMetadata, но второй запрос показывает замок KEY на ImportSourceMetadata.

Другими словами, требуется HOLDLOCK, но только если таблица была только что создана. Почему это?

+0

Это код реального T-SQL, который выполняется. Я немного изменил его, чтобы вы могли выполнить его непосредственно в SQL Server Management Studio. –

+0

И теперь я добавил полный образец. –

ответ

3

Вам также понадобится HOLDLOCK.

Если строка существует, то ваш оператор SELECT выведет замок U по крайней мере на эту строку и сохранит ее до конца транзакции.

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

Без HOLDLOCK две параллельные транзакции могут выполнять как SELECT для несуществующей строки. Не сохраняйте конфликтующие блокировки и оба перемещайтесь на INSERT.

Что касается репродукции в вашем вопросе, кажется, что «строка не существует» проблема немного сложнее, чем я думал.

Если строка ранее существовала, но с тех пор была физически удалена, но все еще физически существует на странице как «призрачная» запись, тогда замок U все еще может быть выведен на призрак, объясняя блокировку, которую вы видите.

Вы можете использовать DBCC PAGE, чтобы просмотреть записи о призраках, как в этом небольшом изменении вашего кода.

SET NOCOUNT ON; 

DROP TABLE ImportSourceMetadata 

CREATE TABLE ImportSourceMetadata 
    (
    SourceId  NVARCHAR(50), 
    LastCheckpoint DATETIME, 
    PRIMARY KEY(SourceId) 
) 

BEGIN TRAN 

SET TRANSACTION ISOLATION LEVEL READ COMMITTED 

SELECT LastCheckpoint 
FROM ImportSourceMetadata WITH (UPDLOCK) 
WHERE SourceId = 'Source' 

INSERT INTO ImportSourceMetadata 
VALUES  ('Source', '2013-12-21') 

DECLARE @DBCCPAGE NVARCHAR(100) 

SELECT TOP 1 @DBCCPAGE = 'DBCC PAGE(0,' + CAST(file_id AS VARCHAR) + ',' + CAST(page_id AS VARCHAR) + ',3) WITH NO_INFOMSGS' 
FROM ImportSourceMetadata 
     CROSS APPLY sys.fn_physloccracker(%%physloc%%) 

ROLLBACK TRAN 

DBCC TRACEON(3604) 

EXEC (@DBCCPAGE) 

DBCC TRACEOFF(3604) 

Вкладка ВСС сообщения показывает

Slot 0 Offset 0x60 Length 31 

Record Type = GHOST_DATA_RECORD  Record Attributes = NULL_BITMAP VARIABLE_COLUMNS 
Record Size = 31      
Memory Dump @0x000000001215A060 

0000000000000000: 3c000c00 00000000 9ba20000 02000001 †<.......¢...... 
0000000000000010: 001f0053 006f0075 00720063 006500††††...S.o.u.r.c.e. 

Slot 0 Column 1 Offset 0x13 Length 12 Length (physical) 12 
+0

Твой права, и теперь я смог воспроизвести «HOLDLOCK», требуя воссоздания таблицы. Однако для меня остается загадкой, почему 'SELECT' с' UPDLOCK' _does_ действительно принимает блокировку 'U', когда я запускаю тест во второй раз, хотя я удаляю все из таблицы. Где разница? –

+0

@FabianSchmied - Хорошо, если в таблице нет строк, она не может блокировать 'U' в строке! На каком ресурсе он работает и каково определение вашей таблицы, включая индексы? –

+0

Блокировка 'U' берется в' resource_type' 'KEY'. Таблица имеет кластерный индекс (первичный ключ) в столбце «SourceId». Я добавил полный образец к вопросу выше. –

Смежные вопросы