Как эффективно получать уникальные номера и управлять блокировками в 1С при длительных транзакциях?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Когда мы разрабатываем решения на платформе «1С:Предприятие», часто сталкиваемся с необходимостью генерации уникальных последовательных номеров для различных объектов, например, документов или элементов справочников. Особенно остро этот вопрос встает при работе с регистрами сведений, которые хранят эти номера. В условиях высокой конкуренции при одновременной работе нескольких пользователей, обеспечение уникальности и последовательности номеров, а также минимизация задержек, вызванных блокировками, становятся критически важными задачами.

Представим типовую ситуацию, описанную автором: у нас есть условно-длинная транзакция по созданию некоего документа. Внутри этой транзакции, возможно, в цикле, происходит обращение к регистру сведений, хранящему порядковые номера объектов, для получения следующего номера. Если для такого объекта номера еще нет, то добавляется запись в регистр с максимальным номером по регистру плюс один.

Чтобы разные сеансы не получили один и тот же номер при одновременном обращении, перед чтением регистра автор устанавливает исключительную блокировку на весь регистр (весь, так как необходимо получить максимальный номер по всему регистру). Проблема заключается в том, что после получения нового номера и его записи в регистр, держать его заблокированным на протяжении всей остальной части длительной транзакции, в общем-то, не нужно. Даже если транзакция будет отменена, пропуски номеров не являются критичными.

Основной вопрос заключается в следующем: есть ли какой-то способ снять блокировку регистра, не дожидаясь завершения основной транзакции? Давайте вместе разберем эту проблему и найдем оптимальные подходы к ее решению, учитывая особенности управляемых блокировок в 1С.

Понимание управляемых блокировок в 1С:Предприятие

Прежде чем перейти к решениям, давайте кратко рассмотрим, как работают управляемые блокировки в платформе 1С, поскольку именно их особенности лежат в основе нашей проблемы.

  1. Автоматические блокировки СУБД и управляемые блокировки 1С:Предприятия. В «1С:Предприятии» существуют два уровня блокировок: блокировки СУБД (автоматические, которые устанавливаются непосредственно системой управления базами данных) и блокировки 1С:Предприятия (управляемые). В управляемом режиме платформа берет управление параллельностью на себя, что позволяет нам блокировать данные более точно – не таблицу целиком, а конкретные записи по значениям измерений регистров, что значительно повышает производительность.
  2. Разделяемые и исключительные блокировки.
    • Разделяемая блокировка (ДляЧтения) позволяет другим транзакциям читать заблокированные данные, но запрещает их изменение. Она устанавливается, например, при простом чтении данных из регистра с помощью запроса, который не содержит конструкции ДЛЯ ИЗМЕНЕНИЯ.
    • Исключительная блокировка (ДляИзменения) полностью запрещает как чтение, так и изменение заблокированных данных для других транзакций. Именно такую блокировку мы обычно используем, когда нам нужно изменить данные, например, записать новый порядковый номер в регистр. Для установки такой блокировки в запросе используется конструкция ДЛЯ ИЗМЕНЕНИЯ, а при работе с объектной моделью — методы записи объектов или наборов записей.
  3. Время жизни блокировки. Ключевой аспект нашей проблемы – это время жизни управляемой блокировки. Управляемые блокировки устанавливаются и снимаются только в рамках транзакции. Это означает, что время жизни блокировки равно времени жизни транзакции. Блокировка снимается только при фиксации или откате всей транзакции. Этот факт прямо подтверждается в обсуждаемой теме на форуме: на вопрос «Ну штатных механизмов именно снятия блокировки внутри транзакции не существует, как я понимаю?» был дан однозначный ответ «да».
  4. Метод НаборЗаписей.Прочитать(). При использовании объектной модели, например, при вызове метода НаборЗаписей.Прочитать() для регистров сведений, накопления, бухгалтерии, расчета и последовательностей, всегда устанавливается управляемая разделяемая блокировка. Эта блокировка также длится до конца транзакции. Если нам нужны данные только для чтения и мы хотим минимизировать блокировки, часто рекомендуется использовать запросы вместо объектной модели.

