При разработке обработок группового пересчета, импорта или пакетной генерации документов программисты часто сталкиваются с критически важной задачей — необходимостью гарантировать строгую последовательность создания документов. Если два пользователя одновременно запустят одну и ту же обработку, документы в базе данных начнут чередоваться (часть из первого потока, часть из второго). Это неизбежно приведет к нарушению хронологии, сбиванию автонумерации и логическим ошибкам в расчетах.
В этой статье мы подробно разберем, почему стандартная попытка заблокировать таблицу документов через БлокировкаДанных не защищает от параллельного создания новых документов, выясним фундаментальные причины такого поведения платформы «1С:Предприятие 8» и рассмотрим три надежных, проверенных на практике способа решения этой задачи.
Проанализируем исходный код, который часто пытаются использовать начинающие разработчики:
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.КА_НарядНаСдельнуюРаботу");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
Для каждого СтрокаТехОперции из КакаяТаблица цикл
// В цикле создаются и записываются новые документы
КонецЦикла;
Попытка
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
На первый взгляд, код кажется логичным: мы накладываем исключительную блокировку на объект метаданных Документ.КА_НарядНаСдельнуюРаботу в управляемом режиме. Однако при параллельном запуске обработки в двух разных сеансах документы все равно продолжат записываться одновременно, вклиниваясь в общую последовательность друг друга. Выясним причину, почему менеджер блокировок 1С пропускает параллельные транзакции.
Специфика пространств управляемых блокировок для ссылочных типов:
В платформе 1С пространство управляемых блокировок строится на основе структуры метаданных. Для регистров (накопления, сведений) пространство блокировки определяется их измерениями (например, Склад и Номенклатура). Но для ссылочных типов (справочники, документы) существует ровно одно пространство блокировки — по полю Ссылка.
Когда мы пишем Блокировка.Добавить("Документ.КА_НарядНаСдельнуюРаботу") и не указываем конкретное значение отбора по ссылке, платформа блокирует только те элементы, которые уже физически существуют в базе данных на момент начала транзакции.
Когда в цикле создается новый документ через метод СоздатьДокумент(), платформа генерирует для него абсолютно новый, уникальный идентификатор (UUID). Менеджер управляемых блокировок 1С сопоставляет пространства блокировок двух параллельных транзакций: поскольку новые ссылки уникальны и никак не пересекаются с заблокированными ранее существующими ссылками, система не обнаруживает конфликта. В результате обе транзакции выполняются параллельно, порождая эффект «фантомной записи» и нарушая хронологический порядок документов.
Для решения этой проблемы нам необходимо использовать другие архитектурные паттерны. Рассмотрим их подробнее.
---
Это самый надежный классический способ синхронизации потоков на уровне базы данных. Вместо попытки заблокировать несуществующие новые документы, мы заблокируем один конкретный, заранее созданный элемент в базе данных, который будет выступать в роли «светофора» (мьютекса).
Для реализации этого метода выполним следующие шаги:
Справочники.СлужебныеМьютексы) или используем уже существующий малоиспользуемый справочник.ГрупповаяОбработкаНарядов.Посмотрим на пример программной реализации этого подхода:
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Блокировка = Новый БлокировкаДанных;
ЭлементМьютекса = Блокировка.Добавить("Справочник.СлужебныеМьютексы");
ЭлементМьютекса.Режим = РежимБлокировкиДанных.Исключительный;
// Указываем отбор по конкретной ссылке предопределенного элемента
ЭлементМьютекса.УстановитьЗначение("Ссылка", Справочники.СлужебныеМьютексы.ГрупповаяОбработкаНарядов);
Попытка
// Блокируем элемент-мьютекс. Если параллельный поток уже захватил его,
// текущий сеанс перейдет в режим ожидания (по умолчанию до 20 секунд),
// после чего вызовет исключение по таймауту.
Блокировка.Заблокировать();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение "Данная обработка уже запущена другим пользователем. Пожалуйста, повторите попытку позже.";
КонецПопытки;
Попытка
// Здесь безопасно выполняем цикл создания документов.
// Ни один другой сеанс не сможет начать этот цикл, пока мы не зафиксируем транзакцию.
Для Каждого СтрокаТехОперации Из ТаблицаОпераций Цикл
НовыйДокумент = Документы.КА_НарядНаСдельнуюРаботу.СоздатьДокумент();
// Заполнение реквизитов...
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Логирование и обработка ошибок
ЗаписатьРаботуСбоя(ОписаниеОшибки());
ВызватьИсключение "Произошла ошибка при генерации документов. Все изменения отменены.";
КонецПопытки;
Как это работает: Так как абсолютно все сеансы при запуске данной обработки будут пытаться наложить исключительную блокировку на одну и ту же ссылку Справочники.СлужебныеМьютексы.ГрупповаяОбработкаНарядов, менеджер управляемых блокировок физически выстроит их в очередь. Второй сеанс не начнет создавать документы, пока первый полностью не завершит свою транзакцию (через COMMIT или ROLLBACK) и не снимет блокировку.
---
Пакетное создание большого количества документов в рамках одной транзакции может занимать длительное время. Удержание длинной транзакции крайне негативно сказывается на производительности СУБД: забивается сетевой трафик, растут таблицы блокировок, и другие пользователи начинают получать ошибки ожидания. Если процесс генерации документов длится дольше нескольких секунд, от транзакции на весь период работы лучше отказаться.
Вместо этого мы можем задействовать механизм пессимистической объектной блокировки платформы 1С с помощью метода ЗаблокироватьДанныеДляРедактирования(). Этот метод резервирует объект на уровне сеанса, а не транзакции базы данных.
Рассмотрим по шагам, как реализовать этот сценарий в форме нашей обработки:
&НаКлиенте
Процедура ЗапуститьОбработку(Команда)
// Получаем ссылку на наш предопределенный элемент-мьютекс
СсылкаМьютекс = ПолучитьСлужебнуюСсылкуНаСервере();
Попытка
// Пытаемся заблокировать объект для редактирования в текущем сеансе.
// Блокировка будет удерживаться до завершения работы обработки или ручного снятия.
ЗаблокироватьДанныеДляРедактирования(СсылкаМьютекс);
Исключение
// Если объект заблокирован в другом сеансе, платформа выбросит исключение.
// В тексте ошибки будет содержаться имя пользователя, который уже работает с обработкой.
ПоказатьПредупреждение(, "Не удалось запустить операцию! С обработкой уже работает другой пользователь.");
Возврат;
КонецПопытки;
// Вызываем серверную процедуру создания документов без открытия общей транзакции.
// Каждый документ будет создаваться и проводиться в своей индивидуальной короткой транзакции.
СоздатьДокументыПакетноНаСервере();
// После успешного (или аварийного) завершения обязательно снимаем блокировку
РазблокироватьДанныеДляРедактирования(СсылкаМьютекс);
ПоказатьПредупреждение(, "Обработка успешно завершена!");
КонецПроцедуры
Преимущества метода: Мы защищаем процесс от параллельного запуска на уровне интерфейса пользователя, не нагружая при этом СУБД тяжелыми транзакциями. Если в процессе создания сотого документа произойдет сбой, предыдущие 99 документов останутся корректно записанными в базе данных, а не откатятся целиком, что экономит аппаратные ресурсы.
---
Для клиент-серверных баз данных лучшей практикой выполнения любых длительных групповых операций является их вынос на уровень сервера приложений 1С в виде фоновых заданий. Платформа «1С:Предприятие 8» предоставляет встроенный, исключительно надежный инструмент контроля уникальности фоновых процессов — параметр Ключ метода Выполнить() менеджера фоновых заданий.
Проанализируем программную логику запуска фонового задания с контролем уникальности:
&НаСервере
Функция ЗапуститьГенерациюВФоновомРежиме() Экспорт
// Задаем уникальный строковый ключ для нашей бизнес-операции
КлючУникальности = "УникальныйКлюч_СозданиеНарядовНаСдельнуюРаботу";
// Проверим, нет ли уже запущенного активного фонового задания с таким же ключом
Отбор = Новый Структура;
Отбор.Вставить("Ключ", КлючУникальности);
Отбор.Вставить("Состояние", СостояниеФоновогоЗадания.Активно);
АктивныеЗадания = ФоновыеЗадания.ПолучитьФоновыеЗадания(Отбор);
Если АктивныеЗадания.Количество() > 0 Тогда
ВызватьИсключение "Процесс пакетного создания документов уже выполняется в фоновом режиме!";
КонецЕсли;
// Запускаем новое фоновое задание.
// Переданный параметр "Ключ" физически заблокирует повторный запуск задания с таким же ключом
// на уровне всего кластера серверов 1С.
ПараметрыЗапуска = Новый Массив; // Сюда помещаем параметры для нашей процедуры
Задание = ФоновыеЗадания.Выполнить(
"ОбщегоНазначенияСервер.СоздатьДокументыНарядовФоновое",
ПараметрыЗапуска,
КлючУникальности,
"Пакетное создание нарядов на работу (Фоновое)"
);
Возврат Задание.УникальныйИдентификатор;
КонецФункции
Почему это наиболее эффективный подход:
---
Подведем итог и определимся, какой метод синхронизации выбрать в зависимости от архитектурных особенностей вашей информационной системы: