Вопрос

Мы наблюдаем некоторые пагубные, но редкие ситуации взаимоблокировки в базе данных 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;
    }
}

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

Это было полезно?

Решение

Согласно MSDN:

http://msdn.microsoft.com/en-us/library/ms191242.aspx

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

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

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

Попробуйте установить эту опцию и УДАЛИТЬ все NOLOCK из запросов кода, если это действительно необходимо.NOLOCK или использование глобальных методов в обработчике контекста базы данных для борьбы с уровнями изоляции транзакций базы данных — это решение проблемы.NOLOCKS будет маскировать фундаментальные проблемы с нашим уровнем данных и, возможно, приведет к выбору ненадежных данных, где автоматический выбор/обновление версий строк, по-видимому, является решением.

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

Другие советы

НОЛОК и ЧИТАТЬ БЕЗ СООТВЕТСТВИЯ являются скользким склоном.Никогда не следует использовать их, если сначала не поймете, почему возникает взаимоблокировка.Меня бы обеспокоило, если бы вы сказали: «Мы добавили (nolock) ко всем запросам SQL».Нужно добавить С НОЛОКОМ везде — это верный признак того, что у вас проблемы на уровне данных.

Сам оператор обновления выглядит немного проблематично.Вы определяете счетчик ранее в транзакции или просто извлекаете его из объекта? AnswerCount = AnswerCount+1 когда добавляется вопрос, это, вероятно, лучший способ справиться с этим.Тогда вам не нужна транзакция для получения правильного подсчета, и вам не придется беспокоиться о проблеме параллелизма, которой вы потенциально подвергаете себя.

Один простой способ обойти этот тип проблемы взаимоблокировки без особых усилий и без включения грязного чтения — использовать "Snapshot Isolation Mode" (новое в SQL 2005), которое всегда даст вам чистое чтение последних немодифицированных данных.Вы также можете довольно легко перехватывать и повторять тупиковые операторы, если хотите корректно их обрабатывать.

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

Вероятно, это проблема, связанная с индексом.Например, предположим, что таблица Posts имеет некластеризованный индекс X, который содержит ParentID и одно (или несколько) обновляемых полей (AnswerCount, LastActivityDate, LastActivityUserId).

Взаимная блокировка может возникнуть, если cmd SELECT выполняет блокировку совместного чтения по индексу X для поиска по ParentId, а затем ему необходимо выполнить блокировку совместного чтения в кластерном индексе, чтобы получить оставшиеся столбцы, в то время как cmd UPDATE выполняет исключительную запись. заблокировать кластерный индекс, и для его обновления необходимо получить эксклюзивную блокировку записи для индекса X.

Теперь у вас есть ситуация, когда A заблокировал X и пытается получить Y, тогда как B заблокировал Y и пытается получить X.

Конечно, нам нужно, чтобы ОП обновил свою публикацию, добавив дополнительную информацию о том, какие индексы задействованы, чтобы подтвердить, действительно ли это является причиной.

Мне очень неловко от этого вопроса, и сопровождающий отвечает.Там много «попробуй эту волшебную пыль!»Нет, эта волшебная пыль!»

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

Все, что вы указали, это то, что возникают некоторые блокировки, а не взаимоблокировки.

В SQL 2005 вы можете получить дополнительную информацию о том, какие блокировки снимаются, используя:

DBCC TRACEON (1222, -1)

чтобы при возникновении тупиковой ситуации у вас была более качественная диагностика.

Вы создаете новый объект LINQ to SQL DataContext для каждой операции или, возможно, используете один и тот же статический контекст для всех своих вызовов?Первоначально я попробовал последний подход, и, насколько я помню, он вызвал нежелательную блокировку БД.Теперь я создаю новый контекст для каждой атомарной операции.

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

Помните, что для тупика требуется (как минимум) 2 блокировки.Соединение 1 имеет блокировку A, хочет блокировку B, и наоборот для соединения 2.Это неразрешимая ситуация, и кто-то должен уступить.

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

Я подозреваю, что вы (или LINQ) начинаете транзакцию с этим оператором UPDATE и заранее ВЫБИРАЕТе какую-то другую информацию.Но вам действительно нужно вернуться через граф тупиков, чтобы найти блокировки. держал по каждому потоку, а затем вернуться через Profiler, чтобы найти операторы, которые привели к предоставлению этих блокировок.

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

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

