В практике разработки высоконагруженных систем на платформе 1С:Предприятие, таких как «1С:Управление торговлей 11» (УТ 11), часто возникают задачи, требующие монопольного доступа к данным на короткий промежуток времени. Классический пример — подбор серий товаров с минимальным сроком годности (FEFO) или резервирование остатков под заказ.
Мы часто сталкиваемся с заблуждением относительно приоритетов режимов блокировок. Давайте разберем ситуацию, когда нам нужно исключить параллельное чтение и изменение остатков, при этом у самого регистра в свойствах стоит режим Автоматический, а вся конфигурация работает в режиме Управляемый. Как действовать правильно: менять режим регистра, использовать SQL-конструкции или применять штатные механизмы платформы?
Представим задачу: система получает файлы от клиента, на основании которых нужно сформировать заказы. Критически важно выбрать серии товаров с истекающим сроком годности. Если два менеджера или два фоновых задания одновременно начнут обрабатывать заказы с одной и той же номенклатурой, может возникнуть состояние «гонки» (race condition). Оба процесса прочитают одни и те же остатки, зарезервируют одну и ту же серию, и при записи возникнет ошибка (в лучшем случае) или пересорт/минус на складе (в худшем).
Разработчик видит, что у регистра ТоварыНаСкладе свойство «Режим управления блокировкой данных» установлено в значение Автоматический. При этом известно, что современные конфигурации (ERP, УТ 11, КА) работают в режиме Управляемый на уровне корня конфигурации.
Возникают вопросы:
ДЛЯ ИЗМЕНЕНИЯ (FOR UPDATE)?Давайте последовательно ответим на эти вопросы и выработаем верное решение.
Первое, что нам необходимо усвоить: главным является режим управления блокировками, установленный для всей Конфигурации.
Если в корне конфигурации свойство «Режим управления блокировкой данных» установлено в значение Управляемый, то платформа переводит работу с СУБД (SQL Server, PostgreSQL) в режим изоляции транзакций, который не накладывает блокировок при обычном чтении (обычно это READ COMMITTED или SNAPSHOT).
В этом случае свойство конкретного объекта (справочника, регистра), установленное в Автоматический, фактически игнорируется платформой в контексте автоматической трансляции в SQL-блокировки. Платформа ожидает, что разработчик сам возьмет на себя ответственность за логическую целостность данных с помощью менеджера транзакционных блокировок.
Вывод: Менять режим регистра ТоварыНаСкладе нет никакого смысла и даже вредно (это затруднит обновление типовой конфигурации). Нужно использовать инструменты управляемых блокировок.
ДЛЯ ИЗМЕНЕНИЯ — это плохая идея?Часто возникает соблазн использовать конструкцию языка запросов ДЛЯ ИЗМЕНЕНИЯ. Давайте разберем, почему в нашем случае это может быть ошибкой.
Конструкция ДЛЯ ИЗМЕНЕНИЯ транслируется в SQL-инструкцию FOR UPDATE (или аналоги). Это накладывает прямую блокировку на записи в таблице СУБД. Однако, менеджер управляемых блокировок 1С (объект БлокировкаДанных) «не знает» об этой блокировке.
Это создает риск классической взаимоблокировки (deadlock) между двумя транзакциями:
ДЛЯ ИЗМЕНЕНИЯ и пытается наложить управляемую блокировку.Поэтому в конфигурациях с управляемым режимом рекомендуется использовать исключительно объект БлокировкаДанных, не смешивая его с прямыми указаниями СУБД в запросах.
БлокировкаДанныхЧтобы решить задачу корректно, мы должны использовать явную установку управляемой блокировки. Рассмотрим этот процесс по шагам.
Любые блокировки имеют смысл и живут только в рамках транзакции. Как только транзакция завершается (фиксируется или отменяется), блокировка снимается. Поэтому наш код должен быть обернут в транзакцию.
В управляемом режиме мы блокируем не «таблицу целиком», а конкретный набор измерений. Это повышает параллельность работы. Если мы подбираем товар «Стол», нам не нужно блокировать работу с товаром «Стул».
Для регистра ТоварыНаСкладе ключевыми измерениями обычно являются:
НоменклатураХарактеристика (если используется)СкладСерия (если учет по сериям)Давайте напишем примерный алгоритм решения задачи автора. Мы сначала подготовим данные для блокировки (список товаров из файла), затем откроем транзакцию, заблокируем конкретные товары и только потом прочитаем остатки.
// Предположим, у нас есть ТаблицаИзФайла с колонками Номенклатура, Склад
// 1. Сначала подготовим данные, чтобы минимизировать время внутри транзакции
// 2. Начинаем транзакцию
НачатьТранзакцию();
Попытка
// 3. Создаем объект блокировки
Блокировка = Новый БлокировкаДанных;
// Указываем, какой регистр будем блокировать
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладе");
// 4. Устанавливаем режим блокировки
// "Исключительный" - запрещает другим и читать (с намерением блокировки), и писать.
// Это именно то, что нужно для предотвращения параллельного распределения.
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// 5. Устанавливаем отбор по измерениям из нашего источника данных
// Это критически важно для производительности!
// ИсточникДанных может быть ТаблицаЗначений, РезультатЗапроса и т.д.
ЭлементБлокировки.ИсточникДанных = ТаблицаИзФайла;
// Сопоставляем поля источника с измерениями регистра
ЭлементБлокировки.ИспользоватьИзИсточника("Номенклатура", "Номенклатура");
ЭлементБлокировки.ИспользоватьИзИсточника("Склад", "Склад");
// 6. Активируем блокировку
// В этот момент система проверит, не заняты ли эти ресурсы другими транзакциями.
// Если заняты - текущий поток встанет в ожидание.
Блокировка.Заблокировать();
// --- ЗОНА МОНОПОЛЬНОГО ДОСТУПА ---
// 7. Выполняем запрос к остаткам
// Теперь мы гарантированно читаем актуальные данные, которые никто не изменит
// до конца нашей транзакции.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладеОстатки.Серия КАК Серия,
| ТоварыНаСкладеОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладеОстатки.ВНаличииОстаток КАК Остаток,
| ТоварыНаСкладеОстатки.Серия.ГоденДо КАК СрокГодности
|ИЗ
| РегистрНакопления.ТоварыНаСкладе.Остатки(
| ,
| (Номенклатура, Склад) В
| (ВЫБРАТЬ
| Т.Номенклатура,
| Т.Склад
| ИЗ
| &ТаблицаТоваров КАК Т)
| ) КАК ТоварыНаСкладеОстатки
|
|УПОРЯДОЧИТЬ ПО
| СрокГодности"; // Наш FEFO алгоритм
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаИзФайла);
Результат = Запрос.Выполнить();
// 8. Обработка результатов, распределение серий и создание документов/записей
// ... код распределения ...
// 9. Фиксация транзакции
// Блокировка снимается автоматически
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Логирование ошибки
ЗаписьЖурналаРегистрации("ОбработкаФайлов", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
ВызватьИсключение; // Пробрасываем ошибку дальше
КонецПопытки;
В процессе обсуждения темы мы затронули несколько важных аспектов, которые стоит проговорить отдельно.
В управляемом режиме это не совсем так. Если вы установили Блокировка.Заблокировать(), вы запретили другим транзакциям:
Однако, если другой пользователь запустит отчет (простой SELECT к базе данных) без установки блокировок, он сможет прочитать данные ("грязное" чтение или чтение версии snapshot, в зависимости от СУБД). Но для нашей задачи распределения товара это не страшно — главное, что конкурирующий алгоритм проведения заказа тоже будет пытаться наложить блокировку и будет ждать, пока мы не закончим.
В конфигурациях уровня УТ 11, помимо физических остатков (ТоварыНаСкладе), существуют регистры, отвечающие за резервы и доступность (например, СвободныеОстатки или вычисляемые механизмы обеспечения). Если ваша задача — просто зарезервировать товар, возможно, стоит использовать штатные методы менеджера обеспечения, а не писать прямые запросы к складским остаткам. Однако, если задача специфична (выбор конкретной серии по сроку годности из файла), подход с управляемой блокировкой, описанный выше, является единственно верным.
НачатьТранзакцию() ... ЗафиксироватьТранзакцию().БлокировкаДанных для исключения параллельного доступа.