При оптимизации регламентных расчетов и массовых загрузок разработчики часто сталкиваются с дилеммой: использовать построчную запись через РегистрСведенийМенеджерЗаписи или выполнять пакетное замещение через РегистрСведенийНаборЗаписей. Построчная запись на объемах в десятки тысяч строк работает медленно, а наивная пакетная схема «прочитал период — очистил/заместил — записал» открывает критическое окно конкурентного доступа (lost update), при котором параллельно внесенные пользователями или другими сеансами строки затираются без ошибок.
Разберем детально, почему возникает потеря данных на уровне СУБД и платформы, как корректно изолировать транзакции с помощью управляемых блокировок, как применять системный РежимЗамещения в актуальных версиях платформы 1С и как эффективно распараллеливать обработку больших массивов данных.
Проанализируем механику работы метода НаборЗаписей.Записать(Истина). На уровне СУБД (MS SQL Server, PostgreSQL) этот вызов всегда трансформируется в операцию полной очистки по отбору (DELETE FROM ... WHERE ...) с последующей вставкой новых строк через серию команд INSERT.
Если между операцией чтения данных набором (или формированием таблицы значений в памяти) и фактическим вызовом Записать(Истина) другой сеанс успевает добавить или изменить запись с тем же периодом, платформа при выполнении транзакции набора удалит чужую запись. Операция чтения НаборЗаписей.Прочитать() не накладывает долгосрочных блокировок на чтение в управляемом режиме блокировок, поэтому окно между чтением и записью остается открытым.
Для предотвращения неявной перезаписи чужих данных необходимо гарантировать, что диапазон измерений и период защищены от параллельных модификаций с самого начала расчета до фиксации транзакции. Снять управляемую блокировку до окончания транзакции невозможно — она удерживается до вызова ЗафиксироватьТранзакцию() или ОтменитьТранзакцию().
Разберем по шагам правильный шаблон установки управляемой блокировки:
НачатьТранзакцию().БлокировкаДанных и добавляем элемент блокировки для целевого регистра сведений в режиме РежимБлокировкиДанных.Исключительный.Заблокировать().РегистрСведенийНаборЗаписей с аналогичным отбором и вызываем Записать(Истина).ЗафиксироватьТранзакцию().Посмотрим на пример кода с защитой периода и разреза:
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ПоказателиНоменклатуры");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Период", РасчетнаяДата);
ЭлементБлокировки.УстановитьЗначение("Склад", ТекущийСклад);
Блокировка.Заблокировать();
Набор = РегистрыСведений.ПоказателиНоменклатуры.СоздатьНаборЗаписей();
Набор.Отбор.Период.Установить(РасчетнаяДата);
Набор.Отбор.Склад.Установить(ТекущийСклад);
// Заполнение набора рассчитанными данными
Для Каждого СтрДанных Из ТаблицаРасчета Цикл
Запись = Набор.Добавить();
ЗаполнитьЗначенияСвойств(Запись, СтрДанных);
Запись.Период = РасчетнаяДата;
Запись.Склад = ТекущийСклад;
КонецЦикла;
Набор.Записать(Истина);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Особенность управляемых блокировок по нескольким измерениям: если в регистре сведений три измерения, а блокировка установлена только по двум (например, Период и Склад), управляемая блокировка 1С для предотвращения фантомного чтения полностью блокирует пространство значений третьего неуказанного измерения (Номенклатура) для данной комбинации. Параллельная запись через МенеджерЗаписи с другим значением третьего измерения встанет в ожидание блокировки до завершения транзакции.
Начиная с современных версий платформы 1С:Предприятие 8.3, метод Записать() набора записей регистра сведений поддерживает параметр РежимЗамещения, исключающий деструктивное удаление данных по всему отбору:
РежимЗамещения.Слияние — выполняет точечную вставку новых строк и обновление существующих по совпадению ключевых полей. Записи с другими измерениями внутри того же периода не удаляются, что исключает потерю строк без необходимости блокировать весь период.РежимЗамещения.Обновление — обновляет значения ресурсов только у тех записей, чей ключ уже есть в регистре, не создавая новые строки.РежимЗамещения.Удаление — выполняет точечное удаление только тех строк, которые переданы в наборе.Посмотрим на пример использования точечного слияния данных:
Набор = РегистрыСведений.ПоказателиНоменклатуры.СоздатьНаборЗаписей();
Набор.Отбор.Период.Установить(РасчетнаяДата);
// Заполняем набор только изменившимися данными
Для Каждого Стр Из ТаблицаНовыхПоказателей Цикл
Запись = Набор.Добавить();
ЗаполнитьЗначенияСвойств(Запись, Стр);
Запись.Период = РасчетнаяДата;
КонецЦикла;
// Запись без очистки чужих измерений за дату
Набор.Записать(РежимЗамещения.Слияние);
При больших объемах (от десятков тысяч записей) удержание единой транзакции на весь объем данных приводит к росту размера журнала транзакций СУБД (LDF/WAL) и риску эскалации блокировок СУБД до уровня таблиц. В то же время запуск отдельного фонового задания на каждый единичный ключ порождает избыточные накладные расходы на создание сеансов.
Оптимальная стратегия параллелизации:
ХешФункция(Ссылка) % N).