У меня есть таблица, которая используется устаревшим приложением в качестве замены IDENTITY
полей в других таблицах.
В каждой строке таблицы хранится последний использованный идентификатор LastID
поля, названного в IDName
.
Иногда хранимый процесс попадает в тупик - я считаю, что я создал соответствующий обработчик ошибок; однако мне интересно посмотреть, работает ли эта методология так, как я думаю, или я лаю здесь не то дерево.
Я вполне уверен, что должен быть способ получить доступ к этой таблице без каких-либо тупиков вообще.
Сама база данных настроена с READ_COMMITTED_SNAPSHOT = 1
.
Во-первых, вот таблица:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
И некластеризованный индекс на IDName
поле:
CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName]
ON [dbo].[tblIDs]
(
[IDName] ASC
)
WITH (
PAD_INDEX = OFF
, STATISTICS_NORECOMPUTE = OFF
, SORT_IN_TEMPDB = OFF
, DROP_EXISTING = OFF
, ONLINE = OFF
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON
, FILLFACTOR = 80
);
GO
Некоторые примеры данных:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
Хранимая процедура используется для обновления значений, хранящихся в таблице, и возврата следующего идентификатора:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Max Vernon
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM tblIDs
WHERE IDName = @IDName),0)+1;
IF (SELECT COUNT(IDName)
FROM tblIDs
WHERE IDName = @IDName) = 0
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID)
ELSE
UPDATE tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
ROLLBACK TRANSACTION;
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
Примеры выполнения хранимого процесса:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
РЕДАКТИРОВАТЬ:
Я добавил новый индекс, поскольку существующий индекс IX_tblIDs_Name не используется SP; Я предполагаю, что обработчик запросов использует кластеризованный индекс, так как ему нужно значение, сохраненное в LastID. В любом случае, этот индекс используется фактическим планом выполнения:
CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID
ON dbo.tblIDs
(
IDName ASC
)
INCLUDE
(
LastID
)
WITH (FILLFACTOR = 100
, ONLINE=ON
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON);
РЕДАКТИРОВАНИЕ № 2:
Я воспользовался советом, который дал @AaronBertrand, и слегка его изменил. Общая идея здесь состоит в том, чтобы уточнить оператор, чтобы устранить ненужную блокировку, и в целом сделать SP более эффективным.
Код ниже заменяет код выше с BEGIN TRANSACTION
на END TRANSACTION
:
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM dbo.tblIDs
WHERE IDName = @IDName), 0) + 1;
IF @NewID = 1
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID);
ELSE
UPDATE dbo.tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
Поскольку наш код никогда не добавляет запись в эту таблицу с 0, LastID
мы можем сделать предположение, что если @NewID равен 1, то мы намерены добавить новый идентификатор в список, в противном случае мы обновляем существующую строку в списке.
источник
SERIALIZABLE
здесь.Ответы:
Во-первых, я бы не стал совершать поездки в базу данных для каждого значения. Например, если ваше приложение знает, что ему нужно 20 новых идентификаторов, не совершайте 20 циклов. Сделайте только один вызов хранимой процедуры и увеличьте счетчик на 20. Также может быть лучше разделить вашу таблицу на несколько.
Можно вообще избежать тупиков. У меня вообще нет тупиков в моей системе. Есть несколько способов сделать это. Я покажу, как бы я использовал sp_getapplock для устранения тупиков. Я понятия не имею, будет ли это работать для вас, потому что SQL Server является закрытым исходным кодом, поэтому я не могу видеть исходный код, и поэтому я не знаю, проверил ли я все возможные случаи.
Ниже описано, что работает для меня. YMMV.
Во-первых, давайте начнем со сценария, в котором мы всегда получаем значительное количество тупиков. Во-вторых, мы будем использовать sp_getapplock для их устранения. Наиболее важным моментом здесь является стресс-тест вашего решения. Ваше решение может быть другим, но вам нужно выставить его на высокий уровень параллелизма, как я покажу позже.
Предпосылки
Давайте создадим таблицу с некоторыми тестовыми данными:
Следующие две процедуры могут оказаться в тупике:
Воспроизведение тупиков
Следующие циклы должны воспроизводить более 20 взаимоблокировок при каждом их запуске. Если вы получите меньше 20, увеличьте количество итераций.
В одной вкладке запустите это;
В другой вкладке запустите этот скрипт.
Убедитесь, что вы запускаете оба в течение нескольких секунд.
Использование sp_getapplock для устранения тупиков
Измените обе процедуры, перезапустите цикл и увидите, что у вас больше нет тупиков:
Использование таблицы с одной строкой для устранения тупиков
Вместо вызова sp_getapplock, мы можем изменить следующую таблицу:
После того, как эта таблица создана и заполнена, мы можем заменить следующую строку
с этим, в обеих процедурах:
Вы можете повторно запустить стресс-тест и убедиться, что у нас нет тупиков.
Вывод
Как мы уже видели, sp_getapplock может использоваться для сериализации доступа к другим ресурсам. Как таковой он может быть использован для устранения тупиков.
Конечно, это может значительно замедлить модификации. Чтобы решить эту проблему, нам нужно выбрать правильную гранулярность для монопольной блокировки и, когда это возможно, работать с наборами вместо отдельных строк.
Прежде чем использовать этот подход, вам необходимо провести стресс-тестирование самостоятельно. Во-первых, вам нужно убедиться, что вы получите как минимум пару дюжин тупиковых ситуаций с вашим оригинальным подходом. Во-вторых, вы не должны получать взаимоблокировки при повторном запуске того же сценария repro с использованием измененной хранимой процедуры.
В целом, я не думаю, что есть хороший способ определить, защищен ли ваш T-SQL от тупиков, просто взглянув на него или взглянув на план выполнения. IMO единственный способ определить, склонен ли ваш код к взаимоблокировкам, - это подвергнуть его высокому параллелизму.
Удачи в устранении тупиков! У нас вообще нет тупиков в нашей системе, что отлично подходит для баланса между работой и личной жизнью.
источник
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
предотвращает взаимные блокировки?Использование
XLOCK
подсказки на вашемSELECT
подходе или на следующемUPDATE
должно быть защищено от этого типа тупика:Вернется с парой других вариантов (если не побежден!).
источник
XLOCK
то, что существующий счетчик не будет обновляться из нескольких подключений, вам не нужно,TABLOCKX
чтобы несколько соединений не добавляли один и тот же новый счетчик?Майк Дефер показал мне элегантный способ сделать это очень легким способом:
(Для полноты вот таблица, связанная с сохраненным процессом)
Это план выполнения для последней версии:
И это план выполнения для оригинальной версии (возможна взаимоблокировка):
Очевидно, что новая версия выигрывает!
Для сравнения, промежуточная версия с
(XLOCK)
etc, выдает следующий план:Я бы сказал, что это победа! Спасибо всем за помощь!
источник
SERIALIZABLE
не существует, чтобы предотвратить фантомы. Он существует для обеспечения сериализуемой семантики изоляции , то есть того же постоянного воздействия на базу данных, как если бы соответствующие транзакции выполнялись последовательно в некотором неопределенном порядке.Не для того, чтобы украсть гром Марка Стори-Смита, но он на что-то со своим постом выше (который, кстати, получил наибольшее количество голосов). Совет, который я дал Максу, был сосредоточен вокруг конструкции «UPDATE set @variable = column = column + value», которую я нахожу действительно классной, но я думаю, что она может быть недокументирована (хотя и должна поддерживаться, хотя и предназначена специально для TCP тесты).
Вот вариант ответа Марка - поскольку вы возвращаете новое значение идентификатора в виде набора записей, вы можете полностью отказаться от скалярной переменной, явной транзакции также не требуется, и я согласен, что возиться с уровнями изоляции не нужно также. Результат очень чистый и довольно гладкий ...
источник
Я исправил подобный тупик в системе в прошлом году, изменив это:
К этому:
В общем, выбор
COUNT
просто определить наличие или отсутствие довольно расточительно. В этом случае, так как это либо 0, либо 1, это не значит, что это большая работа, но (а) эта привычка может распространиться на другие случаи, когда она будет намного дороже (в этих случаях использоватьIF NOT EXISTS
вместоIF COUNT() = 0
), и (б) дополнительное сканирование совершенно не нужно. ПоUPDATE
сути, выполняет ту же проверку.Кроме того, это выглядит как серьезный запах кода для меня:
Какой смысл здесь? Почему бы просто не использовать столбец идентификации или получить эту последовательность, используя
ROW_NUMBER()
во время запроса?источник
IDENTITY
. Эта таблица поддерживает некоторый унаследованный код, написанный в MS Access, который может быть использован для модернизации.SET @NewID=
Линия просто увеличивает значение , сохраненное в таблице для данного ID (но вы уже знаете , что). Можете ли вы рассказать о том, как я мог бы использоватьROW_NUMBER()
?LastID
самом деле означает в вашей модели. Какова его цель? Название не совсем понятно. Как Access использует это?GetNextID('WhatevertheIDFieldIsCalled')
чтобы получить следующий идентификатор для использования, затем вставляет его в новую строку вместе с любыми необходимыми данными.