Технологические вопросы крупных внедрений
05.09.2023

Анализ и устранение взаимоблокировок

Взаимоблокировка - это неразрешимый конфликт блокировок, возникающий при работе двух или более сессий "1С:Предприятия".

Для понимания взаимоблокировок необходимо знать, что такое блокировка и ожидание на блокировке. Подробная информация по этой теме содержится в статье базы знаний "Блокировки данных в 1С:Предприятии 8". Рекомендуется предварительно ознакомиться с этим материалом для лучшего понимания текста настоящей статьи.

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

Такой конфликт не может быть разрешен без участия внешних (по отношению к обеим сессиям) механизмов. Подобные механизмы реализованы во всех СУБД, используемых "1С:Предприятием", а также в самом сервере "1С:Предприятия" (для обнаружения неразрешимых конфликтов управляемых блокировок). Будем называть эти механизмы "менеджерами взаимоблокировок".

Менеджер взаимоблокировок работает следующим образом:

Следует учитывать, что во взаимоблокировках могут участвовать несколько (более двух) сессий "1С:Предприятия". Причем одна и та же причина может приводить к возникновению разных (по конфигурации и по сложности) взаимоблокировок с различным количеством участников и блокировок между ними. Поэтому при анализе большого количества взаимоблокировок в системе всегда имеет смысл начинать с самых простых взаимоблокировок – с минимальным количеством участников и с самыми простыми схемами. Высока вероятность того, что, устранив одну простую взаимоблокировку вы одновременно устраните несколько других – в том числе более сложные.

Другой важной особенностью взаимоблокировок является тот факт, что взаимоблокировка почти всегда "распределена" по нескольким различным строкам кода конфигурации. При этом на начальном этапе анализа пользователю известна только одна строка кода. Это может быть, например, последняя строка, выполненная "жертвой" перед тем, как ее транзакция была отменена менеджером взаимоблокировок (эта строка доступна в информации об ошибке) или строка кода конфигурации, которую выдал ЦУП в дереве аналитической информации.

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

Известны следующие типичные причины возникновения взаимоблокировок в системах на платформе "1С:Предприятие 8":

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

Следует заметить, что взаимоблокировки не всегда поддаются анализу в том случае, если нам известна только одна строка кода – та, в которой было получено сообщение об ошибке взаимоблокировки. Как правило, для анализа взаимоблокировки необходимо использовать подробную информацию, предоставляемую "Центром управления производительностью". Однако для понимания информации ЦУП необходимо знать основные схемы взаимоблокировок и причины, по которым они возникают. Поэтому рекомендуется сначала освоить материал следующих четырех разделов, а затем, если понадобится, переходить к анализу информации, предоставляемой ЦУП.

Текущая версия ЦУП показывает схемы взаимоблокировок только в том случае, если исследуемая информационная база работает с использованием MS SQL Server 2005.

Повышение уровня блокировки ресурса в рамках одной транзакции

Общие сведения

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

Схема возникновения взаимоблокировки такова. Две конкурирующие транзакции (Т1 и Т2) читают один и тот же ресурс – Р1. При этом устанавливаются разделяемые блокировки на этот ресурс. Разделяемые блокировки от конкурирующих транзакций могут существовать одновременно, поэтому к моменту времени t1 обе транзакции успешно установили свои блокировки и продолжают работу.

Затем транзакция Т1 изменяет ресурс Р1 и пытается его записать. При этом устанавливается эксклюзивная блокировка на этот ресурс. Однако эта блокировка не может быть установлена одновременно с разделяемой блокировкой от транзакции Т2, поэтому она устанавливается в состояние ожидания. Транзакция Т1 прекращает работу и ждет, пока будет снята разделяемая блокировка, установленная транзакцией Т2.

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

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

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

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

Применительно к данному случаю перед чтением ресурса необходимо было заблокировать его в эксклюзивном режиме. Тогда картина изменилась бы следующим образом:

В данном случае обе транзакции, перед тем как читать ресурс Р1, устанавливают на него эксклюзивную блокировку. Одна из транзакций (Т1) успевает сделать это первой. К моменту времени t1 ресурс Р1 оказывается заблокированным в эксклюзивном режиме и транзакция Т2, которая не может установить свою блокировку, становится в ожидание на блокировке.

Транзакция Т1 продолжает работу - считывает и записывает ресурс Р1. К моменту времени t2 транзакция Т1 завершается, все установленные ею блокировки автоматически снимаются. После этого начинает работать транзакция Т2 и так же успешно выполняет все свои действия.

Особенности взаимоблокировок данного вида

