Блокировка заказа в Sale для защиты от гонок

27.01.2026

Проблема/контекст

В sale заказ одновременно могут менять несколько процессов: админка, AJAX-обработчик, обмен, агент. Без синхронизации вы получаете потерю изменений: один запрос перезаписывает поля/коллекции, которые уже изменил другой.

Решение с кодом

Используйте встроенную блокировку Bitrix\Sale\Order::lock()/unlock() и всегда снимайте её в finally. Перед блокировкой проверьте статус через getLockedStatus(): если заказ уже заблокирован другим пользователем — корректно завершайте операцию (HTTP 409/ошибка).

        <?php
declare(strict_types=1);

use Bitrix\Main\Context;
use Bitrix\Main\Loader;
use Bitrix\Sale\Order;

Loader::includeModule('sale');

$request = Context::getCurrent()->getRequest();

$orderId = (int)$request->getPost('orderId');
if ($orderId <= 0)
{
    throw new \Bitrix\Main\ArgumentException('Invalid orderId');
}

// 1) Проверяем, нет ли чужой блокировки
$lockStatus = Order::getLockedStatus($orderId)->getData(); // LOCK_STATUS, LOCKED_BY, DATE_LOCK
if (($lockStatus['LOCK_STATUS'] ?? null) === Order::SALE_ORDER_LOCK_STATUS_RED)
{
    // Здесь лучше вернуть 409 Conflict и показать, кем/когда заблокировано
    throw new \RuntimeException('Order is locked by another user');
}

$lockResult = Order::lock($orderId);
if (!$lockResult->isSuccess())
{
    throw new \RuntimeException('Failed to lock order: ' . implode('; ', $lockResult->getErrorMessages()));
}

try {
    // 2) Загружаем и меняем заказ строго внутри блокировки
    $order = Order::load($orderId);
    if (!$order)
    {
        throw new \RuntimeException('Order not found');
    }

    // Пример: безопасное изменение статуса и сохранение
    $r = $order->setField('STATUS_ID', 'P'); // ваш код статуса
    if (!$r->isSuccess())
    {
        throw new \RuntimeException('Status update failed: ' . implode('; ', $r->getErrorMessages()));
    }

    $save = $order->save();
    if (!$save->isSuccess())
    {
        throw new \RuntimeException('Order save failed: ' . implode('; ', $save->getErrorMessages()));
    }
} finally {
    // 3) Снимаем блокировку всегда, даже при исключениях
    Order::unlock($orderId);
}

    

Практические детали:

  • Блокируйте как можно ближе к месту изменения и держите блокировку минимальное время.
  • Не делайте внутри блокировки долгие операции (внешние API, генерация файлов, длительные расчёты). Сначала подготовьте данные, затем коротко: lock → load → change → save → unlock.
  • Если операция может выполняться без веб-пользователя (агент/CLI), заранее продумайте контекст прав: unlock() проверяет права текущего пользователя, поэтому тип выполнения важен.

Итог

Order::lock()/unlock() — простой способ избежать потери изменений при параллельных запросах. Ключевое правило — проверка статуса и снятие блокировки в finally.

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