Когда мы разрабатываем решения на платформе «1С:Предприятие», часто сталкиваемся с необходимостью генерации уникальных последовательных номеров для различных объектов, например, документов или элементов справочников. Особенно остро этот вопрос встает при работе с регистрами сведений, которые хранят эти номера. В условиях высокой конкуренции при одновременной работе нескольких пользователей, обеспечение уникальности и последовательности номеров, а также минимизация задержек, вызванных блокировками, становятся критически важными задачами.
Представим типовую ситуацию, описанную автором: у нас есть условно-длинная транзакция по созданию некоего документа. Внутри этой транзакции, возможно, в цикле, происходит обращение к регистру сведений, хранящему порядковые номера объектов, для получения следующего номера. Если для такого объекта номера еще нет, то добавляется запись в регистр с максимальным номером по регистру плюс один.
Чтобы разные сеансы не получили один и тот же номер при одновременном обращении, перед чтением регистра автор устанавливает исключительную блокировку на весь регистр (весь, так как необходимо получить максимальный номер по всему регистру). Проблема заключается в том, что после получения нового номера и его записи в регистр, держать его заблокированным на протяжении всей остальной части длительной транзакции, в общем-то, не нужно. Даже если транзакция будет отменена, пропуски номеров не являются критичными.
Основной вопрос заключается в следующем: есть ли какой-то способ снять блокировку регистра, не дожидаясь завершения основной транзакции? Давайте вместе разберем эту проблему и найдем оптимальные подходы к ее решению, учитывая особенности управляемых блокировок в 1С.
Прежде чем перейти к решениям, давайте кратко рассмотрим, как работают управляемые блокировки в платформе 1С, поскольку именно их особенности лежат в основе нашей проблемы.
ДляЧтения) позволяет другим транзакциям читать заблокированные данные, но запрещает их изменение. Она устанавливается, например, при простом чтении данных из регистра с помощью запроса, который не содержит конструкции ДЛЯ ИЗМЕНЕНИЯ.ДляИзменения) полностью запрещает как чтение, так и изменение заблокированных данных для других транзакций. Именно такую блокировку мы обычно используем, когда нам нужно изменить данные, например, записать новый порядковый номер в регистр. Для установки такой блокировки в запросе используется конструкция ДЛЯ ИЗМЕНЕНИЯ, а при работе с объектной моделью — методы записи объектов или наборов записей.НаборЗаписей.Прочитать(). При использовании объектной модели, например, при вызове метода НаборЗаписей.Прочитать() для регистров сведений, накопления, бухгалтерии, расчета и последовательностей, всегда устанавливается управляемая разделяемая блокировка. Эта блокировка также длится до конца транзакции. Если нам нужны данные только для чтения и мы хотим минимизировать блокировки, часто рекомендуется использовать запросы вместо объектной модели.Таким образом, мы видим, что основная причина невозможности досрочного снятия блокировки заключается в архитектуре работы транзакций и управляемых блокировок в 1С. Они неразрывно связаны с жизненным циклом транзакции, гарантируя целостность и изоляцию данных на протяжении всего ее выполнения.
Как мы уже выяснили, и как было подтверждено в ходе обсуждения на форуме, штатных механизмов для снятия управляемой блокировки с объекта (например, регистра) внутри незавершенной транзакции не существует. Блокировка привязывается к контексту транзакции и автоматически освобождается только после ее фиксации (ЗафиксироватьТранзакцию()) или отмены (ОтменитьТранзакцию()). Попытка обойти это ограничение может привести к нарушению целостности данных, так как платформа гарантирует изоляцию изменений в рамках одной транзакции.
Представьте, что транзакция в процессе изменения данных может снять блокировку. Тогда другая транзакция могла бы увидеть или изменить данные, которые еще не были окончательно зафиксированы первой транзакцией. Это привело бы к нарушению принципов ACID (атомарность, согласованность, изоляция, долговечность), на которых строятся надежные системы управления базами данных.
Наиболее эффективным и безопасным подходом к решению нашей проблемы, как было предложено и поддержано на форуме (сообщения 2, 14, 15), является выделение операции получения следующего порядкового номера и его записи в отдельную, максимально короткую транзакцию. Давайте рассмотрим этот подход подробнее.
Разберем, как это можно реализовать на практике:
НачатьТранзакцию() для инициирования новой транзакции, которая будет отвечать только за получение номера.ДЛЯ ИЗМЕНЕНИЯ или при работе с НаборЗаписей и последующей записью). Это гарантирует, что никакой другой сеанс не сможет одновременно получить тот же номер. Находим максимальный номер по интересующему нас объекту или по всему регистру, в зависимости от логики нумерации, которую мы хотим реализовать.ЗафиксироватьТранзакцию(). В этот момент управляемая исключительная блокировка с регистра номеров немедленно снимается, и ресурс становится доступен для других сеансов.Давайте посмотрим, как это может выглядеть в коде 1С. Предположим, у нас есть регистр сведений НомераОбъектов с измерениями ВидОбъекта и Номер, где ВидОбъекта определяет, для какого типа сущностей мы выдаем номера.
// Функция для получения следующего уникального номера
Функция ПолучитьСледующийНомер(Знач ВидОбъекта) Экспорт
Перем СледующийНомер;
// *** КОРОТКАЯ ТРАНЗАКЦИЯ ДЛЯ ПОЛУЧЕНИЯ НОМЕРА ***
НачатьТранзакцию();
Попытка
// 1. Устанавливаем блокировку данных для регистра НомераОбъектов.
// Мы блокируем по ВидуОбъекта, если нумерация сквозная по виду,
// или по всему регистру, если нужен глобальный максимальный номер.
// Для примера, блокируем по ВидуОбъекта.
БлокировкаДанных = Новый БлокировкаДанных();
ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.НомераОбъектов");
ЭлементБлокировки.УстановитьРежим(РежимБлокировкиДанных.Исключительный);
// Устанавливаем условие блокировки по измерению, чтобы не блокировать весь регистр,
// если это возможно по логике нумерации.
// Если номера сквозные по всему регистру, то условие не задаем, блокируется весь регистр.
ЭлементБлокировки.УстановитьЗначение("ВидОбъекта", ВидОбъекта);
БлокировкаДанных.Заблокировать();
// 2. Получаем текущий максимальный номер для данного вида объекта
Запрос = Новый Запрос();
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(НомераОбъектов.Номер) КАК МаксимальныйНомер
|ИЗ
| РегистрСведений.НомераОбъектов КАК НомераОбъектов
|ГДЕ
| НомераОбъектов.ВидОбъекта = &ВидОбъекта";
Запрос.УстановитьПараметр("ВидОбъекта", ВидОбъекта);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() И Выборка.МаксимальныйНомер <> NULL Тогда
СледующийНомер = Выборка.МаксимальныйНомер + 1;
Иначе
// Если номеров для этого вида объекта еще нет, начинаем с 1
СледующийНомер = 1;
КонецЕсли;
// 3. Записываем новый номер в регистр
// Создаем набор записей для регистра НомераОбъектов
НаборЗаписей = РегистрыСведений.НомераОбъектов.СоздатьНаборЗаписей();
// Устанавливаем отбор по ВидуОбъекта, чтобы работать только с нужными записями
НаборЗаписей.Отбор.ВидОбъекта.Установить(ВидОбъекта);
// Прочитываем существующие записи, чтобы их обновить или создать новые
НаборЗаписей.Прочитать();
// В простейшем случае, если регистр хранит *текущий* максимальный номер,
// то мы либо обновляем существующую запись, либо добавляем новую.
Если НаборЗаписей.Количество() > 0 Тогда
// Предполагаем, что запись для данного ВидаОбъекта одна и хранит текущий макс. номер
Запись = НаборЗаписей[0];
Запись.Номер = СледующийНомер;
Иначе
// Если записи не существует, создаем новую
Запись = НаборЗаписей.Добавить();
Запись.ВидОбъекта = ВидОбъекта;
Запись.Номер = СледующийНомер;
КонецЕсли;
НаборЗаписей.Записать(Истина); // Истина - записывать с замещением, если запись уже есть
ЗафиксироватьТранзакцию(); // *** ФИКСАЦИЯ КОРОТКОЙ ТРАНЗАКЦИИ ***
Возврат СледующийНомер;
Исключение
// В случае ошибки обязательно отменяем транзакцию
ОтменитьТранзакцию();
ВызватьИсключение "Ошибка при получении следующего номера: " + ОписаниеОшибки();
КонецПопытки;
КонецФункции
// *** ПРИМЕР ИСПОЛЬЗОВАНИЯ В ОСНОВНОЙ ДЛИТЕЛЬНОЙ ТРАНЗАКЦИИ ***
// Этот код будет вызываться, например, в обработчике ПриЗаписи или ПередЗаписью документа
Процедура ПриЗаписиДокумента()
НачатьТранзакцию();
Попытка
// Получаем номер через короткую транзакцию,
// которая уже зафиксирована к моменту возврата значения
НовыйНомерДокумента = ПолучитьСледующийНомер(Перечисления.ВидыОбъектовНумерации.МойДокумент);
// Далее выполняем длительные операции по созданию и заполнению документа.
// Регистр НомераОбъектов уже не заблокирован основной транзакцией!
ДокументОбъект = ЭтотОбъект; // Предполагаем, что это метод объекта документа
ДокументОбъект.Номер = НовыйНомерДокумента;
// ... Заполнение других реквизитов, обращение к другим регистрам,
// выполнение сложных расчетов, которые могут занять много времени.
// Записываем и проводим документ в рамках основной транзакции
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию(); // Фиксация основной длительной транзакции
Исключение
// В случае ошибки основной транзакции - отменяем ее
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Обратите внимание, что в примере выше мы использовали явные вызовы НачатьТранзакцию(), ЗафиксироватьТранзакцию() и ОтменитьТранзакцию() для управления обеими транзакциями. Это позволяет нам строго контролировать их границы и время жизни блокировок. Также важно отметить использование Знач для параметра функции ПолучитьСледующийНомер, чтобы избежать передачи объекта по ссылке и возможных нежелательных изменений вне функции.
Помимо ручного управления транзакциями, платформа 1С предлагает и другие механизмы, которые могут быть полезны в зависимости от конкретных требований к нумерации. Мы рассмотрим их, чтобы иметь более полное представление о возможностях платформы.
Платформа «1С:Предприятие» имеет встроенные механизмы автоматической нумерации для таких объектов, как документы, справочники и некоторые другие. Эти механизмы оптимизированы для обеспечения высокой параллельности и могут работать в различных режимах.
ПолучитьНовыйНомер() или УстановитьНовыйНомер(). Для документов и справочников, у которых установлен режим автонумерации, мы можем просто использовать метод ПолучитьНовыйНомер() (для справочников) или УстановитьНовыйНомер() (для документов). Платформа сама позаботится о блокировках и выдаче уникального номера, используя свои внутренние оптимизированные механизмы.
// Пример получения номера для документа с автонумерацией
// В форме документа или модуле объекта:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если Новый Тогда // Если документ новый
ЭтотОбъект.УстановитьНовыйНомер(); // Платформа сама запросит и установит новый номер
КонецЕсли;
КонецПроцедуры
Однако важно понимать, что стандартные механизмы автонумерации работают преимущественно для свойств Номер стандартных объектов. В нашем случае, когда речь идет о регистре сведений, хранящем порядковые номера некоторых объектов, может потребоваться более гибкий подход, такой как разделение транзакций, который мы рассмотрели выше.
Хотя в исходной теме речь идет о регистрах сведений для получения произвольных номеров, для документов, критически важных для хронологии и целостности учета (например, партийный учет, последовательность взаиморасчетов), можно рассмотреть использование объекта "Последовательности". Последовательности в 1С предназначены для контроля правильности внесения изменений документами в учетные данные и помогают восстанавливать хронологию при сбоях или отмене проведения. Они не являются прямым механизмом для выдачи уникальных номеров в том смысле, как это требуется в исходной задаче, но обеспечивают другую форму "нумерации" – контроль хронологического порядка проведения.
Последовательности позволяют нам регистрировать границы проведения документов, что является важным аспектом для многих видов учета, где важен порядок ввода данных. Работа с последовательностями не отменяет необходимости в генерации уникальных номеров для самих документов, но дополняет систему контроля целостности.
Если мы все же вынуждены получать максимальный номер из регистра сведений в рамках короткой транзакции, всегда следует убедиться, что запрос максимально эффективен. Для получения именно максимального значения (а не просто количества записей), всегда используйте агрегатную функцию МАКСИМУМ() в запросе. Например: ВЫБРАТЬ МАКСИМУМ(ИмяРегистра.Номер) ИЗ РегистрСведений.ИмяРегистра КАК ИмяРегистра. Также важно убедиться, что по измерениям, используемым в условиях отбора или группировки, созданы необходимые индексы в СУБД для ускорения поиска. Это критически важно для производительности короткой транзакции.
В ходе обсуждения на форуме были предложены и такие идеи, которые, на наш взгляд, следует избегать из-за их потенциальных негативных последствий.
В целом, запуск фонового задания с ожиданием его завершения – это антипаттерн, который сводит на нет все преимущества асинхронной работы и создает дополнительные риски для стабильности и производительности системы.
Мы выяснили, что стандартных механизмов для досрочного снятия управляемой блокировки с регистра внутри незавершенной транзакции в 1С:Предприятии не предусмотрено. Это является фундаментальной особенностью работы транзакций и системы блокировок платформы, обеспечивающей целостность и согласованность данных.
Однако, для эффективного решения задачи получения уникальных порядковых номеров при длительных транзакциях, наиболее правильным и производительным подходом является выделение операции получения номера в отдельную, максимально короткую транзакцию. Этот метод позволяет значительно повысить параллельность работы системы, минимизируя время блокировки критического ресурса.
Всегда стремитесь к тому, чтобы ваши транзакции были максимально короткими, особенно те, которые устанавливают исключительные блокировки на высококонкурентные ресурсы. Это золотое правило разработки высокопроизводительных решений на платформе «1С:Предприятие». Избегайте использования сложных, неоптимизированных подходов, таких как запуск фоновых заданий с ожиданием внутри основной транзакции, чтобы не создавать дополнительные проблемы для производительности и стабильности вашей информационной системы.