Обновлять и вставлять запросы, создавая тупик
Я попытаюсь объяснить мою проблему как можно более подробно, и я был бы признателен за любую помощь/предложение. Моя проблема заключается в том, что тупик вызван двумя запросами (одна вставка и одно обновление). Я использую сервер MS-SQL 2008
У меня есть два приложения, использующие одну и ту же базу данных:
- Веб-приложение (по каждому запросу несколько записей вставляются в таблицу Impressions, вызывая хранимую процедуру)
- Служба Windows (вычисляет все показы, выполненные за одну минуту, каждую минуту, в течение предыдущей минуты и устанавливает флаг для каждого из показов, вычисленных с помощью хранимой процедуры)
Веб-приложение вставляет записи показов без использования транзакции, в то время как приложение службы Windows рассчитывает показы при использовании транзакции IsolationLevel.ReadUncommitted
. Хранимая процедура в приложении Windows Service делает примерно следующее:
Хранимая процедура службы Windows:
Циклы через все показы, для которых флаг isCalculated
установлен в значение false, а date < @now, увеличивает счетчик и другие данные в другой таблице, связанной с таблицей показов, и устанавливает флаг isCalculated
равным true для показов, которые имеют дату < @Теперь. Поскольку эта хранимая процедура довольно большая, нет смысла вставлять ее, вот укороченный фрагмент кода того, что делает proc:
DECLARE @nowTime datetime = convert(datetime, @now, 21)
DECLARE dailyCursor CURSOR FOR
SELECT Daily.dailyId,
Daily.spentDaily,
Daily.impressionsCountCache ,
SUM(Impressions.amountCharged) as sumCharged,
COUNT(Impressions.impressionId) as countImpressions
FROM Daily INNER JOIN Impressions on Impressions.dailyId = Daily.dailyId
WHERE Impressions.isCharged=0 AND Impressions.showTime < @nowTime AND Daily.isActive = 1
GROUP BY Daily.dailyId, Daily.spentDaily, Daily.impressionsCountCache
OPEN dailyCursor
DECLARE @dailyId int,
@spentDaily decimal(18,6),
@impressionsCountCache int,
@sumCharged decimal(18,6),
@countImpressions int
FETCH NEXT FROM dailyCursor INTO @dailyId,@spentDaily, @impressionsCountCache, @sumCharged, @countImpressions
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE Daily
SET spentDaily= @spentDaily + @sumCharged,
impressionsCountCache = @impressionsCountCache + @countImpressions
WHERE dailyId = @dailyId
FETCH NEXT FROM dailyCursor INTO @dailyId,@spentDaily, @impressionsCountCache, @sumCharged, @countImpressions
END
CLOSE dailyCursor
DEALLOCATE dailyCursor
UPDATE Impressions
SET isCharged=1
WHERE showTime < @nowTime AND isCharged=0
Сохраненная процедура веб-приложения:
Эта процедура довольно проста, она просто вставляет запись в таблицу. Ниже приведен сокращенный фрагмент кода:
INSERT INTO Impressions
(dailyId, date, pageUrl,isCalculated) VALUES
(@dailyId, @date, @pageUrl, 0)
Код
Код, который вызывает эти хранимые процедуры, довольно просто, он просто создает команды SQL, передающие необходимые параметры и выполняющие их
//i send the date like this
string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff",
CultureInfo.InvariantCulture);
SqlCommand comm = sql.StoredProcedureCommand("storedProcName",
parameters, values);
У меня очень часто возникают взаимоблокировки (исключения происходят в веб-приложении, а не в службе Windows), и после использования SQL-Profiler я обнаружил, что взаимоблокировки, вероятно, происходят из-за этих двух запросов (я У меня большой опыт анализа данных профилировщика.
Последние данные трассировки, собранные из профилировщика SQL-сервера, можно найти в нижней части этого вопроса
В теории эти две хранимые процедуры должны работать вместе, потому что первая вставляет записи один за другим с датой = DateTime.Now, а вторая вычисляет Impressions, которые имеют дату < DateTime.Now.
Edit:
Ниже приведен код запуска приложения Windows:
SQL sql = new SQL();
DateTime endTime = DateTime.Now;
//our custom DAL class that opens a connection
sql.StartTransaction(IsolationLevel.ReadUncommitted);
try
{
List<string> properties = new List<string>() { "now" };
List<string> values = new List<string>() { endTime.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) };
SqlCommand comm = sql.StoredProcedureCommannd("ChargeImpressions", properties, values);
comm.Transaction = sql.Transaction;
ok = sql.CheckExecute(comm);
}
catch (Exception up)
{
ok = false;
throw up;
}
finally
{
if (ok)
sql.CommitTransaction();
else
sql.RollbackTransactions();
CloseConn();
}
EDIT:
Я добавил индексы на обе таблицы, как предложил Мартин Смит следующим образом:
CREATE NONCLUSTERED INDEX [IDX_Daily_DailyId] ON [dbo].[Daily]
(
[daily] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
и
CREATE NONCLUSTERED INDEX [IDX_Impressions_isCharged_showTime] ON [dbo].[Impressions]
(
[isCharged] ASC,
[showTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
Пока нет исключений, отчитается позже
Edit:
К сожалению, это не решило проблему взаимоблокировки. Я запустил бы тупик в профилировщике, чтобы убедиться, что тупики такие же, как и раньше.
Изменить:
Вставить новую трассировку (для меня она выглядит так же, как и предыдущая), не удалось захватить экран плана выполнения (слишком большой), но вот xml из плана выполнения. И вот скриншот плана выполнения запроса insert:
![execution plan of the insert query]()
<deadlock victim="process14e29e748">
<process-list>
<process id="process14e29e748" taskpriority="0" logused="952" waitresource="KEY: 6:72057594045071360 (f473d6a70892)" waittime="4549" ownerId="2507482845" transactionname="INSERT" lasttranstarted="2011-09-05T11:59:16.587" XDES="0x15bef83b0" lockMode="S" schedulerid="1" kpid="2116" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2011-09-05T11:59:16.587" lastbatchcompleted="2011-09-05T11:59:16.587" clientapp=".Net SqlClient Data Provider" hostpid="2200" isolationlevel="snapshot (5)" xactid="2507482845" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="dbo.InsertImpression" line="27" stmtstart="2002" stmtend="2560" sqlhandle="0x03000600550e30512609e200529f00000100000000000000">
INSERT INTO Impressions
(dailyId, languageId, showTime, pageUrl, amountCharged, age, ipAddress, userAgent, portalId, isCharged,isCalculated) VALUES
(@dailyId, @languageId, @showTime, @pageUrl, @amountCharged, @age, @ip, @userAgent, @portalId, 0, 0) </frame>
</executionStack>
<inputbuf>
Proc [Database Id = 6 Object Id = 1362103893] </inputbuf>
</process>
<process id="process6c9dc8" taskpriority="0" logused="335684" waitresource="KEY: 6:72057594045464576 (5fcc21780b69)" waittime="4475" ownerId="2507482712" transactionname="transaction_name" lasttranstarted="2011-09-05T11:59:15.737" XDES="0x1772119b0" lockMode="U" schedulerid="2" kpid="3364" status="suspended" spid="88" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2011-09-05T11:59:15.737" lastbatchcompleted="2011-09-05T11:59:15.737" clientapp=".Net SqlClient Data Provider" hostpid="1436" isolationlevel="read uncommitted (1)" xactid="2507482712" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="dbo.ChargeImpressions" line="60" stmtstart="4906" stmtend="5178" sqlhandle="0x03000600e3c5474f0609e200529f00000100000000000000">
UPDATE Impressions
SET isCharged=1
WHERE showTime &lt; @nowTime AND isCharged=0
</frame>
</executionStack>
<inputbuf>
Proc [Database Id = 6 Object Id = 1330103779] </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594045071360" dbid="6" objectname="dbo.Daily" indexname="PK_Daily" id="lock14c6aab00" mode="X" associatedObjectId="72057594045071360">
<owner-list>
<owner id="process6c9dc8" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process14e29e748" mode="S" requestType="wait"/>
</waiter-list>
</keylock>
<keylock hobtid="72057594045464576" dbid="6" objectname="dbo.Impressions" indexname="IDX_Impressions_isCharged_showTime" id="lock14c901200" mode="X" associatedObjectId="72057594045464576">
<owner-list>
<owner id="process14e29e748" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process6c9dc8" mode="U" requestType="wait"/>
</waiter-list>
</keylock>
</resource-list>
</deadlock>
Изменить:
После предложений Джонатана Дикинсона:
- Я изменил хранимую процедуру (удалил курсор),
- Я изменил IDX_Impressions_isCharged_showTime, чтобы не разрешать PAGE_LOCKS и
- Я добавил -1 секунду в свойство @now в приложении для обслуживания Windows, чтобы избежать случаев сбоев в пограничных тупиках.
Update:
Время выполнения запроса было уменьшено после последних изменений, но количество исключений не было.
Надеюсь, последнее обновление:
Изменения, предложенные Мартином Смитом, теперь живут, запрос на ввод теперь использует некластеризованный индекс, и теоретически это должно решить проблему. На данный момент никаких исключений не сообщалось (скрестив пальцы)
Ответы
Ответ 1
Ваш курсор обслуживания Windows обновляет различные строки в Daily
, для которых требуется блокировка X
. Они не будут выпущены до завершения транзакции.
Затем ваше веб-приложение вставляет в Impressions
и сохраняет блокировку X
во вновь вставленной строке, пока она ждет блокировку S
на одной из строк в Daily
, которые заблокированы другим обработать. Он должен прочитать это, чтобы проверить ограничение FK.
Затем ваша служба Windows выполняет обновление на Impressions
с фиксацией U
на строках, которые он просматривает на этом пути. Нет индекса, который позволяет ему искать строки, поэтому это сканирование включает строку, добавленную веб-приложением.
So
(1) Вы можете добавить составной индекс в Impressions
на showTime, isCharged
или наоборот (проверьте планы выполнения), чтобы разрешить строки, которые служба Windows обновит, с помощью поиска индекса, а не полное сканирование.
-Or
(2) Вы можете добавить избыточный некластеризованный индекс на Daily(DailyId)
. Это будет намного более узким, чем кластеризованное, поэтому проверка FK, скорее всего, будет использовать это, предпочитая блокировку S
в кластерной строке индекса.
Edit
Отказ от ответственности: следующее основано на предположении и наблюдении, а не на том, что я нашел документированным!
Кажется, что идея (2) не работает "как есть". План выполнения показывает, что проверка FK по-прежнему продолжается против кластерного индекса, независимо от того, что теперь доступен более узкий индекс. sys.foreign_keys
имеет столбцы referenced_object_id, key_index_id
, и я предполагаю, что проверка всегда будет выполняться по указанному там индексу, а оптимизатор запросов в настоящее время не рассматривает альтернативы, но не нашел ничего документирующего этого.
Я обнаружил, что соответствующие значения в sys.foreign_keys
и план запроса изменились, чтобы начать использовать более узкий индекс после того, как я сбросил и повторно добавил ограничение внешнего ключа.
CREATE TABLE Daily(
DailyId INT IDENTITY(1,1) PRIMARY KEY CLUSTERED NOT NULL,
Filler CHAR(4000) NULL,
)
INSERT INTO Daily VALUES ('');
CREATE TABLE Impressions(
ImpressionId INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
DailyId INT NOT NULL CONSTRAINT FK REFERENCES Daily (DailyId),
Filler CHAR(4000) NULL,
)
/*Execution Plan uses clustered index - There is no NCI*/
INSERT INTO Impressions VALUES (1,1)
ALTER TABLE Daily ADD CONSTRAINT
UQ_Daily UNIQUE NONCLUSTERED(DailyId)
/*Execution Plan still use clustered index even after NCI created*/
INSERT INTO Impressions VALUES (1,1)
ALTER TABLE Impressions DROP CONSTRAINT FK
ALTER TABLE Impressions WITH CHECK ADD CONSTRAINT FK FOREIGN KEY(DailyId)
REFERENCES Daily (DailyId)
/*Now Execution Plan now uses non clustered index*/
INSERT INTO Impressions VALUES (1,1)
![План]()
Ответ 2
Избегайте курсоров, этот запрос им не нужен. SQL является не обязательным языком (поэтому он получает плохое имя, потому что каждый использует его как один) - это язык набора.
Первое, что вы можете сделать, это ускорить базовое выполнение вашего SQL, меньше времени на разбор/выполнение запроса означает меньше шансов на тупик:
- Префикс всех ваших таблиц с
[dbo]
- это сокращает до 30% от этапа анализа.
- Псевдоним ваших таблиц - он отсекает небольшую сумму от этапа планирования.
- Идентификаторы котировок могут ускорить процесс.
- Это советы от ex-SQL-PM, прежде чем кто-либо решит оспаривать его.
Вы можете использовать CTE для получения данных для обновления, а затем использовать оператор UPDATE ... FROM ... SELECT
для выполнения фактических обновлений. Это будет быстрее, чем курсор, потому что курсоры собака медленная по сравнению с чистыми операциями набора (даже самый быстрый "пожарный шланг", похожий на ваш). Меньше времени, затрачиваемого на обновление, означает меньше шансов на тупик. Примечание. У меня нет исходных таблиц, я не могу проверить это, поэтому проверьте его на базе базы данных разработки.
DECLARE @nowTime datetime = convert(datetime, @now, 21);
WITH [DailyAggregates] AS
(
SELECT
[D].[dailyId] AS [dailyId],
[D].[spentDaily] AS [spentDaily],
[D].[impressionsCountCache] AS [impressionsCountCache],
SUM([I].[amountCharged]) as [sumCharged],
COUNT([I].[impressionId]) as [countImpressions]
FROM [dbo].[Daily] AS [D]
INNER JOIN [dbo].[Impressions] AS [I]
ON [I].[dailyId] = [D].[dailyId]
WHERE [I].[isCharged] = 0
AND [I].[showTime] < @nowTime
AND [D].[isActive] = 1
GROUP BY [D].[dailyId], [D].[spentDaily], [D].[impressionsCountCache]
)
UPDATE [dbo].[Daily]
SET [spentDaily] = [A].[spentDaily] + [A].[sumCharged],
[impressionsCountCache] = [A].[impressonsCountCache] + [A].[countImpressions]
FROM [Daily] AS [D]
INNER JOIN [DailyAggregates] AS [A]
ON [D].[dailyId] = [A].[dailyId];
UPDATE [dbo].[Impressions]
SET [isCharged] = 1
WHERE [showTime] < @nowTime
AND [isCharged] = 0;
Кроме того, вы можете запретить блокировку PAGE в вашем индексе, это уменьшит вероятность того, что несколько строк заблокируют целую страницу (из-за блокировки эскалации необходимо заблокировать только определенный процент строк до того, как вся страница будет заблокирована).
CREATE NONCLUSTERED INDEX [IDX_Impressions_isCharged_showTime] ON [dbo].[Impressions]
(
[showTime] ASC, -- I have a hunch that switching these around might have an effect.
[isCharged] ASC
)
WITH (ALLOW_PAGE_LOCKS = OFF)
ON [PRIMARY]
GO
Это просто уменьшит шансы на тупик. Вы можете попытаться ограничить дату @в прошлом (т.е. today - 1 day
), чтобы убедиться, что вставленная строка не попадает в предикат обновления; скорее всего, это полностью предотвратит тупик.
Ответ 3
Я уверен, что изменения, которые предлагают другие ответы, вызваны, поскольку, например, использование курсора в вашем случае не требуется... из кода, который вы указали, даже не нужно использовать WHILE
...
Я не парень SQL Server... Если мне нужно было сделать то, что делает ваша Хранимая процедура, я бы удостоверился, что @nowTime = DateTime.Now.AddSeconds(-1)
и код похож на следующий:
BEGIN
UPDATE Daily D SET
D.spentDaily= D.spentDaily + (SELECT SUM(I.amountCharged) FROM Impressions I WHERE I.isCharged=0 AND I.showTime < @nowTime AND I.DailyId = D.DailyId),
D.impressionsCountCache = D.impressionsCountCache + (SELECT COUNT(I.impressionId) FROM Impressions I WHERE I.isCharged=0 AND I.showTime < @nowTime AND I.DailyId = D.DailyId)
WHERE D.DailyId IN (SELECT I.DailyId FROM Impressions I WHERE I.isCharged=0 AND I.showTime < @nowTime AND I.DailyId = D.DailyId) AND D.isActive = 1;
UPDATE Impressions I SET
I.isCharged=1
WHERE I.showTime < @nowTime AND I.isCharged=0;
COMMIT;
END
Даже при высокой нагрузке никогда не было проблем с тупиком с любым параллельным INSERT
/UPDATE
/DELETE
на Impressions
таким образом (хотя это был Oracle)... HTH