Таким образом, мы видим, что основная причина невозможности досрочного снятия блокировки заключается в архитектуре работы транзакций и управляемых блокировок в 1С. Они неразрывно связаны с жизненным циклом транзакции, гарантируя целостность и изоляцию данных на протяжении всего ее выполнения.

Почему нельзя снять блокировку раньше завершения транзакции?

Как мы уже выяснили, и как было подтверждено в ходе обсуждения на форуме, штатных механизмов для снятия управляемой блокировки с объекта (например, регистра) внутри незавершенной транзакции не существует. Блокировка привязывается к контексту транзакции и автоматически освобождается только после ее фиксации (ЗафиксироватьТранзакцию()) или отмены (ОтменитьТранзакцию()). Попытка обойти это ограничение может привести к нарушению целостности данных, так как платформа гарантирует изоляцию изменений в рамках одной транзакции.

Представьте, что транзакция в процессе изменения данных может снять блокировку. Тогда другая транзакция могла бы увидеть или изменить данные, которые еще не были окончательно зафиксированы первой транзакцией. Это привело бы к нарушению принципов ACID (атомарность, согласованность, изоляция, долговечность), на которых строятся надежные системы управления базами данных.

Оптимальное решение: выделение получения номера в отдельную короткую транзакцию

Наиболее эффективным и безопасным подходом к решению нашей проблемы, как было предложено и поддержано на форуме (сообщения 2, 14, 15), является выделение операции получения следующего порядкового номера и его записи в отдельную, максимально короткую транзакцию. Давайте рассмотрим этот подход подробнее.

  1. Суть подхода. Вместо того чтобы включать всю логику получения номера в вашу основную, длительную транзакцию по созданию документа, мы выполняем эту небольшую операцию (чтение максимального номера, его инкремент и запись нового значения) в своей собственной, независимой транзакции. Эта мини-транзакция будет очень короткой, что значительно снизит время блокировки регистра номеров. После фиксации этой мини-транзакции, основной длинной транзакции доступен уже полученный уникальный номер, а блокировка на регистр номеров снимается.
  2. Преимущества.
    • Значительное повышение параллельности работы. Ресурс (регистр с порядковыми номерами) будет заблокирован на минимальное время, что позволит другим сеансам практически мгновенно получить свой номер, не дожидаясь завершения длительной основной транзакции.
    • Сохранение целостности данных. Несмотря на разделение транзакций, уникальность и последовательность номеров гарантируется благодаря короткой исключительной блокировке внутри мини-транзакции.
    • Чистота архитектуры. Отделение логики получения номеров делает код более модульным, понятным и тестируемым, поскольку функция получения номера становится независимой единицей.
  3. Пошаговая реализация.

    Разберем, как это можно реализовать на практике:

    1. Начинаем первую (короткую) транзакцию. Мы используем явный вызов НачатьТранзакцию() для инициирования новой транзакции, которая будет отвечать только за получение номера.
    2. Получаем текущий максимальный номер. Внутри этой короткой транзакции мы обращаемся к регистру сведений, устанавливая на него исключительную блокировку (с помощью запроса ДЛЯ ИЗМЕНЕНИЯ или при работе с НаборЗаписей и последующей записью). Это гарантирует, что никакой другой сеанс не сможет одновременно получить тот же номер. Находим максимальный номер по интересующему нас объекту или по всему регистру, в зависимости от логики нумерации, которую мы хотим реализовать.
    3. Инкрементируем номер и записываем новое значение. Увеличиваем полученный максимальный номер на единицу и записываем его обратно в регистр сведений. Если записи для такого объекта еще не было, создаем новую.
    4. Фиксируем первую (короткую) транзакцию. Сразу после успешной записи нового номера вызываем ЗафиксироватьТранзакцию(). В этот момент управляемая исключительная блокировка с регистра номеров немедленно снимается, и ресурс становится доступен для других сеансов.
    5. Начинаем вторую (основную) транзакцию. Теперь, когда у нас есть гарантированно уникальный номер, мы можем начать основную длительную транзакцию по созданию документа, используя уже полученный номер.
    6. Выполняем все остальные операции. Внутри этой длительной транзакции мы уже не блокируем регистр номеров, что позволяет ей выполняться дольше без негативного влияния на параллельную работу с системой нумерации.
  4. Пример концептуального кода:

    Давайте посмотрим, как это может выглядеть в коде 1С. Предположим, у нас есть регистр сведений НомераОбъектов с измерениями ВидОбъекта и Номер, где ВидОбъекта определяет, для какого типа сущностей мы выдаем номера.

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

    Обратите внимание, что в примере выше мы использовали явные вызовы НачатьТранзакцию(), ЗафиксироватьТранзакцию() и ОтменитьТранзакцию() для управления обеими транзакциями. Это позволяет нам строго контролировать их границы и время жизни блокировок. Также важно отметить использование Знач для параметра функции ПолучитьСледующийНомер, чтобы избежать передачи объекта по ссылке и возможных нежелательных изменений вне функции.

