Разработчики в среде 1С часто сталкиваются с крайне неприятной ошибкой: «Конфликт блокировок при выполнении транзакции. Превышено максимальное время ожидания предоставления блокировки». Она может парализовать работу пользователей, заблокировать выполнение регламентных заданий и привести к экстренным перезапускам службы Агента сервера 1С:Предприятия.
В этой статье мы подробно проанализируем, почему возникает этот конфликт, как на него влияют неявные соединения при обращении к реквизитам через точку (разыменование), как правильно оптимизировать запросы (удобнее через набор инструментов разработчика для оптимизации запросов и анализа метаданных) и какие изменения в архитектуре помогут навсегда избавиться от этой проблемы.
Для начала разберем физическую суть конфликта. Когда платформа 1С выполняет запись объекта (например, методом Записать()), СУБД (MS SQL Server, PostgreSQL и др.) пытается наложить исключительную блокировку (X-lock) на конкретные строки таблиц, чтобы гарантировать целостность данных во время транзакции.
Если в этот же момент параллельный сеанс (другой пользователь или регламентное задание) читает или изменяет те же самые таблицы, возникает конфликт интересов. СУБД выстраивает процессы в очередь. Если первый процесс не успевает освободить ресурсы за отведенное время (по умолчанию в 1С это 20 секунд), второй процесс аварийно завершается с ошибкой превышения времени ожидания.
Рассмотрим основные факторы, которые резко увеличивают вероятность возникновения конфликта:
ОбъектЗадачи) имеет составной тип, включающий десятки видов документов, то при обращении к его свойствам через точку платформа генерирует SQL-запрос с огромным количеством соединений LEFT OUTER JOIN со всеми таблицами, входящими в этот тип.ИЛИ мешает оптимизатору СУБД эффективно использовать индексы, что приводит к полному сканированию таблиц (Table Scan / Index Scan) и блокировке избыточного диапазона данных.Проанализируем классический пример проблемного кода. Допустим, у нас есть задача, у которой реквизит ОбъектЗадачи имеет составной тип (может быть ссылкой на различные документы: «Реализация товаров», «Поступление товаров», «ЗаказКлиента» и т.д.). Нам нужно выбрать задачи, по которым связанные документы проведены или помечены на удаление.
Посмотрим на пример неоптимального запроса:
ВЫБРАТЬ
Ссылка КАК ЗадачаСсылка
ИЗ
Задача.ЗадачаИсполнителя КАК Задача
ГДЕ
Задача.Выполнена = ЛОЖЬ
И (Задача.ОбъектЗадачи.Проведен = ИСТИНА
ИЛИ Задача.ОбъектЗадачи.ПометкаУдаления = ИСТИНА)
При выполнении этого запроса платформа 1С будет вынуждена соединить таблицу задач со всеми таблицами документов, которые теоретически могут находиться в поле ОбъектЗадачи. Это порождает колоссальные избыточные блокировки на СУБД.
Для исправления этой ситуации применим оператор ВЫРАЗИТЬ. Мы должны явно указать платформе, к какому конкретно типу документа мы обращаемся. Если бизнес-логика предполагает работу только с определенными типами документов, мы жестко ограничиваем их перечень:
ВЫБРАТЬ
Задача.Ссылка КАК ЗадачаСсылка
ИЗ
Задача.ЗадачаИсполнителя КАК Задача
ГДЕ
Задача.Выполнена = ЛОЖЬ
И (ВЫРАЗИТЬ(Задача.ОбъектЗадачи КАК Документ.РеализацияТоваровУслуг).Проведен = ИСТИНА
ИЛИ ВЫРАЗИТЬ(Задача.ОбъектЗадачи КАК Документ.РеализацияТоваровУслуг).ПометкаУдаления = ИСТИНА)
Благодаря использованию ВЫРАЗИТЬ, СУБД выполнит соединение только с одной таблицей — «Реализация товаров и услуг», проигнорировав остальные типы документов. Диапазон блокировок существенно сузится.
Даже после явного приведения типов оператор ИЛИ в условии запроса может провоцировать неоптимальные планы выполнения на уровне СУБД. Проанализируем, как переписать запрос, заменив ИЛИ на объединение подзапросов с помощью ОБЪЕДИНИТЬ ВСЕ.
Рассмотрим оптимальную структуру запроса:
ВЫБРАТЬ
Задача.Ссылка КАК ЗадачаСсылка
ИЗ
Задача.ЗадачаИсполнителя КАК Задача
ГДЕ
Задача.Выполнена = ЛОЖЬ
И ВЫРАЗИТЬ(Задача.ОбъектЗадачи КАК Документ.РеализацияТоваровУслуг).Проведен = ИСТИНА
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Задача.Ссылка КАК ЗадачаСсылка
ИЗ
Задача.ЗадачаИсполнителя КАК Задача
ГДЕ
Задача.Выполнена = ЛОЖЬ
И ВЫРАЗИТЬ(Задача.ОбъектЗадачи КАК Документ.РеализацияТоваровУслуг).ПометкаУдаления = ИСТИНА
Такой подход позволяет СУБД построить два простых и быстрых плана выполнения по индексам таблиц и объединить их результаты, минимизируя время наложения разделяемых блокировок чтения.
Чтобы СУБД могла быстро находить нужные строки без сканирования всей таблицы, необходимо индексировать поля, участвующие в условиях отбора — поможет анализ объема данных и структуры индексов таблиц 1С. В нашем случае критически важным является поле Выполнена таблицы задач.
Выполним индексирование реквизита:
ЗадачаИсполнителя).Выполнена.Хотя оптимизация запросов через ВЫРАЗИТЬ и избавление от ИЛИ дают быстрый результат, самым надежным и профессиональным решением является изменение архитектуры хранения данных — для этого пригодится архитектурный анализ конфигураций и графа зависимостей объектов 1С.
Постоянное чтение реквизитов связанных документов из задач — это архитектурная ошибка. Вместо этого мы можем дублировать необходимые состояния (например, признак ДокументПроведенИлиУдален) непосредственно в саму задачу при записи связанного документа.
Реализуем этот механизм по шагам:
ЗадачаИсполнителя новый реквизит ОбъектНеактивен с типом Булево и включим для него свойство «Индексировать».ПередЗаписью или ПриЗаписи документов) добавим код, который при изменении состояния проведения или пометки удаления автоматически находит незакрытые задачи по этому документу и обновляет в них реквизит ОбъектНеактивен — для этого есть инструмент отладки кода для поиска ошибок в режиме Предприятия.Посмотрим на пример кода в подписке на событие при записи документа:
Процедура ОбновитьСтатусЗадачДокумента(Источник, Отказ) Экспорт
Если Источник.ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Проверим, изменилось ли состояние
СостояниеИзменилось = Источник.Проведен ИЛИ Источник.ПометкаУдаления;
Если СостояниеИзменилось Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Задача.Ссылка КАК Ссылка
|ИЗ
| Задача.ЗадачаИсполнителя КАК Задача
|ГДЕ
| Задача.ОбъектЗадачи = &ДокументСсылка
| И Задача.Выполнена = ЛОЖЬ";
Запрос.УстановитьПараметр("ДокументСсылка", Источник.Ссылка);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
ЗадачаОбъект = Выборка.Ссылка.ПолучитьОбъект();
ЗадачаОбъект.ОбъектНеактивен = ИСТИНА;
Попытка
ЗадачаОбъект.Записать();
Исключение
// Обработка исключения при параллельной блокировке
ЗаписьЖурналаРегистрации("ОбновлениеЗадач", УровеньЖурналаРегистрации.Предупреждение, , , ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
После реализации этой архитектурной схемы наш периодический запрос в регламентном задании станет максимально простым, быстрым и изолированным. Он будет обращаться только к одной таблице задач без каких-либо соединений:
ВЫБРАТЬ
Задача.Ссылка КАК ЗадачаСсылка
ИЗ
Задача.ЗадачаИсполнителя КАК Задача
ГДЕ
Задача.Выполнена = ЛОЖЬ
И Задача.ОбъектНеактивен = ИСТИНА
Этот запрос отработает за миллисекунды и гарантированно не вызовет конфликтов блокировок в СУБД.
Если база данных все еще работает в автоматическом режиме блокировок (свойство конфигурации «Режим управления блокировкой данных» установлено в «Автоматический»), СУБД использует максимальный уровень изоляции транзакций SERIALIZABLE. В этом режиме транзакции блокируют избыточные диапазоны строк, что делает дедлоки неизбежными.
Для кардинального решения проблемы переведем базу данных в Управляемый режим блокировок:
В управляемом режиме СУБД работает на уровне изоляции READ COMMITTED (с включенным snapshot-изолированием). Чтение данных запросами больше не накладывает блокировок СУБД, а транзакционные блокировки контролируются самой платформой 1С через объект БлокировкаДанных.
Посмотрим на пример явного наложения управляемой блокировки в коде перед записью объекта:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Задача.ЗадачаИсполнителя");
ЭлементБлокировки.УстановитьПараметр("Ссылка", СсылкаНаЗадачу);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Попытка
Блокировка.Заблокировать();
// Производим чтение и последующую запись объекта
ЗадачаОбъект = СсылкаНаЗадачу.ПолучитьОбъект();
ЗадачаОбъект.Выполнена = ИСТИНА;
ЗадачаОбъект.Записать();
Исключение
// Обработка ситуации, если объект уже заблокирован параллельным процессом
КонецПопытки;
Если блокировки продолжают возникать, и вы не можете определить их точный источник, необходимо настроить сбор Технологического журнала (ТЖ) 1С — для этого подойдёт анализ производительности 1С на основе техжурнала и СУБД. Это позволит точно зафиксировать транзакцию-«виновника» и транзакцию-«жертву».
Создадим файл конфигурации ТЖ logcfg.xml в папке настроек сервера 1С (обычно C:\Program Files\1cv8\conf\) со следующим содержимым:
<config xmlns="http://v8.1c.ru/v8/tech-log">
<log location="C:\v8logs\locks" history="24">
<event>
<eq property="Name" value="TLOCK"/>
</event>
<event>
<eq property="Name" value="TTIMEOUT"/>
</event>
<event>
<eq property="Name" value="TDEADLOCK"/>
</event>
<property name="all"/>
</log>
</config>
После возникновения ошибки проанализируем файлы логов в каталоге C:\v8logs\locks. Событие TTIMEOUT покажет точное время конфликта, а параметры WaitConnections укажут номера сеансов, которые удерживали блокировку и мешали выполнению вашей транзакции.
Мы подробно разобрали причины возникновения блокировок при работе с транзакциями в 1С и методы их устранения. Применяя на практике типизацию составных типов через ВЫРАЗИТЬ, избавление от сложных условий с ИЛИ, а также декуплинг данных, вы сможете обеспечить стабильную, быструю и бесконфликтную работу вашей информационной системы даже при высокой интенсивности параллельных процессов.