Диагностика взаимоблокировок в SQL Server 2005 - PullRequest
82 голосов
/ 21 августа 2008

Мы видим некоторые пагубные, но редкие тупиковые условия в базе данных Stack Overflow SQL Server 2005.

Я прикрепил профилировщик, настроил профиль трассировки с помощью этой превосходной статьи по устранению тупиковых ситуаций и собрал кучу примеров. Странно то, что запись с блокировкой равна всегда то же самое :

UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0

Другое взаимоблокирующее утверждение варьируется, но обычно это просто тривиальное, простое чтение таблицы сообщений. Этого всегда убивают в тупике. Вот пример

SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount], 
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId], 
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0

Чтобы быть совершенно ясным, мы не видим тупиковые ситуации записи / записи, а читаем / пишем.

На данный момент у нас есть смесь запросов LINQ и параметризованного SQL. Мы добавили with (nolock) ко всем запросам SQL. Это, возможно, помогло некоторым. У нас также был один (очень) плохо написанный запрос на значок, который я исправил вчера, который каждый раз занимал более 20 секунд и выполнялся каждую минуту, вдобавок к этому. Я надеялся, что это было источником некоторых проблем с блокировкой!

К сожалению, я получил еще одну ошибку взаимоблокировки около 2 часов назад. Точно такие же симптомы, точно такой же виновник.

Действительно странная вещь заключается в том, что оператор SQL с блокировкой записи, который вы видите выше, является частью очень специфического пути кода. Он только выполняется при добавлении нового ответа на вопрос - он обновляет родительский вопрос новым счетчиком ответов и последней датой / пользователем. Это, очевидно, не так часто по сравнению с огромным количеством операций чтения, которые мы делаем! Насколько я могу судить, мы не выполняем огромное количество операций записи в любом месте приложения.

Я понимаю, что NOLOCK - своего рода гигантский молот, но большинство запросов, которые мы здесь выполняем, не должны быть такими точными. Вас не волнует, устарел ли ваш профиль пользователя на несколько секунд?

Использование NOLOCK с Linq немного сложнее, поскольку Скотт Хансельман обсуждает здесь .

Мы заигрываем с идеей использования

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

в базовом контексте базы данных, чтобы все наши запросы LINQ имели этот набор. Без этого нам пришлось бы обернуть все вызовы LINQ, которые мы делаем (ну, в общем, простые считывающие вызовы, а это подавляющее большинство из них), в блок кода транзакции из 3-4 строк, что ужасно.

Полагаю, я немного разочарован тем, что тривиальные операции чтения в SQL 2005 могут привести к тупиковой ситуации при записи. Я мог видеть тупики записи / записи, являющиеся огромной проблемой, но читает? У нас здесь нет банковского сайта, нам не нужна идеальная точность каждый раз.

Идеи? Мысли?


Вы создаете новый объект LINQ to SQL DataContext для каждой операции или, возможно, у вас общий статический контекст для всех ваших вызовов?

Джереми, мы разделяем один статический текст данных в основном контроллере по большей части:

private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request's controllers.
/// </summary>
public DBContext DB
{
    get
    {
        if (_db == null)
        {
            _db = new DBContext() { SessionName = GetType().Name };
            //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
        }
        return _db;
    }
}

Рекомендуете ли мы создавать новый контекст для каждого Контроллера, или для Страницы, или ... чаще?

Ответы [ 22 ]

3 голосов
/ 22 августа 2008

Установка по умолчанию для чтения незафиксированных не является хорошей идеей. Вы, несомненно, внесете несоответствия и получите проблему, которая хуже, чем у вас сейчас. Изоляция моментальных снимков может работать хорошо, но это радикальное изменение в работе Sql Server, которое создает огромную нагрузку 1002 * для базы данных tempdb.

Вот что вы должны сделать: используйте try-catch (в T-SQL), чтобы обнаружить условие взаимоблокировки. Когда это произойдет, просто повторите запрос. Это стандартная практика программирования баз данных.

Хорошие примеры этой техники есть в Библии Пола Нильсона Sql Server 2005 .

Вот быстрый шаблон, который я использую:

-- Deadlock retry template

declare @lastError int;
declare @numErrors int;

set @numErrors = 0;

LockTimeoutRetry:

begin try;

-- The query goes here

return; -- this is the normal end of the procedure

end try begin catch
    set @lastError=@@error
    if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock
    begin;
        if @numErrors >= 3 -- We hit the retry limit
        begin;
            raiserror('Could not get a lock after 3 attempts', 16, 1);
            return -100;
        end;

        -- Wait and then try the transaction again
        waitfor delay '00:00:00.25';
        set @numErrors = @numErrors + 1;
        goto LockTimeoutRetry;

    end;

    -- Some other error occurred
    declare @errorMessage nvarchar(4000), @errorSeverity int
    select    @errorMessage = error_message(),
            @errorSeverity = error_severity()

    raiserror(@errorMessage, @errorSeverity, 1)

    return -100
end catch;    
3 голосов
/ 21 августа 2008

@ Джефф - Я определенно не эксперт в этом, но у меня были хорошие результаты в создании нового контекста почти при каждом вызове. Я думаю, что это похоже на создание нового объекта Connection при каждом вызове с ADO. Затраты не так плохи, как вы думаете, так как пул соединений все равно будет использоваться.

Я просто использую глобальный статический помощник, подобный этому:

