Как устранить неустранимый конфликт блокировок при чтении и записи регистра сведений в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки высоконагруженных систем на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с критической ошибкой: «Конфликт блокировок при выполнении транзакции: Неустранимый конфликт блокировок». Особенно остро эта проблема проявляется при работе с регистрами сведений, которые используются для интеграций (например, с Битрикс24), где множество документов одновременно пытаются записать данные в одну таблицу. Рассмотрим подробно, почему возникает эта ошибка и как правильно спроектировать механизм записи, чтобы избежать дедлоков.

Выясним причину: почему возникает «Смертельный захват» (Deadlock)?

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

  1. Сеанс №1 читает данные из регистра. Платформа устанавливает Shared lock (разделяемую блокировку). Это позволяет другим читать те же данные, но запрещает их изменять.
  2. Сеанс №2 также читает эти данные. Накладывается вторая разделяемая блокировка.
  3. Сеанс №1 решает записать данные. Для этого ему нужно повысить уровень блокировки до Exclusive (исключительной). Однако он не может этого сделать, так как Сеанс №2 удерживает свою разделяемую блокировку.
  4. Сеанс №2 одновременно пытается совершить запись и ждет, пока Сеанс №1 отпустит свою блокировку.

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

Решение №1: Установка исключительной блокировки перед чтением

Главное правило работы в режиме управляемых блокировок: блокируй данные на запись до того, как начнешь их читать для анализа. Если мы сначала прочитаем данные методом Прочитать() или обычным запросом, СУБД наложит разделяемую блокировку, которую потом будет крайне сложно «повысить» до исключительной без риска дедлока.

Рассмотрим правильный алгоритм действий:


// 1. Создаем объект блокировки
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ИмяВашегоРегистра");

// 2. Устанавливаем режим Исключительный СРАЗУ
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

// 3. Указываем конкретные измерения, которые будем анализировать
// Это крайне важно, чтобы не блокировать весь регистр целиком
ЭлементБлокировки.УстановитьЗначение("Объект", ТекОбъект);
ЭлементБлокировки.УстановитьЗначение("НастройкаПодключения", ТекНастройка);

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

Решение №2: Типизация таблицы значений и использование источника данных

Часто данные для регистрации приходят в виде таблицы значений (ТЗ). Если использовать ИсточникДанных в объекте БлокировкаДанных, платформа автоматически заблокирует все строки из этой таблицы. Однако здесь кроется ловушка: если колонки ТЗ не типизированы (имеют тип Произвольный), блокировка может работать некорректно или вызывать избыточное блокирование.

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


// Получаем описания типов напрямую из метаданных регистра
ТипОбъекта = Метаданные.РегистрыСведений.ИмяРегистра.Измерения.Объект.Тип;
ТипНастройки = Метаданные.РегистрыСведений.ИмяРегистра.Измерения.НастройкаПодключения.Тип;

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

// Заполняем таблицу данными из ПакетаДанных
// ...

// Теперь используем эту ТЗ как источник для блокировки
БлокировкаДанных = Новый БлокировкаДанных;
ЭлементБлокировкиДанных = БлокировкаДанных.Добавить("РегистрСведений.ИмяРегистра");
ЭлементБлокировкиДанных.ИсточникДанных = ПакетДанныхДляЗаписи;

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

НачатьТранзакцию();
БлокировкаДанных.Заблокировать();
// Далее следует безопасная запись данных

Решение №3: Оптимизация записи через запрос (исключение дублей)

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

Сначала мы помещаем входящий пакет данных во временную таблицу, затем левым соединением проверяем наличие записей в регистре. В результат запроса попадут только те строки, которых в БД еще нет (ГДЕ Регистр.Объект ЕСТЬ NULL).


Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ПакетДанных", ПакетДанных);
Запрос.Текст = 
"ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Пакет ИЗ &ПакетДанных КАК Пакет;
|ВЫБРАТЬ Пакет.* ИЗ ВТ_Пакет КАК Пакет
|ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ИмяРегистра КАК РеалРегистр
|ПО Пакет.Объект = РеалРегистр.Объект 
|И Пакет.Настройка = РеалРегистр.Настройка
|ГДЕ РеалРегистр.Объект ЕСТЬ NULL";

РезультатЗапроса = Запрос.Выполнить();

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

Важные рекомендации для стабильной работы

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

Соблюдение этих правил и использование исключительных управляемых блокировок позволит полностью избавиться от «Неустранимых конфликтов» и обеспечить высокую параллельность работы пользователей в системе.

← На главную