Взаимоблокировки данного вида проявляются, как правило, при конкурентном выполнении однотипных действий (например, несколько пользователей вводят документы одного вида - РеализацияТоваровУслуг). Для анализа таких взаимоблокировок обычно достаточно знать, в какой строке кода было получено сообщение об ошибке взаимоблокировки. Исходя из этого можно провести анализ кода конфигурации и решить проблему, не обращаясь к подробной информации о взаимоблокировке, предоставляемой ЦУП.

Если такой анализ не дал результатов, необходимо исследовать взаимоблокировку при помощи "Центра управления производительностью" , который выдаст полную информацию по схеме взаимоблокировки.

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

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

Для возникновения взаимоблокировки данного вида необходимо одновременное выполнение следующих условий:

Автоматический режим. Чтение без опции "ДЛЯ ИЗМЕНЕНИЯ" и последующая запись в рамках одной транзакции

Рассмотрим следующий пример.

Копировать в буфер обмена
// Обработчик проведения документа
//
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
   Если Не ПроверитьОстатки() Тогда
   Отказ = Истина;
   Возврат;
   КонецЕсли; 
   СписатьТовары();
КонецПроцедуры // ОбработкаПроведения() 

Обратите внимание на то, что проведение документа выполняется в неявной транзакции, которая автоматически открывается "1С:Предприятием". Таким образом, весь текст обработчика проведения будет выполнен в рамках одной транзакции.

Функция "ПроверитьОстатки":

Копировать в буфер обмена
// Проверить наличие товаров на складах
//
// Возвращаемое значение:
// Булево – Истина, товаров достаточно
//
Функция ПроверитьОстатки()
   Запрос = Новый Запрос;
   Запрос.УстановитьПараметр("Документ", Ссылка);
   Запрос.Текст = "
      |ВЫБРАТЬ
      | ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
      | СУММА(НеобходимыеТовары.Количество) КАК Необходимо,
      | СУММА(ТоварыНаСкладахОстатки.КоличествоОстаток) КАК Остаток
      |ИЗ
      | РегистрНакопления.ТоварыНаСкладах.Остатки(
      | ,
      | (Склад, Номенклатура) В
      | (ВЫБРАТЬ
      | Документ.Реализация.Товары.Склад,
      | Документ.Реализация.Товары.Номенклатура
      | ИЗ
      | Документ.Реализация.Товары
      | ГДЕ
      | Документ.Реализация.Товары.Ссылка = &Документ)) КАК ТоварыНаСкладахОстатки
      |
      | ЛЕВОЕ СОЕДИНЕНИЕ Документ.Реализация.Товары КАК НеобходимыеТовары
      | ПО НеобходимыеТовары.Ссылка = &Документ
      | И НеобходимыеТовары.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура
      |
      |СГРУППИРОВАТЬ ПО
      | ТоварыНаСкладахОстатки.Номенклатура";
   Результат = Запрос.Выполнить().Выбрать();
   Пока Результат.Следующий() Цикл
      Если Результат.Остаток = null Или Результат.Остаток - Результат.Необходимо < 0 Тогда
         Сообщить("Недостаточно (" + Результат.Остаток + " из " + Результат.Необходимо + ") товара (" + Результат.Номенклатура + ")!");
         Возврат Ложь;
      КонецЕсли;
   КонецЦикла;
   Возврат Истина;
КонецФункции // ПроверитьОстатки()

При выполнении запроса, описанного в данной функции, происходит чтение остатков регистра накопления "ТоварыНаСкладах". При этом автоматически устанавливается разделяемая блокировка остатков списываемых товаров. Эта блокировка не позволит конкурирующей транзакции изменить остатки по данным товарам, но позволит ей считать остатки. То есть чтение является неблокирующим.

Функция "СписатьТовары":

Копировать в буфер обмена
// Списать товары со складов
//
Процедура СписатьТовары()
   Для Каждого ТекСтрокаТовары Из Товары Цикл
      Движение = Движения.ТоварыНаСкладах.Добавить();
      Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
      Движение.Период = Дата;
      Движение.Склад = ТекСтрокаТовары.Склад;
      Движение.Характеристика = ТекСтрокаТовары.Характеристика;
      Движение.Номенклатура = ТекСтрокаТовары.Номенклатура;
      Движение.Количество = ТекСтрокаТовары.Количество;
   КонецЦикла;
   Движения.ТоварыНаСкладах.Записать();
КонецПроцедуры // СписатьТовары()

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

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

Для того чтобы избежать возникновения взаимоблокировки, необходимо сделать чтение остатков блокирующим. Для этого используется опция "ДЛЯ ИЗМЕНЕНИЯ".

