Как в запросе 1С пропустить заблокированные записи и не ждать их освобождения?

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

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

Рассмотрим, как обойти это ожидание и заставить систему сразу переходить к следующей свободной записи, реализуя аналог логики SKIP LOCKED, известной в других СУБД.

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

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

  1. Начинает транзакцию.
  2. Выполняет запрос ВЫБРАТЬ ПЕРВЫЕ 1 ..., чтобы найти одну свободную запись (например, заказ с незаполненным полем Пользователь).
  3. Получив ссылку на заказ, пытается установить на него управляемую блокировку через объект БлокировкаДанных.
  4. Если блокировка успешна, изменяет запись (заполняет поле Пользователь) и фиксирует транзакцию.

Проблема возникает, когда два сеанса практически одновременно выполняют шаг 2. Оба получают ссылку на один и тот же "Заказ №1". Первый сеанс успевает дойти до шага 3 и установить блокировку. Второй сеанс, пытаясь заблокировать тот же "Заказ №1", встает в очередь и ждет, пока первый сеанс не завершит свою транзакцию. Это ожидание и является основной проблемой.

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

Решение 1: Выборка пакета записей с попыткой блокировки по таймауту

Этот метод основан на идее получить сразу несколько потенциально свободных задач и в цикле пытаться "захватить" первую доступную. Это самый простой в реализации способ, не требующий изменения структуры метаданных.

Алгоритм действий:

  1. Изменяем запрос: Вместо ВЫБРАТЬ ПЕРВЫЕ 1 делаем выборку небольшого пакета записей, например, ПЕРВЫЕ 10. Это повышает шансы найти свободную запись в полученном наборе.
  2. Перебираем результат в цикле: Организуем цикл по результату запроса.
  3. Пытаемся заблокировать с таймаутом: Внутри цикла для каждой записи пытаемся установить управляемую блокировку, но используем ключевой параметр метода Заблокировать() — таймаут. Установка небольшого таймаута (например, 100 миллисекунд) не позволит сеансу "зависнуть" в ожидании. Если блокировка не будет получена за это время, система сгенерирует исключение.
  4. Обрабатываем результат:
    • Если блокировка установлена успешно (код в блоке Попытка выполнился), значит, мы "захватили" задачу. Обрабатываем ее и выходим из цикла с помощью оператора Прервать.
    • Если было сгенерировано исключение (истек таймаут), это означает, что запись уже занята другим сеансом. Мы просто игнорируем это исключение и переходим к следующей записи в цикле.

Посмотрим на пример кода:


Функция ВзятьЗаказВРаботу()

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

        Исключение
            // Не удалось заблокировать, значит, занято. Просто отменяем транзакцию и идем дальше
            ОтменитьТранзакцию();
            Продолжить; // Переходим к следующей итерации цикла
        КонецПопытки;
        
    КонецЦикла;
    
    Возврат ЗаказПринят;

КонецФункции

Преимущества: не требует изменения объектов конфигурации, относительно прост в реализации.

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

Решение 2: "Мягкие блокировки" через вспомогательный регистр

Этот подход предполагает создание собственного механизма блокировок, который работает быстрее и надежнее управляемых блокировок для данной конкретной задачи. Идея в том, чтобы фиксировать факт "захвата" задачи в отдельном, легковесном регистре.

Подготовка:

Создадим новый независимый непериодический регистр сведений, например, ЗадачиВРаботе.

Ключевым моментом является то, что измерение Заказ будет уникальным, и СУБД не позволит вставить две записи с одним и тем же заказом.

Алгоритм действий:

  1. Выбираем свободный заказ. В запросе на получение свободного заказа используем ЛЕВОЕ СОЕДИНЕНИЕ с нашим новым регистром ЗадачиВРаботе и отбираем только те заказы, для которых не нашлось соответствия. Это сразу исключит из выборки уже "захваченные" задачи.
  2. Пытаемся сделать запись. Получив ссылку на свободный заказ, мы в транзакции пытаемся добавить запись в регистр ЗадачиВРаботе. Эта операция "проверить и вставить" будет атомарной на уровне СУБД.
  3. Обрабатываем результат записи:
    • Если запись в ЗадачиВРаботе прошла успешно, значит, мы "выиграли гонку" и первыми заявили права на этот заказ. Теперь можно спокойно выполнять основную бизнес-логику (изменять основной регистр Заказы), не опасаясь конкурентов.
    • Если при записи возникло исключение о нарушении уникальности ключа, это означает, что другой сеанс успел "захватить" этот заказ буквально на миллисекунды раньше. В этом случае мы отменяем свою транзакцию и просто повторяем всю операцию с самого начала (с шага 1).
  4. Освобождаем "мягкую блокировку". После того как основная работа с заказом завершена, необходимо удалить соответствующую запись из регистра ЗадачиВРаботе.

Пример запроса на шаге 1:


ВЫБРАТЬ ПЕРВЫЕ 1
    Заказы.Заказ КАК Заказ
ИЗ
    РегистрСведений.Заказы КАК Заказы
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЗадачиВРаботе КАК ЗадачиВРаботе
        ПО Заказы.Заказ = ЗадачиВРаботе.Заказ
ГДЕ
    Заказы.Пользователь = ЗНАЧЕНИЕ(Справочник.Пользователи.ПустаяСсылка)
    И ЗадачиВРаботе.Заказ ЕСТЬ NULL

Преимущества: очень высокая производительность, отсутствие ожиданий на блокировках 1С, надежность за счет атомарности операций СУБД.

Недостатки: усложнение архитектуры (требуется новый объект метаданных), необходимость предусматривать механизм очистки "подвисших" записей в ЗадачиВРаботе, если сеанс-исполнитель завершился аварийно.

Решение 3: Разделение данных на пулы

Еще один архитектурный подход — это предотвращение самой причины конфликтов путем разделения общего потока задач на несколько параллельных, непересекающихся очередей (пулов).

Алгоритм действий:

  1. В регистр сведений Заказы добавляется новый реквизит, например, НомерПула (число).
  2. При создании нового заказа ему присваивается номер пула (например, случайным образом от 1 до N).
  3. Каждому пользовательскому сеансу, который обрабатывает заказы, при старте также назначается номер пула, с которым он будет работать.
  4. Запрос на получение свободного заказа модифицируется и теперь содержит дополнительное условие в секции ГДЕ: И Заказы.НомерПула = &НомерПулаТекущегоСеанса.

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

Преимущества: практически полное устранение конфликтов.

Недостатки: возможно неравномерное распределение нагрузки (в одном пуле могут закончиться задачи, а в другом их будет много), требует дополнительной логики по распределению сеансов по пулам и, возможно, механизма переключения сеанса на другой пул.

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

← На главную