Альтернативные подходы к генерации уникальных номеров

Помимо ручного управления транзакциями, платформа 1С предлагает и другие механизмы, которые могут быть полезны в зависимости от конкретных требований к нумерации. Мы рассмотрим их, чтобы иметь более полное представление о возможностях платформы.

  1. Использование стандартных механизмов автонумерации 1С.

    Платформа «1С:Предприятие» имеет встроенные механизмы автоматической нумерации для таких объектов, как документы, справочники и некоторые другие. Эти механизмы оптимизированы для обеспечения высокой параллельности и могут работать в различных режимах.

    • Менеджер автонумерации. В современных версиях 1С реализован специальный менеджер автонумерации. Он разработан таким образом, чтобы минимизировать блокировки базы данных, выдавая номера эффективно даже при высокой нагрузке. Он может работать как с возвратом неиспользованных номеров (если требуется строгая последовательность без пропусков), так и без возврата (если пропуски допустимы).
    • Префиксы. Механизм автонумерации поддерживает использование префиксов (например, префиксы организации, информационной базы, структурного подразделения), что позволяет создавать уникальные номера в различных сегментах системы.
    • Метод ПолучитьНовыйНомер() или УстановитьНовыйНомер(). Для документов и справочников, у которых установлен режим автонумерации, мы можем просто использовать метод ПолучитьНовыйНомер() (для справочников) или УстановитьНовыйНомер() (для документов). Платформа сама позаботится о блокировках и выдаче уникального номера, используя свои внутренние оптимизированные механизмы.
    
    // Пример получения номера для документа с автонумерацией
    // В форме документа или модуле объекта:
    Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
        Если Новый Тогда // Если документ новый
            ЭтотОбъект.УстановитьНовыйНомер(); // Платформа сама запросит и установит новый номер
        КонецЕсли;
    КонецПроцедуры
    

    Однако важно понимать, что стандартные механизмы автонумерации работают преимущественно для свойств Номер стандартных объектов. В нашем случае, когда речь идет о регистре сведений, хранящем порядковые номера некоторых объектов, может потребоваться более гибкий подход, такой как разделение транзакций, который мы рассмотрели выше.

  2. Использование объекта "Последовательности" (для документов).

    Хотя в исходной теме речь идет о регистрах сведений для получения произвольных номеров, для документов, критически важных для хронологии и целостности учета (например, партийный учет, последовательность взаиморасчетов), можно рассмотреть использование объекта "Последовательности". Последовательности в 1С предназначены для контроля правильности внесения изменений документами в учетные данные и помогают восстанавливать хронологию при сбоях или отмене проведения. Они не являются прямым механизмом для выдачи уникальных номеров в том смысле, как это требуется в исходной задаче, но обеспечивают другую форму "нумерации" – контроль хронологического порядка проведения.

    Последовательности позволяют нам регистрировать границы проведения документов, что является важным аспектом для многих видов учета, где важен порядок ввода данных. Работа с последовательностями не отменяет необходимости в генерации уникальных номеров для самих документов, но дополняет систему контроля целостности.

  3. Оптимизация запроса на максимальный номер.

    Если мы все же вынуждены получать максимальный номер из регистра сведений в рамках короткой транзакции, всегда следует убедиться, что запрос максимально эффективен. Для получения именно максимального значения (а не просто количества записей), всегда используйте агрегатную функцию МАКСИМУМ() в запросе. Например: ВЫБРАТЬ МАКСИМУМ(ИмяРегистра.Номер) ИЗ РегистрСведений.ИмяРегистра КАК ИмяРегистра. Также важно убедиться, что по измерениям, используемым в условиях отбора или группировки, созданы необходимые индексы в СУБД для ускорения поиска. Это критически важно для производительности короткой транзакции.