Необходимо изменить функцию "ПроверитьОстатки" следующим образом:

Копировать в буфер обмена
// Проверить наличие товаров на складах
//
// Возвращаемое значение:
// Булево – Истина, товаров достаточно
//
Функция ПроверитьОстатки()
      Запрос = Новый Запрос;
      Запрос.УстановитьПараметр("Документ", Ссылка);
      Запрос.Текст = "ВЫБРАТЬ
         | ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
         | СУММА(НеобходимыеТовары.Количество) КАК Необходимо,
         | СУММА(ТоварыНаСкладахОстатки.КоличествоОстаток) КАК Остаток
         |ИЗ
         | РегистрНакопления.ТоварыНаСкладах.Остатки(
         | ,
         | (Склад, Номенклатура) В
         | (ВЫБРАТЬ
         | Документ.Реализация.Товары.Склад,
         | Документ.Реализация.Товары.Номенклатура
         | ИЗ
         | Документ.Реализация.Товары
         | ГДЕ
         | Документ.Реализация.Товары.Ссылка = &Документ)) КАК ТоварыНаСкладахОстатки
         | ЛЕВОЕ СОЕДИНЕНИЕ Документ.Реализация.Товары КАК НеобходимыеТовары
         | ПО (НеобходимыеТовары.Ссылка = &Документ)
         | И (НеобходимыеТовары.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура)
         |
         |СГРУППИРОВАТЬ ПО
         | ТоварыНаСкладахОстатки.Номенклатура
         |         
         |ДЛЯ ИЗМЕНЕНИЯ
         | РегистрНакопления.ТоварыНаСкладах.Остатки";
   Результат = Запрос.Выполнить().Выбрать();
   Пока Результат.Следующий() Цикл
      Если Результат.Остаток = null Или Результат.Остаток - Результат.Необходимо < 0 Тогда
         Сообщить("Недостаточно (" + Результат.Остаток + " из " + Результат.Необходимо + ") товара (" + Результат.Номенклатура + ")!");
         Возврат Ложь;
      КонецЕсли;
   КонецЦикла;
   Возврат Истина;
КонецФункции // ПроверитьОстатки()

Автоматический режим. Чтение в объектной технике и последующая запись

Предположим, что в коде конфигурации определена следующая процедура

Копировать в буфер обмена
Процедура ИзменитьНаименованиеКонтрагента(Кнопка)
   НачатьТранзакцию();
   КонтрагентОбъект = КонтрагентСсылка.ПолучитьОбъект();
   КонтрагентОбъект.Наименование = КонтрагентОбъект.Наименование + " (изменения)";
   КонтрагентОбъект.Записать();
   ЗафиксироватьТранзакцию();
КонецПроцедуры

В транзакции считывается элемент справочника "Контрагенты". Чтение реализовано в объектной технике. При выполнении этой строки кода "1С:Предприятие" выполнит запрос к базе данных, который автоматически установит разделяемую блокировку для данного контрагента.

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

Для решения этой проблемы необходимо установить блокировку обновления для данного контрагента перед чтением. Единственный способ сделать это в автоматическом режиме выполнить запрос с опцией "ДЛЯ ИЗМЕНЕНИЯ" аналогично тому, как это было сделано в предыдущем примере.

В этом случае процедура будет выглядеть следующим образом:

Копировать в буфер обмена
  Процедура ИзменитьНаименованиеКонтрагента(Кнопка)
   НачатьТранзакцию();
   Запрос = Новый Запрос;
   Запрос.Текст = "ВЫБРАТЬ
      | Контрагент.Ссылка
      |ИЗ
      | Справочник.Контрагент КАК Контрагент
      |ГДЕ
      | Контрагент.Ссылка = &Ссылка
      |
      |ДЛЯ ИЗМЕНЕНИЯ
      | Справочник.Контрагент";
   Запрос.УстановитьПараметр("Ссылка", КонтрагентСсылка);
   Результат = Запрос.Выполнить(); // Установлена блокировка обновления
   КонтрагентОбъект = КонтрагентСсылка.ПолучитьОбъект();
   КонтрагентОбъект.Наименование = КонтрагентОбъект.Наименование + " (изменения)";
   КонтрагентОбъект.Записать();
   ЗафиксироватьТранзакцию();
КонецПроцедуры

Управляемый режим. Чтение без установки блокировки (либо с установкой разделяемой блокировки) и последующая запись

