При работе с крупными базами данных и сложными алгоритмами (например, расчет себестоимости, закрытие месяца или распределение затрат) разработчики часто сталкиваются с длительным проведением документов. Время выполнения таких операций может занимать от нескольких минут до получаса. В этот период система испытывает колоссальную нагрузку, и крайне важно информировать других пользователей или смежные сеансы о том, что расчет уже запущен. Это необходимо, чтобы избежать повторного запуска тяжелых процедур или формирования неактуальных отчетов.
Типичная попытка решить эту задачу «в лоб» — сделать быструю запись в регистр сведений в самом начале процедуры ОбработкаПроведения, установив флаг ВыполняетсяРасчет = Истина, а в конце сбросить его в Ложь. Однако на практике разработчики обнаруживают, что другие пользователи базы данных не видят изменения статуса вплоть до полного завершения проведения документа. Давайте подробно разберем, почему так происходит, и рассмотрим эффективные варианты решения этой проблемы на платформе 1С:Предприятие 8.3.
Для того чтобы понять природу этой проблемы, проанализируем поведение транзакций в СУБД и платформе 1С. Любая модификация данных при проведении документа (включая обработчики событий ПередЗаписью, ПриЗаписи и непосредственно ОбработкаПроведения) выполняется в рамках одной общей, неделимой транзакции базы данных.
Современные версии платформы 1С 8.3, работающие на СУБД MS SQL Server, PostgreSQL, IBM DB2 или Oracle, по умолчанию используют уровень изоляции транзакций Read Committed (а для MS SQL Server — его разновидность Read Committed Snapshot Isolation, или RCSI). Основные правила этого режима:
В старых версиях платформы 1С 8.2 или при использовании режима совместимости с версией 8.2 разработчики иногда могли наблюдать эффект «грязного чтения», когда незафиксированные записи были видны из других сеансов. В современной технологической платформе 1С 8.3 без режима совместимости такие трюки невозможны и недопустимы, так как нарушают стандарты надежности данных.
Ниже мы подробно разберем четыре практических способа решения этой задачи, соответствующих современным стандартам разработки 1С.
Фоновые задания в 1С выполняются в отдельном сеансе на сервере приложений. У них собственное сетевое подключение к СУБД и своя, абсолютно независимая от документа транзакция. Если мы запустим фоновое задание в самом начале проведения документа, оно мгновенно выполнит запись статуса в регистр сведений и зафиксирует изменения в СУБД. Эти данные сразу же станут доступны другим пользователям.
Разберем по шагам реализацию данного механизма. Сначала создадим независимый регистр сведений СтатусыРасчетов с измерением Организация (тип СправочникСсылка.Организации) и ресурсом ВыполняетсяРасчет (тип Булево).
В общем модуле с флагом Сервер добавим экспортную процедуру, которая будет вызываться фоновым заданием:
Процедура УстановитьСтатусРасчетаФоновымЗаданием(ПараметрыСтатуса) Экспорт
Организация = ПараметрыСтатуса.Организация;
НовыйСтатус = ПараметрыСтатуса.Статус;
// Используем менеджер записи для быстрой фиксации в независимом регистре
МенеджерЗаписи = РегистрыСведений.СтатусыРасчетов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Организация = Организация;
МенеджерЗаписи.ВыполняетсяРасчет = НовыйСтатус;
МенеджерЗаписи.Записать(Истина); // Запись с замещением
КонецПроцедуры
Теперь в модуле документа (например, в обработчике события ПередЗаписью или в самом начале ОбработкаПроведения) мы инициируем запуск этого фонового задания:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
// Подготовим параметры для фонового задания
ПараметрыСтатуса = Новый Структура;
ПараметрыСтатуса.Вставить("Организация", ЭтотОбъект.Организация);
ПараметрыСтатуса.Вставить("Статус", Истина);
ПараметрыЗапуска = Новый Массив;
ПараметрыЗапуска.Добавить(ПараметрыСтатуса);
// Запускаем фоновое задание, которое выполнится вне текущей транзакции
ФоновыеЗадания.Выполнить("ИмяОбщегоМодуля.УстановитьСтатусРасчетаФоновымЗаданием", ПараметрыЗапуска);
КонецЕсли;
КонецПроцедуры
Важное предупреждение: Ни в коем случае не передавайте в фоновое задание ссылку на сам проводимый документ или целый объект документа с целью его чтения внутри фонового задания. Так как документ заблокирован транзакцией проведения основного сеанса, фоновое задание просто зависнет в режиме ожидания блокировки СУБД. Передавайте только простые типы данных (числа, строки, УИДы или ссылки на другие справочники, например, на организацию).
Если конечная цель нашей задачи заключается в том, чтобы во время расчета себестоимости заблокировать формирование тяжелых отчетов или предупредить пользователей, нам вообще не обязательно физически записывать данные в таблицы СУБД. Для этого отлично подходит механизм управляемых транзакционных блокировок 1С.
Мы можем наложить исключительную управляемую блокировку на виртуальное пространство (например, на определенную организацию в служебном регистре сведений), которая будет автоматически удерживаться менеджером блокировок 1С до самого окончания транзакции проведения.
Рассмотрим пример установки такой блокировки в обработчике проведения нашего документа:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Инициализируем блокировку данных
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СтатусыРасчетов");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Организация", ЭтотОбъект.Организация);
// Блокировка накладывается мгновенно и держится до конца транзакции (все 10 минут)
Блокировка.Заблокировать();
// Далее следует длительный алгоритм расчетов...
ВыполнитьДлительныйРасчетСебестоимости(ЭтотОбъект.Организация);
КонецПроцедуры
Теперь проанализируем, как использовать эту блокировку в отчете. Перед тем как начать формировать отчет, мы попытаемся установить блокировку на эту же область данных с минимальным временем ожидания (таймаутом). Если блокировка не установилась и система выдала ошибку — значит, в данный момент идет расчет!
Функция ПроверитьВыполнениеРасчета(Организация)
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СтатусыРасчетов");
// Запрашиваем разделяемый режим блокировки
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
ЭлементБлокировки.УстановитьЗначение("Организация", Организация);
Попытка
// Пытаемся заблокировать. Время ожидания ограничено платформой
Блокировка.Заблокировать();
// Если блокировка установилась успешно, значит, документ расчета сейчас не проводится
Возврат Ложь;
Исключение
// Исключение сработает, если транзакция проведения удерживает исключительную блокировку
Возврат Истина;
КонецПопытки;
КонецФункции
Этот метод является самым надежным с точки зрения отказоустойчивости системы. Если сервер аварийно отключится, процесс проведения завершится сбоем или пользователь принудительно закроет программу — управляемая блокировка мгновенно исчезнет сама. Вам не придется вручную чистить «зависшие» в базе данных статусы Истина.
Если запуск проведения всегда осуществляется пользователем интерактивно из экранной формы, мы можем перехватить этот процесс и выполнить запись статуса до того, как транзакция записи документа будет открыта СУБД. Для этого переопределим стандартную команду проведения на форме документа.
Создадим на форме документа собственную команду проведения «Провести расчет» и пропишем в ней следующий алгоритм совместной работы клиента и сервера:
&НаКлиенте
Процедура ПровестиРасчет(Команда)
// Шаг 1. Записываем статус "Выполняется" на сервере вне транзакции документа
УстановитьСтатусБлокировкиНаСервере(Объект.Организация, Истина);
// Шаг 2. Запускаем стандартную процедуру проведения документа
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("РежимЗаписи", РежимЗаписиДокумента.Проведение);
Попытка
Записать(ПараметрыЗаписи);
Исключение
// Если проведение завершилось ошибкой, сбрасываем статус блокировки обратно
УстановитьСтатусБлокировкиНаСервере(Объект.Организация, Ложь);
ВызватьИсключение;
КонецПопытки;
// Шаг 3. В случае успешного проведения сбрасываем статус блокировки
УстановитьСтатусБлокировкиНаСервере(Объект.Организация, Ложь);
КонецПроцедуры
&НаСервереБезКонтекста
Процедура УстановитьСтатусБлокировкиНаСервере(Организация, Статус)
МенеджерЗаписи = РегистрыСведений.СтатусыРасчетов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Организация = Организация;
МенеджерЗаписи.ВыполняетсяРасчет = Статус;
МенеджерЗаписи.Записать(Истина);
КонецПроцедуры
Преимуществом данного метода является простота реализации и отсутствие необходимости работать с фоновыми заданиями. Однако помните о существенном ограничении: этот способ не защитит вас, если документ проводится программно (например, групповой обработкой или регламентным заданием), минуя интерактивную форму.
Любые действия, выполняемые программным кодом 1С за пределами базы данных СУБД, по определению не могут быть изолированы или откатаны транзакцией. Мы можем использовать этот факт для немедленной передачи статуса во внешнюю среду.
Рассмотрим два варианта такой реализации:
НайтиФайлы().Проанализируем пример кода с отправкой HTTP-запроса из обработчика проведения:
Процедура ОтправитьСтатусЧерезHTTP(ОрганизацияКод, Статус)
// Настраиваем соединение со своей же базой, опубликованной на веб-сервере
Соединение = Новый HTTPСоединение("localhost", , "Администратор", "Пароль");
// Формируем запрос к нашему HTTP-сервису
Запрос = Новый HTTPЗапрос("/StatusApp/hs/CalculationStatus/Set");
Запрос.УстановитьТелоИзСтроки(Строка(Статус) + "," + ОрганизацияКод);
Попытка
// Отправка запроса происходит мгновенно вне текущей БД-транзакции
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
Исключение
// Обработка ошибок связи
КонецПопытки;
КонецПроцедуры
Рассмотренные выше обходные пути помогают решить локальную задачу индикации процесса. Однако с точки зрения архитектурных стандартов разработки фирмы «1С», удержание открытой транзакции СУБД в течение 10 минут — это критическая ошибка (серьезный антипаттерн).
Длительные транзакции неизбежно приводят к блокировкам таблиц, невозможности параллельной работы других пользователей, риску возникновения взаимоблокировок (deadlocks) и взрывному росту файлов логов СУБД (LDF-файлов). В современных системах, таких как 1С:ERP Управление предприятием или 1С:Комплексная автоматизация, длительные расчеты проектируются принципиально иначе.
Рассмотрим правильную схему асинхронного расчета:
ДлительныеОперации.Используя предложенные методики, мы можем выбрать наиболее подходящий вариант решения проблемы — от простых "семафоров" на управляемых блокировках до глубокой архитектурной реструктуризации тяжелых расчетов вашей информационной системы.