public static class AppData
{
    /// <summary>
    /// Gets a new database context
    /// </summary>
    public static CoreDataContext DB
    {
        get
        {
            var dataContext = new CoreDataContext
            {
                DeferredLoadingEnabled = true
            };
            return dataContext;
        }
    }
}

и затем я делаю что-то вроде этого:

var db = AppData.DB;

var results = from p in db.Posts where p.ID = id select p;

И я бы сделал то же самое для обновлений. В любом случае, у меня не так много трафика, как у вас, но я определенно получал некоторую блокировку, когда я использовал общий DataContext на раннем этапе с небольшой группой пользователей. Гарантий нет, но стоит попробовать.

Обновление : Опять же, просматривая свой код, вы делитесь контекстом данных только на время жизни этого конкретного экземпляра контроллера, что в принципе кажется нормальным, если только он каким-то образом не используется одновременно множественными вызовами внутри контроллер. В теме по теме СкоттГу сказал:

Контроллеры живут только для одного запроса - поэтому в конце обработки запроса они собираются сборщиком мусора (что означает сбор данных DataContext) ...

Так или иначе, это может быть не так, но опять же, вероятно, стоит попробовать, возможно, в сочетании с некоторым нагрузочным тестированием.

2 голосов
/ 21 августа 2008

В прошлом у меня работало то, что все мои запросы и обновления обращаются к ресурсам (таблицам) в одном и том же порядке.

То есть, если один запрос обновляет в порядке Table1, Table2, а другой запрос обновляет его в порядке Table2, Table1, то вы можете увидеть тупики.

Не уверен, что вы можете изменить порядок обновлений, поскольку вы используете LINQ. Но на это нужно взглянуть.

1 голос
/ 21 августа 2008

Вас волнует, устарел ли ваш профиль пользователя на несколько секунд?

Несколько секунд определенно будет приемлемым. В любом случае, похоже, что это будет не так долго, если только огромное количество людей не представят ответы одновременно.

1 голос
/ 25 августа 2008

Так в чем же проблема с реализацией механизма повторных попыток? Всегда будет вероятность возникновения тупиковой ситуации, так почему бы не иметь логики для ее идентификации и просто попробовать еще раз?

Не приведут ли, по крайней мере, некоторые другие опции к штрафам за производительность, которые применяются постоянно, когда система повторных попыток срабатывает редко?

Кроме того, не забывайте регистрировать записи, когда происходит повторная попытка, чтобы вы не попали в такую ​​редкую ситуацию.

1 голос
/ 21 августа 2008

Вы должны реализовать грязное чтение.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

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

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

У нас здесь нет банковского сайта, нам не нужна идеальная точность каждый раз

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

Без этого нам пришлось бы обернуть каждый выполняемый нами вызов LINQ (ну, в общем, простые считывающие, а это подавляющее большинство из них) в блок кода транзакции из 3-4 строк, что ужасно

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

1 голос
/ 21 августа 2008

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

Вот шаблон, который мы использовали для Video.Show ( ссылка на представление источника в CodePlex ):

using System.Configuration;
namespace VideoShow.Data
{
  public class DataContextFactory
  {
    public static VideoShowDataContext DataContext()
    {
        return new VideoShowDataContext(ConfigurationManager.ConnectionStrings["VideoShowConnectionString"].ConnectionString);
    }
    public static VideoShowDataContext DataContext(string connectionString)
    {
        return new VideoShowDataContext(connectionString);
    }
  }
}

Затем на уровне обслуживания (или даже более детально, для обновлений):

private VideoShowDataContext dataContext = DataContextFactory.DataContext();

public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType)
{
  var videos =
  from video in DataContext.Videos
  where video.StatusId == (int)VideoServices.VideoStatus.Complete
  orderby video.DatePublished descending
  select video;
  return GetSearchResult(videos, pageSize, pageNumber);
}
1 голос
/ 21 августа 2008

Я согласен с Джереми в этом. Вы спрашиваете, следует ли вам создавать новый контекст данных для каждого контроллера или для каждой страницы - я стремлюсь создать новый контекст для каждого независимого запроса.

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

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

Конечно, я испытывал некоторое временное давление, поэтому пытался делать несколько вещей одновременно, поэтому я не могу быть на 100% уверен, что это то, что исправило это, но у меня высокий уровень уверенности - давайте поставим это так.

0 голосов
/ 28 октября 2008

Была такая же проблема, и не удалось использовать «IsolationLevel = IsolationLevel.ReadUncommitted» на TransactionScope, поскольку на сервере не включен DTS (!).

То, что я сделал с методом расширения:

public static void SetNoLock(this MyDataContext myDS)
{
    myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}

Итак, для избранных, которые используют критические таблицы параллелизма, мы включаем «nolock» следующим образом:

using (MyDataContext myDS = new MyDataContext())
{
   myDS.SetNoLock();

   //  var query = from ...my dirty querys here...
}

Приветствия приветствуются!

0 голосов
/ 11 октября 2008

Я бы продолжил настраивать все; как работает дисковая подсистема? Какова средняя длина очереди диска? Если операции ввода-вывода резервируются, реальной проблемой могут быть не эти два взаимоблокировочных запроса, а другой запрос, который ставит узкие места в системе; Вы упомянули запрос, который занял 20 секунд, которые были настроены, есть другие?

Сосредоточьтесь на сокращении длительных запросов, держу пари, что проблемы взаимоблокировки исчезнут.

...