Если в примере 1 перевести конфигурацию в управляемый режим блокировки данных в транзакции, то опция "ДЛЯ ИЗМЕНЕНИЯ" будет игнорирована "1С:Предприятием", и необходимой эксклюзивной блокировки перед чтением остатков не произойдет. Кроме того, разделяемая блокировка, которая автоматически установится на уровне СУБД, будет снята после завершения выполнения запроса (то есть до конца транзакции). Такая ситуация может привести к нарушению бизнес-логики системы.

Разделяемые блокировки чтения были сняты непосредственно после выполнения запросов на чтение, то есть к моменту времени t1. Последующая запись не блокируется, и взаимоблокировки не происходит. Вместо этого обе транзакции по очереди записывают измененные остатки. Обратите внимание на то, что контроль остатков при этом работает некорректно. Например, на складе было 5 единиц товара. Обе транзакции могут беспрепятственно списать по 5 единиц товара, то есть остатки на складе после выполнения обеих транзакций будут равны -5.

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

Источником этой проблемы является низкий уровень изоляции транзакции на уровне СУБД. Разделяемые блокировки, которые были автоматически установлены в момент чтения остатков, снялись непосредственно после выполнения запроса и остались незащищенными (то есть незаблокированными) до конца транзакции. Отсюда следует необходимость установить такую блокировку, которая бы защищала итоги до окончания транзакции. Для решения таких задач предназначены явные управляемые блокировки, устанавливаемые из кода конфигурации.

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

Для данного примера измененный текст функции должен быть следующим:

Копировать в буфер обмена
Функция ПроверитьОстатки()
   Блокировка = Новый БлокировкаДанных;
   ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
   ЭлементБлокировки.ИсточникДанных = ЭтотОбъект.Товары;
   ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
   ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Склад", "Склад");
   ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
   Блокировка.Заблокировать();
   Запрос = Новый Запрос;
   //… И так далее по тексту функции без изменений

Обратите внимание на то, что для блокировки выбран режим "Исключительный". Это сделано в соответствии со сформулированным выше правилом: необходимо изначально устанавливать блокировку с наивысшим требуемым уровнем изоляции ресурсов.

Если в данном случае установить режим блокировки "Разделяемый", то при одновременной работе нескольких пользователей возможно возникновение взаимоблокировки на уровне "1С:Предприятия".

Захват ресурсов в разном порядке

Общие сведения

Захват ресурсов в разном порядке является второй наиболее распространенной причиной возникновения взаимоблокировок.

  

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

Схема возникновения взаимоблокировки для двух ресурсов такова. В начале две конкурирующие транзакции (Т1 и Т2) захватывают два разных ресурса – Р1 и Р2. Устанавливаемые при этом блокировки не мешают друг другу (так как заблокированы разные ресурсы), поэтому к моменту времени t1 обе транзакции успешно блокируют ресурсы и продолжают работу.

Затем транзакция Т1 пытается заблокировать ресурс Р2. Однако это невозможно, поскольку он уже заблокирован транзакцией Т1. В свою очередь, транзакция Т2 пытается заблокировать ресурс Р2, но это также невозможно, поскольку он уже захвачен транзакцией Т1.

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

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

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

Причина возникновения взаимоблокировки данного вида – разный порядок захвата ресурсов в транзакции. Исходя из этого можно сформулировать следующее правило: ресурсы в транзакции должны блокироваться в одинаковом порядке. Если это по каким-то причинам невозможно (например, противоречит требованиям алгоритмов обработки данных), то следует использовать искусственную блокировку, которая обеспечит нужный порядок. Пример такой блокировки дан ниже в этой статье.

При блокировании ресурсов в одинаковом порядке картина изменится следующим образом:

  
 

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

Транзакция Т1 продолжает работу, выполняет все необходимые действия (при этом, в частности, захватывает ресурс Р2) и завершается. После завершения транзакции все установленные ей блокировки автоматически снимаются и транзакция Т2 продолжает работу. Ей также никто не мешает, и она успешно выполняет все необходимые действия, включая установку блокировок на ресурсы.

Особенности взаимоблокировок данного вида

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

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

Как правило, для анализа взаимоблокировок данного вида необходимо использовать информацию, предоставляемую "Центром управления производительностью".

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

Типичные причины возникновения взаимоблокировок данного вида таковы:

Захват ресурсов без учета порядка

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

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

Приведем пример.

Предположим, что в конфигурации определены два вида документов: "РеализацияТоваров" и "ПоступлениеТоваров". Документы записывают движения в два регистра накопления: "ТоварыНаСкладах" и "ТоварыОрганизаций". При этом движения заполняются и записываются в явном виде в обработчиках проведения документов.

При написании этого кода разработчики не учли порядок захвата ресурсов, в результате чего движения записываются в разном порядке. Например, в документе "РеализацияТоваровУслуг":

