Как организовать очередь записи в регистр сведений и избежать ошибок блокировки данных?

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

В процессе разработки высоконагруженных систем на платформе 1С:Предприятие часто возникает ситуация, когда несколько пользователей одновременно пытаются записать данные в один и тот же РегистрСведений. Если при этом программный код рассчитывает некое уникальное значение (например, номер или идентификатор по принципу "Максимальный + 1"), возникают конфликты блокировок. Вместо ожидаемого ожидания своей очереди, пользователи получают сообщение об ошибке. В этой статье мы подробно разберем, как правильно настроить механизм блокировок, чтобы система выстраивала обращения в очередь и корректно обрабатывала транзакции.

Анализируем причину: почему блокировка не ждет?

Первым делом проанализируем типичную ошибку в коде. Часто разработчики пытаются установить блокировку до начала транзакции. В режиме управляемых блокировок вызов Блокировка.Заблокировать() вне транзакции либо не имеет смысла, либо работает некорректно. Кроме того, если система мгновенно выдает ошибку, это может свидетельствовать о возникновении Deadlock (взаимоблокировки). Это ситуация, когда два процесса заблокировали ресурсы и бесконечно ждут друг друга. Система 1С или СУБД обнаруживает этот цикл и принудительно прерывает одну из транзакций.

Рассмотрим также стандартное время ожидания. По умолчанию платформа ждет освобождения ресурса 20 секунд. Если ошибка "Объект заблокирован" появляется раньше или сразу, значит, порядок обращения к объектам в коде нарушен. Наша задача — сделать блокировку максимально короткой и точечной.

Решение 1: Исправление порядка вызова транзакции и блокировки

Разберем правильный паттерн работы с объектом БлокировкаДанных. Блокировка должна устанавливаться строго внутри транзакции. Если мы сначала вызываем блокировку, а потом НачатьТранзакцию(), мы нарушаем логику работы менеджера блокировок.

Правильный алгоритм действий выглядит следующим образом:

  1. Открываем транзакцию.
  2. Создаем объект блокировки и описываем область (конкретный регистр).
  3. Устанавливаем режим Исключительный, чтобы никто другой не мог начать чтение или запись в этот момент.
  4. Вызываем метод Заблокировать().
  5. Выполняем расчет номера (Чтение Max + 1).
  6. Записываем данные.
  7. Фиксируем транзакцию.

Посмотрим на пример правильного кода:


НачатьТранзакцию();
Попытка
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ДопСведения");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    
    // Блокируем весь регистр или конкретный диапазон
    Блокировка.Заблокировать();
    
    // Теперь мы в безопасности, другие сеансы будут ждать в очереди здесь
    Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(Номер) ИЗ РегистрСведений.ДопСведения");
    Результат = Запрос.Выполнить();
    // ... логика получения нового номера и записи ...
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ЗаписьЖурналаРегистрации("Ошибка записи", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
    ВызватьИсключение "Не удалось записать данные из-за высокой нагрузки. Попробуйте еще раз.";
КонецПопытки;

Решение 2: Использование вспомогательного объекта (узкое горлышко)

Блокировка всего регистра — это радикальный метод, который может замедлить работу всей системы ("антипаттерн"). Проанализируем более изящный способ: блокировку вспомогательного объекта. Вместо того чтобы закрывать доступ ко всему регистру ДопСведения, мы можем блокировать одну конкретную запись в специальном маленьком регистре (назовем его Счетчики) или использовать Константу.

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

Рассмотрим пример с константой:


НачатьТранзакцию();
// Блокируем константу, которая служит "ключом" для очереди
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Константа.ПоследнийИД");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

// Получаем значение, увеличиваем и записываем обратно
НовыйНомер = Константы.ПоследнийИД.Получить() + 1;
Константы.ПоследнийИД.Установить(НовыйНомер);

// Используем полученный номер для записи в основной регистр
// ... логика записи ...

ЗафиксироватьТранзакцию();

Решение 3: Оптимизация записи больших массивов данных

Проанализируем ситуацию, когда за один раз нужно добавить сотни записей. Если делать расчет Max + 1 внутри цикла для каждой строки, время транзакции увеличивается в геометрической прогрессии, что неизбежно ведет к ошибкам блокировки у других пользователей.

Рекомендуется использовать следующую стратегию:

  1. Один раз войти в транзакцию и установить блокировку.
  2. Одним запросом прочитать текущий максимальный идентификатор.
  3. В оперативной памяти (в массиве или таблице значений) подготовить все записи, инкрементируя номер.
  4. Записать весь массив данных за один вызов НаборЗаписей.Записать().
  5. Закрыть транзакцию.

Это сокращает время удержания блокировки с нескольких секунд до миллисекунд.

Решение 4: Программная очередь через "Попытку" и цикл

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

Посмотрим, как реализовать цикл повторений:


УспешноЗаписано = Ложь;
КоличествоПопыток = 0;

Пока Не УспешноЗаписано И КоличествоПопыток < 5 Цикл
    НачатьТранзакцию();
    Попытка
        // Установка блокировки и логика записи
        // ...
        ЗафиксироватьТранзакцию();
        УспешноЗаписано = Истина;
    Исключение
        ОтменитьТранзакцию();
        КоличествоПопыток = КоличествоПопыток + 1;
        // Пауза перед следующей попыткой (имитация через задержку)
        // Для серверного кода можно использовать небольшую паузу
    КонецПопытки;
КонецЦикла;

Если Не УспешноЗаписано Тогда
    ВызватьИсключение "Превышено количество попыток доступа к регистру.";
КонецЕсли;

Решение 5: Переход к справочникам для автонумерации

Выясним, является ли РегистрСведений оптимальным местом для хранения данных с уникальными номерами. Если ваша задача — просто хранить записи с уникальным кодом, рассмотрите использование Справочника. У справочников механизм генерации кодов оптимизирован на уровне платформы. При включенном свойстве Автонумерация и режиме Освобождать автоматически, 1С получает новый номер крайне быстро, используя внутренние кэшированные последовательности СУБД, что практически исключает конфликты при генерации ID.

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

← На главную