обновить и вставить запросы, создающие тупик - PullRequest
13 голосов
/ 02 сентября 2011

Я постараюсь объяснить мою проблему как можно более подробно, и я был бы признателен за любую помощь / предложение.Моя проблема касается взаимоблокировки, вызванной двумя запросами (одна вставка и одно обновление).Я использую MS-SQL server 2008

У меня есть два приложения, использующих одну и ту же базу данных:

  1. Веб-приложение (при каждом запросе несколько записей вставляются в таблицу Impressions путем вызовахранимая процедура)
  2. Служба Windows (вычисляет все показы, выполненные за одну минуту, каждую минуту, за предыдущую минуту и ​​устанавливает флажок для каждого из показов, рассчитанных также с помощью хранимой процедуры)

Веб-приложение вставляет записи показов без использования транзакции, в то время как приложение-служба Windows вычисляет показы, используя транзакцию IsolationLevel.ReadUncommitted.Хранимая процедура в приложении службы Windows работает примерно так:

Хранимая процедура службы Windows:

Повторяет все показы, для которых для флага isCalculated установлено значениеfalse и date <@now, увеличивает счетчик и другие данные в другой таблице, связанной с таблицей показов, и устанавливает для флага <code>isCalculated значение true для показов с датой <@now.Поскольку эта хранимая процедура довольно велика, вставлять ее нет смысла, вот сокращенный фрагмент кода того, что делает процесс: </p>

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-сервера, можно найти в нижней части этого вопроса

Теоретически эти две хранимые процедуры должны работать вместе, поскольку первая вставляет записи одну за другой с date = DateTime.Now, а вторая вычисляет показы с датой

Редактировать:

Вот код, запускаемый в приложении службы 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();
}

РЕДАКТИРОВАТЬ:

Я добавляюd индексы обеих таблиц, предложенные Мартином Смитом, выглядят следующим образом:

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

Пока без исключений, сообщу позже

Изменить:

К сожалению, это не решило проблему тупика.Я начну трассировку взаимоблокировки в профилировщике, чтобы проверить, не являются ли взаимоблокировки такими же, как раньше.

Редактировать:

Вставить новую трассировку (для меня она выглядит так же, как и предыдущая),не удалось сделать снимок экрана плана выполнения (он слишком большой), но - это xml из плана выполнения . А вот снимок экрана с планом выполнения запроса на вставку:

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 &amp;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>

Редактировать:

После предложений Джонатана Дикинсона:

  1. Я изменил хранимую процедуру (убрал курсор),
  2. Я изменил IDX_Impressions_isCharged_showTime, чтобы не разрешать PAGE_LOCKS, и
  3. Я добавил -1 секунду к свойству @now в приложении службы Windows, чтобы избежать пограничных случаев тупиковой ситуации.

Обновление:

Время выполнения запроса было уменьшено после последних изменений, но число исключений не изменилось.

Надеемся, последнее обновление:

Изменения, предложенные Мартином Смитом, теперь действительны, запрос вставки теперь использует некластеризованный индекс, и теоретически это должно решить проблему.На данный момент исключений не поступало (скрестив пальцы)

Ответы [ 3 ]

4 голосов
/ 05 сентября 2011

Избегайте курсоров, в этом запросе они не нужны.SQL не является императивным языком (, поэтому он получает дурную славу, потому что все используют его как один ) - это заданный язык.

Первое, что вы можетеЭто ускоряет базовое выполнение вашего SQL, меньше времени разбора / выполнения запроса означает меньше шансов на тупик:

  • Префикс всех ваших таблиц с [dbo] - это сокращает до 30%этап синтаксического анализа.
  • Псевдоним ваших таблиц - он отсекает небольшую сумму от этапа планирования.
  • Цитирование идентификаторов может ускорить процесс.
  • Это советы бывшего эксперта по SQL, прежде чем кто-либо решит оспорить его.

Вы можете использовать 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

Это будетпросто уменьшить вероятность тупика.Вы можете попытаться ограничить @now датой в прошлом (т.е. today - 1 day), чтобы убедиться, что вставленная строка не попадает в предикат обновления;скорее всего, это полностью предотвратит тупик.

2 голосов
/ 02 сентября 2011

Ваш курсор службы Windows обновляет различные строки в Daily, для которых требуется X блокировки. Они не будут выпущены до завершения транзакции.

Затем ваше веб-приложение выполняет вставку в Impressions и сохраняет блокировку X для вновь вставленной строки, пока оно ожидает блокировки S для одной из строк в Daily, которые заблокированы другой процесс. Он должен прочитать это, чтобы проверить ограничение FK.

Затем служба Windows обновляет Impressions, принимая U блокировки строк, которые она сканирует по пути. Нет индекса, который позволял бы выполнять поиск в строках, поэтому этот просмотр включает строку, добавленную веб-приложением.

Итак

(1) Вы можете добавить составной индекс к Impressions на showTime, isCharged или наоборот (проверить планы выполнения), чтобы позволить строкам, которые обновит служба Windows, быть найденными при поиске индекса, а не полное сканирование.

-или

(2) Вы можете добавить избыточный некластеризованный индекс на Daily(DailyId). Это будет намного уже, чем кластеризованный, поэтому проверка FK, скорее всего, будет использовать его вместо необходимости блокировки S в строке кластеризованного индекса.

Редактировать

Отказ от ответственности: нижеследующее основано на предположениях и наблюдениях, а не на том, что я нашел документированным!

Кажется, что идея (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)    

Plan

0 голосов
/ 05 сентября 2011

Я уверен, что изменения, предложенные другими ответами, требуются, поскольку, например, использование курсора в вашем случае не является необходимым ... из предоставленного вами кода даже нет необходимости в 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

...