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