Не рекомендуемые подходы

В ходе обсуждения на форуме были предложены и такие идеи, которые, на наш взгляд, следует избегать из-за их потенциальных негативных последствий.

  1. Фоновое задание внутри транзакции с ожиданием. Идея запустить фоновое задание внутри основной транзакции и ждать его завершения для получения номера (как было предложено в сообщении 3 и подтверждено в сообщении 6) является крайне нежелательной. Давайте проанализируем, почему этот подход считается «антипаттерном» (как было метко отмечено в сообщении 7):
    • Замедление работы и блокировка. Ожидание завершения фонового задания внутри синхронного кода приводит к тому, что основной поток выполнения будет заблокирован, что равносильно выполнению тяжелой операции синхронно, но с дополнительными накладными расходами на запуск фонового задания, управление его состоянием и ожидание результата. Мы не получаем выгоды от асинхронности, но добавляем сложности.
    • Увеличение нагрузки на сервер. Каждое фоновое задание потребляет ресурсы сервера (процессорное время, память, лицензии). При высокой конкуренции и частом запуске таких заданий это может быстро исчерпать доступные ресурсы, что приведет к деградации производительности всей системы.
    • Возникновение взаимоблокировок (Deadlocks). Если фоновое задание также будет работать с данными, заблокированными основной транзакцией (или наоборот), или будет пытаться получить свою собственную блокировку на те же ресурсы, это может легко привести к взаимоблокировкам. Взаимоблокировки крайне сложно отлаживать, и они полностью блокируют работу затронутых сеансов до принудительного прерывания транзакций СУБД.
    • Сложность отката и управления транзакциями. В случае ошибки внутри фонового задания или основной транзакции, корректная обработка отката и обеспечение целостности данных становятся значительно сложнее. Управление несколькими транзакциями (основной и неявно созданной фоновым заданием) и их связанными блокировками становится нетривиальной задачей.
    • Нарушение принципов изоляции. В зависимости от реализации, фоновое задание может работать в своем собственном контексте транзакции, и его изменения могут быть зафиксированы независимо от основной транзакции. Это может нарушить принцип атомарности основной операции, если номер получается в фоновом задании, а затем основная транзакция отменяется.

    В целом, запуск фонового задания с ожиданием его завершения – это антипаттерн, который сводит на нет все преимущества асинхронной работы и создает дополнительные риски для стабильности и производительности системы.

Подведем итоги

Мы выяснили, что стандартных механизмов для досрочного снятия управляемой блокировки с регистра внутри незавершенной транзакции в 1С:Предприятии не предусмотрено. Это является фундаментальной особенностью работы транзакций и системы блокировок платформы, обеспечивающей целостность и согласованность данных.

Однако, для эффективного решения задачи получения уникальных порядковых номеров при длительных транзакциях, наиболее правильным и производительным подходом является выделение операции получения номера в отдельную, максимально короткую транзакцию. Этот метод позволяет значительно повысить параллельность работы системы, минимизируя время блокировки критического ресурса.

Всегда стремитесь к тому, чтобы ваши транзакции были максимально короткими, особенно те, которые устанавливают исключительные блокировки на высококонкурентные ресурсы. Это золотое правило разработки высокопроизводительных решений на платформе «1С:Предприятие». Избегайте использования сложных, неоптимизированных подходов, таких как запуск фоновых заданий с ожиданием внутри основной транзакции, чтобы не создавать дополнительные проблемы для производительности и стабильности вашей информационной системы.

← На главную