Копировать в буфер обмена
// Заполнение таблицы движений по регистру ТоварыНаСкладах
…
Движения.ТоварыНаСкладах.Записать();
//…
// Заполнение таблицы движений по регистру ТоварыОрганизаций
…
Движения.ТоварыОрганизаций.Записать();
При этом в документе "ПоступлениеТоваров" те же движения записываются в обратном порядке:
Копировать в буфер обмена
// Заполнение таблицы движений по регистру ТоварыНаСкладах
…
Движения.ТоварыОрганизаций.Записать();
//…
// Заполнение таблицы движений по регистру ТоварыОрганизаций
…
Движения.ТоварыНаСкладах.Записать();
В этой ситуации возможно возникновение взаимоблокировок при конкурентном вводе документов этих двух видов. Фактически блокировки будут происходить не на таблицах движения этих регистров, а на таблицах остатков.

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

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

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

Захват ресурсов в разном порядке в соответствии с требованиями алгоритмов обработки данных

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

Возможна ситуация, при которой такой порядок записи движений, как в предыдущем примере, применен осознанно в связи с тем, что таков алгоритм обработки информации. Например, документ "РеализацияТоваров" должен сначала записать регистр "ТоварыНаСкладах", затем при помощи запросов к этому и другим регистрам произвести некоторые вычисления и только потом – на основании результатов этих вычислений – сформировать и записать движения регистра "ТоварыОрганизаций". В свою очередь, документ "ПоступлениеТоваров" может, наоборот, вычислять данные для регистра "ТоварыНаСкладах" на основании данных регистра "ТоварыОрганизаций".

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

Например, при работе в автоматическом режиме мы можем выполнить запрос с опцией "ДЛЯ ИЗМЕНЕНИЯ" к виртуальной таблице остатков регистра "ТоварыОрганизаций" в обработчике проведения документа "РеализацияТоваров". В качестве условия в этот запрос следует передать набор значений измерений, который будет изменяться в этой транзакции (то есть выборку из табличной части документа). В этом случае последовательность установки блокировок будет такая же, как в документе "ПоступлениеТоваров": сначала будет блокироваться регистр "ТоварыОрганизаций", а затем "ТоварыНаСкладах". Взаимоблокировка при этом будет исключена.

При работе в управляемом режиме вместо запроса с опцией "ДЛЯ ИЗМЕНЕНИЯ" следует прописать в коде обработчика проведения явную блокировку пространства "РегистрНакопления.ТоварыОрганизаций" по соответствующему набору значений измерений.

Непредсказуемый порядок захвата ресурсов в СУБД при выполнении сложных запросов

При выполнении сложных запросов, включающих соединение нескольких таблиц, не всегда можно точно предсказать, в каком порядке эти таблицы будут обрабатываться СУБД. Этот порядок будет зависеть от выбора плана запроса, а план, в свою очередь, от множества непредсказуемых факторов – распределения данных по индексам, размеров таблиц, нагрузки на систему и т. д.

Иначе говоря, возможны такие ситуации, когда СУБД будет устанавливать блокировки при выполнении одного и того же запроса к нескольким ресурсам в разном порядке. При этом также возможно возникновение взаимоблокировок. Для устранения таких взаимоблокировок необходимо также установить искусственные блокировки на ресурсы в предопределенном порядке.

Неоптимальная работа запроса

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

Ошибка блокировок при работе внутренних механизмов MS SQL Server

Известны случаи возникновения взаимоблокировок при работе внутренних механизмов MS SQL Server. О возникновении таких взаимоблокировок говорят следующие сообщения об ошибках:

Рекомендации по устранению таких взаимоблокировок даны в статье "Ошибки "Intra-query parallelism" и "Deadlock on thread communication buffer"".

Обычно для возникновения взаимоблокировки необходимо участие четырех и более запросов (по два в двух конкурирующих транзакциях). Однако при выполнении некоторых запросов MS SQL Server может самостоятельно повышать уровень изоляции заблокированных ресурсов, что иногда также приводит к возникновению взаимоблокировки. В этом случае возможна взаимоблокировка, в которой участвуют всего два запроса от двух конкурирующих транзакций. Для возникновения такой взаимоблокировки необходимо одновременное выполнение следующих условий:

Соответственно, для устранения такой взаимоблокировки необходимо исключить любое из трех условий:

Анализ взаимоблокировок при помощи ЦУП

Как уже было отмечено выше, взаимоблокировки не всегда поддаются анализу на основании одной строки кода – той, в которой было получено сообщение об ошибке взаимоблокировки. Если исследуемая база работает с использованием MS SQL Server 2005, то есть возможность получить детальную информацию по взаимоблокировке с помощью Центра управления производительностью.