Нет, это вполне приемлемо.Установка уровня изоляции базовой транзакции, вероятно, является лучшим/самым чистым способом.

Типичная тупиковая ситуация чтения/записи возникает из-за доступа к индексному порядку.Чтение (T1) находит строку по индексу A, а затем ищет проецируемый столбец по индексу B (обычно кластеризованный).Запись (T2) изменяет индекс B (кластер), а затем обновляет индекс A.T1 имеет S-Lck на A, хочет S-Lck на B, T2 имеет X-Lck на B, хочет U-Lck на A.Тупик, пыхтение.Т1 убит.Это распространено в средах с интенсивным OLTP-трафиком и слишком большим количеством индексов :).Решение состоит в том, чтобы либо чтение не переходило от A к B (т.включить столбец в A или удалить столбец из прогнозируемого списка) или T2 не нужно переходить из B в A (не обновлять индексированный столбец).К сожалению, linq здесь вам не друг...

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

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

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

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

К.Зачем ты хранишь AnswerCount в Posts стол в первую очередь?

Альтернативный подход – исключить «обратную запись» в Posts таблицу, не сохраняя AnswerCount в таблице, а динамически подсчитывать количество ответов на пост по мере необходимости.

Да, это будет означать, что вы выполняете дополнительный запрос:

SELECT COUNT(*) FROM Answers WHERE post_id = @id

или, что более типично (если вы показываете это для домашней страницы):

SELECT p.post_id, 
     p.<additional post fields>,
     a.AnswerCount
FROM Posts p
    INNER JOIN AnswersCount_view a
    ON <join criteria>
WHERE <home page criteria>

но это обычно приводит к INDEX SCAN и может быть более эффективным в использовании ресурсов, чем использование READ ISOLATION.

Есть несколько способов снять шкуру с кошки.Преждевременная денормализация схемы базы данных может привести к проблемам масштабируемости.

Вы определенно хотите, чтобы READ_COMMITTED_SNAPSHOT был включен, чего нет по умолчанию.Это дает вам семантику MVCC.Это то же самое, что Oracle использует по умолчанию.Наличие базы данных MVCC невероятно полезно, и НЕ использовать ее — безумие.Это позволяет вам запускать внутри транзакции следующее:

Обновить ПОЛЬЗОВАТЕЛЕЙ Set FirstName = 'foobar';//решаем поспать год.

Между тем, не выполняя вышеизложенного, каждый может продолжать выбирать из этой таблицы.Если вы не знакомы с MVCC, вы будете шокированы тем, что когда-либо могли жить без него.Серьезно.

Установка по умолчанию чтения незафиксированных данных — не лучшая идея.Вы, несомненно, внесете несоответствия и в конечном итоге столкнетесь с проблемой, которая хуже, чем та, которая у вас есть сейчас.Изоляция моментальных снимков может работать хорошо, но это радикальное изменение в способе работы Sql Server. огромный загрузить в 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;    

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

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

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

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

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

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

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

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

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

Вам следует реализовать грязное чтение.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

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

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

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

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

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

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

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

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

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

Теперь, когда я вижу ответ Джереми, я думаю, что помню, что слышал, что лучше всего использовать новый 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);
}

Мне пришлось бы согласиться с Грегом при условии, что установка уровня изоляции для чтения незафиксированных данных не будет иметь каких-либо негативных последствий для других запросов.

Мне было бы интересно узнать, Джефф, как установка этого параметра на уровне базы данных повлияет на такой запрос:

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran

Меня ничего не беспокоит, если мой профиль устарел хотя бы на несколько минут.

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

Если реализация «READ UNCOMMITTED» не решает вашу проблему, то трудно помочь, не зная больше об обработке.Возможно, есть какой-то другой вариант настройки, который поможет этому поведению.Если на помощь не придет какой-нибудь гуру MSSQL, я рекомендую сообщить о проблеме поставщику.

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

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

Была та же проблема, и я не мог использовать «IsolationLevel = IsolationLevel.ReadUncommited» в 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...
}

Предложения приветствуются!

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top