При реализации механизмов обмена данными между базами 1С через веб-сервисы или внешние соединения разработчики часто сталкиваются с проблемой конкурентного доступа. Ошибка «Ошибка блокировки объекта. Объект уже заблокирован» обычно возникает в момент вызова метода НачатьЗапись() объекта ЗаписьСообщенияПланаОбмена. В этой статье мы подробно разберем, почему возникает эта ситуация, как работает механизм блокировок в планах обмена и какие стратегии (включая программные семафоры) помогут нам выстроить запросы в очередь.
Для начала проанализируем внутреннюю логику работы платформы 1С. Метод НачатьЗапись не просто подготавливает XML-структуру. Его главная задача — зафиксировать номер исходящего сообщения для конкретного узла плана обмена. Чтобы гарантировать уникальность и последовательность номеров сообщений, платформа устанавливает исключительную блокировку на запись в таблице узлов плана обмена, соответствующую нашему УзелОбмена.
Если в этот же самый момент другой сеанс (например, второй вызов веб-сервиса или регламентное задание) пытается вызвать НачатьЗапись для того же самого узла, он получает отказ, так как объект уже заблокирован первым сеансом. Это не ошибка в коде как таковая, а штатное поведение СУБД и менеджера блокировок 1С, защищающее целостность данных обмена.
Самый правильный способ с точки зрения платформы 8.3 — это использование объекта БлокировкаДанных. Вместо того чтобы ждать, пока НачатьЗапись «упадет» с ошибкой, мы можем явно попытаться заблокировать узел перед началом всех операций. Рассмотрим код реализации:
// Создаем объект блокировки
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("ПланОбмена.УниверсальныйОбмен.Узел");
ЭлементБлокировки.УстановитьЗначение("Ссылка", УзелОбмена);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Попытка
// Пытаемся установить блокировку
Блокировка.Заблокировать();
// Если мы здесь, значит узел наш, можем начинать запись
ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();
ЗаписьСообщения.НачатьЗапись(ЗаписьXML, УзелОбмена);
// ... логика выгрузки данных ...
ЗаписьСообщения.ЗавершитьЗапись();
Исключение
// Сюда попадем, если узел уже занят другим процессом
ЗаписьЖурналаРегистрации("ОбменДанными", УровеньЖурналаРегистрации.Предупреждение, , ,
"Узел " + УзелОбмена + " заблокирован другим сеансом.");
КонецПопытки;
Использование этого метода позволяет системе поставить текущий поток в очередь ожидания (в пределах таймаута, установленного на сервере 1С), что фактически реализует поведение семафора на уровне СУБД.
Разберем ситуацию, когда вызовы веб-сервиса происходят слишком часто. Вместо того чтобы выполнять тяжелый обмен непосредственно в теле метода веб-сервиса, мы можем использовать механизм фоновых заданий. Это позволит нам контролировать количество одновременно работающих процессов для одного узла.
Проанализируем алгоритм работы диспетчера:
ФоновыеЗадания.ПолучитьФоновыеЗадания() с фильтром по ключу. В качестве ключа удобно использовать строковое представление ссылки узла.
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Ключ", "Обмен_" + УзелОбмена.УникальныйИдентификатор());
ПараметрыОтбора.Вставить("Состояние", СостояниеФоновогоЗадания.Активно);
АктивныеЗадания = ФоновыеЗадания.ПолучитьФоновыеЗадания(ПараметрыОтбора);
Если АктивныеЗадания.Количество() = 0 Тогда
// Запускаем новое задание, так как старых нет
ФоновыеЗадания.Выполнить("ОбменДаннымиСервер.ВыполнитьВыгрузку",
Новый Массив(УзелОбмена),
ПараметрыОтбора.Ключ,
"Обмен данными с " + УзелОбмена);
КонецЕсли;
Если нам нужно жестко упорядочить вызовы, мы можем создать регистр сведений ОчередьОбмена. Рассмотрим, как это поможет нам в управлении потоками:
При каждом обращении к обмену мы записываем в этот регистр новую запись с текущей датой и ссылкой на узел. Затем мы выбираем из этого регистра самую старую необработанную запись. Таким образом, даже если 10 сеансов одновременно обратятся к системе, они будут выстроены в логическую очередь.
Важный момент: Чтобы избежать коллизий при чтении самой очереди, чтение и отметка о начале обработки должны происходить в одной транзакции с установкой блокировки на запись регистра.
В некоторых случаях блокировка носит кратковременный характер. Например, один процесс уже почти закончил ЗавершитьЗапись(), а второй только начал. Чтобы не прерывать работу пользователя ошибкой, применим цикл с ожиданием. Посмотрим на пример реализации:
МаксимальноеЧислоПопыток = 5;
ТекущаяПопытка = 1;
Успешно = Ложь;
Пока ТекущаяПопытка <= МаксимальноеЧислоПопыток И НЕ Успешно Цикл
Попытка
НачатьТранзакцию();
ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();
ЗаписьСообщения.НачатьЗапись(ЗаписьXML, УзелОбмена);
// ... Логика обмена ...
ЗаписьСообщения.ЗавершитьЗапись();
ЗафиксироватьТранзакцию();
Успешно = Истина;
Исключение
ОтменитьТранзакцию();
ТекущаяПопытка = ТекущаяПопытка + 1;
Если ТекущаяПопытка <= МаксимальноеЧислоПопыток Тогда
// Приостанавливаем выполнение на 1-2 секунды перед повтором
ОбщегоНазначения.Пауза(2);
Иначе
ВызватьИсключение "Не удалось выполнить обмен после " + МаксимальноеЧислоПопыток + " попыток.";
КонецЕсли;
КонецПопытки;
КонецЦикла;
Выясним причину, по которой узел удерживается слишком долго. Часто разработчики совершают ошибку, помещая длительное формирование тела XML-сообщения внутрь транзакции между НачатьЗапись и ЗавершитьЗапись.
Рассмотрим правильный подход:
ЗаписьXML и вызывайте НачатьЗапись.Чем меньше времени проходит между вызовами НачатьЗапись и ЗавершитьЗапись, тем меньше вероятность, что другие сеансы столкнутся с ошибкой блокировки объекта. Помните, что НачатьЗапись блокирует узел на всё время до момента вызова ЗавершитьЗапись или окончания транзакции.
Таким образом, комбинируя использование БлокировкаДанных для предотвращения конфликтов и механизм фоновых заданий для диспетчеризации, мы можем создать отказоустойчивую систему обмена, которая корректно обрабатывает конкурентные запросы и не «валится» с ошибками блокировок.