При анализе взаимоблокировок с помощью ЦУП нужно учитывать следующий фактор. Одна ошибка в прикладном коде конфигурации (например, отсутствие опции "ДЛЯ ИЗМЕНЕНИЯ" в запросе, контролирующем остатки) может привести к возникновению множества разнообразных взаимоблокировок. Они могут существенно отличаться друг от друга по сложности, в том числе включать различное количество конкурирующих транзакций. Всегда имеет смысл начинать анализ с более простой взаимоблокировки – с понятной вам схемой и с минимальным количеством участников. Обнаружив и устранив ее причину, вы можете одновременно избавиться от множества других (в том числе более сложных) взаимоблокировок.

ЦУП пытается автоматически ранжировать взаимоблокировки по сложности, показывая на верхних позициях в списках более простые случаи. Однако оценить сложность взаимоблокировки возможно не всегда. Не стоит тратить много времени, пытаясь разобраться в сложной для понимания взаимоблокировке. Следует выбрать из списка более простой и понятный вам случай.

Для получения информации по взаимоблокировке необходимо выполнить следующую последовательность действий:

1. Настроить права доступа и подключиться к исследуемой информационной базе в режиме мониторинга (см. руководство по использованию ЦУП).

2. Добавить показатель "Анализ взаимоблокировок" в список показателей ЦУП.

3. Включить запись этого показателя.

Внимание!
При включении записи аналитических показателей может наблюдаться падение производительности в исследуемой информационной базе.

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

Внимание!
После включения записи аналитического показателя ЦУП начинает сбор информации примерно через 1 минуту. Необходимо выдержать паузу между включением записи показателя и воспроизведением взаимоблокировки.

5. Выключить запись показателя "Анализ взаимоблокировок". После выключения записи ЦУП начнет разбор и анализ собранной информации. Этот процесс может занять длительное время.

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

7. Нажать кнопку "Анализ".

8. В форме анализа проблем производительности выбрать закладку "Код конфигурации", раскрыть первую вершину дерева и двойным щелчком открыть форму анализа взаимоблокировок:

9. Будет открыта форма списка взаимоблокировок, в которых участвует выбранный источник проблем – строка кода конфигурации. Группы взаимоблокировок объединяют одинаковые (по схеме) взаимоблокировки, произошедшие в разное время. Поскольку в системе может наблюдаться множество различных взаимоблокировок, следует проанализировать каждую группу. Из группы следует выбирать наиболее простую взаимоблокировку.

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

10. Провести анализ схемы взаимоблокировки и причин ее возникновения.

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

Далее приведены примеры анализа взаимоблокировок двух основных видов:

Примеры получены на измененной конфигурации УПП при помощи теста "Продажи" и доступны для самостоятельного изучения в демонстрационной базе ЦУП.

Повышение уровня блокировки ресурса в рамках одной транзакции

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

Во взаимоблокировке участвуют два процесса (транзакции):

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

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

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

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

1. Во взаимоблокировке участвует один ресурс – регистр накопления "ТоварыВРезервеНаСкладах":

2. Процессы являются однотипными, то есть выполняются одинаковые прикладные действия и, соответственно, работает один и тот же код конфигурации:

Откроем конфигурацию исследуемой базы и проанализируем запрос, который исполняется в строке 506 модуля набора записей регистра накопления "ТоварыКПередачеСоСкладов". При этом нас будет интересовать только обращение к заблокированному ресурсу – регистру "ТоварыВРезервеНаСкладах".

Ниже приведен сокращенный текст процедуры КонтрольСвободныхОстатков_Реализация(), из которой вызывается интересующий нас запрос.

Копировать в буфер обмена
Процедура КонтрольСвободныхОстатков_Реализация()
   …
   Запрос = Новый Запрос;
   …
   ТекстЗапроса = "
      |ВЫБРАТЬ // Запрос, контролирующий остатки на складах
      | Док.Номенклатура.Представление КАК НоменклатураПредставление,
      | …
      |ЛЕВОЕ СОЕДИНЕНИЕ
      | РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки(, " + УсловияТаблицыОстатков.БезКачества + ") КАК Резервы
      |ПО …
      | …
      |ДЛЯ ИЗМЕНЕНИЯ
      | РегистрНакопления.ТоварыКПередачеСоСкладов.Остатки";
   …
   Выборка = Запрос.Выполнить().Выбрать(); // Строка 506
 

При выполнении данного запроса идет чтение остатков регистра ТоварыВРезервеНаСкладах. При этом будет установлена разделяемая блокировка, так как опция "ДЛЯ ИЗМЕНЕНИЯ" применена только к регистру "ТоварыКПередачеСоСкладов".

Затем проанализируем код, который исполняется во второй интересующей нас строке: модуль набора записей регистра накопления "ТоварыВРезервеНаСкладах", строка 395. Нас по-прежнему будут интересовать только обращения к регистру "ТоварыВРезервеНаСкладах".

Копировать в буфер обмена
  Процедура КонтрольОстатков_Реализация()
   …
   Запрос = Новый Запрос;
   …
   ТекстЗапроса = "
      |ВЫБРАТЬ // Запрос, контролирующий остатки на складах
      | Док.Номенклатура.Представление 
      | …
      |ЛЕВОЕ СОЕДИНЕНИЕ
      | РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки(," + УсловияТаблицыОстатков.Резерв + ") КАК Резервы
      |ПО …
      |…
      |ДЛЯ ИЗМЕНЕНИЯ
      | РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки";
   …
   Выборка = Запрос.Выполнить().Выбрать(); // Строка 395

При выполнении этого запроса также производится чтение остатков регистра "ТоварыВРезервеНаСкладах", но они читаются с опцией "ДЛЯ ИЗМЕНЕНИЯ", то есть при этом будет установлена эксклюзивная блокировка.

Таким образом, регистр "ТоварыВРезервеНаСкладах" блокируется в рамках одной транзакции сначала разделяемой блокировкой, а затем эксклюзивной. Это и является причиной возникновения данной взаимоблокировки.

Для исправления этой ошибки необходимо изначально заблокировать остатки регистра "ТоварыВРезервеНаСкладах" с максимальным необходимым уровнем изоляции, то есть с опцией "ДЛЯ ИЗМЕНЕНИЯ". При этом код процедуры изменится следующим образом:

Копировать в буфер обмена
Процедура КонтрольСвободныхОстатков_Реализация()
   …
   Запрос = Новый Запрос;
   …
   ТекстЗапроса = "
      |ВЫБРАТЬ // Запрос, контролирующий остатки на складах
      | Док.Номенклатура.Представление КАК НоменклатураПредставление,
      | …
      |ЛЕВОЕ СОЕДИНЕНИЕ
      | РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки(, " + УсловияТаблицыОстатков.БезКачества + ") КАК Резервы
      |ПО …
      | …
      |ДЛЯ ИЗМЕНЕНИЯ
      | РегистрНакопления.ТоварыКПередачеСоСкладов.Остатки,
      | РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки";
   …
   Выборка = Запрос.Выполнить().Выбрать(); // Строка 506

Захват ресурсов в разном порядке

Рассмотрим пример взаимоблокировки, возникшей в результате захвата ресурсов в разном порядке:

  

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

1. Во взаимоблокировке участвуют несколько (в данном случае два) ресурсов. При этом они захватываются в разном порядке.

2. Процессы являются разнотипными, то есть выполняются разные прикладные действия и, соответственно, разный код конфигурации.

Рассмотрим действия, которые выполняют эти два процесса, и блокировки, которые при этом устанавливаются.

1. Действия и блокировки процесса 1:

1.1. Процесс 1 выполняет запрос контроля остатков в строке 506 модуля набора записей регистра накопления "ТоварыКПередачеСоСкладов". Рассмотрим фрагменты кода процедуры "КонтрольСвободныхОстатков_Реализация", обращая внимание только на обращения к таблице "ТоварыНаСкладах.Остатки".

Копировать в буфер обмена
Процедура КонтрольСвободныхОстатков_Реализация()
   …
   Запрос = Новый Запрос;
   …
   ТекстЗапроса = "
      |ВЫБРАТЬ // Запрос, контролирующий остатки на складах
      | Док.Номенклатура.Представление КАК НоменклатураПредставление,
      | …
      |ЛЕВОЕ СОЕДИНЕНИЕ
      | (ВЫБРАТЬ
      | …
      | ИЗ
      | РегистрНакопления.ТоварыНаСкладах.Остатки(, " + УсловияТаблицыОстатков.Полное + ") КАК ТоварыНаСкладахОстатки
      | …
      |ДЛЯ ИЗМЕНЕНИЯ
      | РегистрНакопления.ТоварыКПередачеСоСкладов.Остатки";
   …
   Выборка = Запрос.Выполнить().Выбрать(); // Строка 506

В результате выполнения этого запроса успешно устанавливается разделяемая блокировка на остатки регистра "ТоварыНаСкладах".

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

Открыв форму выполнения запроса, мы увидим полный стек вызовов кода конфигурации. Из этого стека становится понятно что интересующий нас запрос выполнялся при проведении документа "РеализацияТоваровУслуг" (в коде тестовой обработки соответствующая переменная имеет имя "Реализация").

1.2. Следующее действие, выполненное процессом 1, имеет этот же стек вызова, но он заканчивается на строке "Реализация.Записать".

Это означает, что оба действия были выполнены в результате проведения одного документа "РеализацияТоваровУслуг", но первое действие было вызвано в явном виде из кода конфигурации, а второе – выполнено автоматически платформой.

Посмотрим, какой SQL-запрос был сгенерирован платформой при выполнении этого действия.

Это обновление таблицы остатков регистра накопления "ТоварыКПередачеОрганизаций". При выполнении этого действия была установлена эксклюзивная блокировка на таблицу остатков этого регистра. Однако эта блокировка находится в режиме ожидания, так как имеется несовместимая с ней блокировка процесса 2.

2. Действия и блокировки процесса 2:

Процесс 2 выполняет одно-единственное действие – запись и проведение документа "РасходныйОрдер". Оба действия, имеющие отношение к взаимоблокировке, выполняются платформой автоматически при проведении документа.

2.1. Первое действие: обновление остатков регистра накопления "ТоварыКПередачеОрганизаций":

В результате этого действия устанавливается эксклюзивная блокировка на соответствующие остатки регистра "ТоварыКПередачеОрганизаций". Именно эта блокировка мешает процессу 1 установить аналогичную блокировку при выполнении действия 1.2.

2.2. Второе действие: обновление остатков регистра накопления "ТоварыНаСкладах":

В результате этого действия устанавливается эксклюзивная блокировка на соответствующие остатки регистра "ТоварыНаСкладах". Однако в системе уже имеется несовместимая блокировка на эти же записи регистра, которая была установлена процессом 1 при выполнении действия 1.1. По этой причине блокировка процесса 2 устанавливается в режиме ожидания.

Итак, процесс 1 ожидает окончания транзакции процесса 2, в то время как процесс 2 ожидает окончания транзакции процесса 1. Получаем взаимоблокировку по причине захвата ресурсов в разном порядке. Процесс 1 захватил сначала регистр накопления "ТоварыНаСкладах", а затем регистр "ТоварыКПередачеОрганизаций". Процесс 2 захватил эти же регистры, но в обратном порядке.

Все запросы, попавшие во взаимоблокировку, выполняются при автоматической записи движений по регистрам при проведении документа. Проблема в том, что в документе "РеализацияТоваровУслуг" при записи движений в регистр "ТоварыКПередачеСоСкладов" захватывается таблица остатков регистра "ТоварыНаСкладах". Именно это является причиной взаимоблокировки, однако обойтись без чтения этой таблицы, очевидно, нельзя, поскольку это часть алгоритма обработки данных.

Можно было бы попробовать изменить порядок записи движений в документе "РасходныйОрдер", прописав в обработчике проведения этого документа следующий код:

Копировать в буфер обмена
Движения.ТоварыНаСкладах.Записать();
Движения.ТоварыКПередачеОрганизаций.Записать();

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

Поэтому в данном случае рекомендуется другой способ устранения взаимоблокировки – установка искусственной блокировки. Поскольку процесс 2 (проведение документа "РасходныйОрдер") выполняет захват ресурсов автоматически, будем считать именно этот порядок правильным. Процесс 1 ("РеализацияТоваровУслуг") захватывает ресурсы в неправильном порядке – сначала блокируется регистр "ТоварыНаСкладах", затем – "ТоварыКПередачеОрганизаций". Именно этот порядок должен быть изменен.

Для этого следует выполнить запрос с опцией "ДЛЯ ИЗМЕНЕНИЯ" к таблице остатков регистра "ТоварыКПередачеОрганизаций". Этот запрос должен быть выполнен перед запросом в строке 506 модуля набора записей регистра "ТоварыКПередачеСоСкладов". Условия запроса должны быть такими, чтобы считать (и, соответственно, заблокировать) все остатки, которые будут изменены при записи набора записей в действии 1.2.

Существует еще один вариант устранения этой взаимоблокировки. Блокировки, устанавливаемые процессом 1 (документ "РеализаяТоваровУслуг"), возникают только при работе в автоматическом режиме. Характерным признаком таких блокировок является значение поля режим = "Range…":

Эти блокировки полностью исчезают при переходе в управляемый режим блокировки данных в транзакции. То есть данная взаимоблокировка будет невозможна при работе в управляемом режиме.