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

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

Как проявляется ошибка резерва

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

Сначала ограничьте последствия

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

Определите владельца остатка

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

  • Запишите, где хранится фактический on hand и где создается резерв.
  • Определите, какой сервис подтверждает возможность оформить заказ.
  • Проверьте, не ведут ли сайт и учетная система параллельные независимые резервы.
  • Уточните приоритет ручной корректировки, приемки, перемещения и возврата.
  • Зафиксируйте единый идентификатор SKU, склада, партии и единицы измерения.

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

Базовая идея выглядит как «фактический остаток минус активные резервы», но в реальной системе учитываются статусы, склады, партии, поврежденный товар, заказы на сборке и ожидаемые перемещения. Главная ошибка — вычитать не все действующие резервы или считать отмененные и просроченные активными.

  • On hand должен относиться к тому же складу и SKU, что и резерв.
  • В доступный остаток не включаются карантин, брак и уже переданные в сборку единицы.
  • Статусы резерва должны однозначно определять, влияет ли запись на доступность.
  • Количество хранится в одной нормализованной единице измерения.
  • Компоненты комплектов учитываются согласованно с готовым товаром.
  • Возврат становится доступным только после фактической приемки, если так устроен процесс.

Почему проверка перед записью создает гонку

Опасный сценарий выглядит так: запрос A читает остаток 1, запрос B тоже читает 1, затем каждый отдельно создает резерв на одну единицу. Обе проверки были верными в момент чтения, но итоговое состояние стало невозможным. Между проверкой и изменением должна существовать гарантия атомарности.

  • Проверка доступности только в браузере не защищает от параллельных запросов.
  • Обычная последовательность SELECT, затем UPDATE без транзакционной защиты оставляет окно гонки.
  • Кэш нельзя использовать как единственное подтверждение последней единицы.
  • Распределенная блокировка без корректного срока и владельца сама может создать зависший резерв.
  • Повтор HTTP-запроса после timeout не должен создавать второй резерв.

Как сделать резервирование атомарным

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

  1. Начните операцию с идентификатором заказа и уникальным ключом идемпотентности.
  2. Получите актуальное состояние из системы-владельца остатка.
  3. Защитите проверяемую строку либо выполните условное атомарное изменение.
  4. Создайте запись резерва и свяжите ее с заказом в той же согласованной операции.
  5. Если доступного количества недостаточно, отмените всю операцию и верните понятный результат.
  6. После подтверждения опубликуйте событие для витрины и других систем без повторного изменения резерва.

Идемпотентность защищает от повторных резервов

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

  • Создайте уникальное ограничение на бизнес-идентификатор резерва.
  • Различайте повтор того же запроса и новую попытку с измененным количеством.
  • Храните результат операции, чтобы безопасно отвечать после сетевого timeout.
  • Обрабатывайте повторные события очереди без повторного списания.
  • Не используйте случайный новый ключ при каждом автоматическом retry.

Статусы и срок жизни резерва

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

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

Проверьте кэш и витрину

Даже при правильной базе витрина может показывать старое количество. Это влияет на ожидания покупателя, но не должно позволять завершить второй заказ: сервер обязан повторно подтвердить резерв перед оплатой или финальным оформлением.

  • Ключ кэша должен включать SKU, склад, регион и другие измерения остатка.
  • Событие изменения остатка должно инвалидировать правильную запись, а не только общую карточку.
  • Не увеличивайте TTL ради снижения нагрузки без оценки риска oversell.
  • Показывайте пользователю результат серверной проверки, если последняя единица уже занята.
  • После освобождения резерва обновляйте витрину тем же надежным механизмом.

Синхронизация между сайтом, складом и маркетплейсами

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

  • Передавайте изменения с уникальным id события и версией остатка.
  • Не применяйте более старое событие поверх нового состояния.
  • Отделяйте подтвержденное количество от расчетного остатка витрины.
  • Настройте очередь повторов и отдельный список необработанных событий.
  • Контролируйте задержку синхронизации и расхождения по каждому каналу.
  • Для дефицитного товара используйте безопасный буфер или централизованное подтверждение.

Как найти конкретную причину

  1. Выберите один конфликтующий SKU и два заказа, претендующие на одну единицу.
  2. Постройте временную шкалу чтения остатка, создания резерва, оплаты и складских событий.
  3. Сравните идентификаторы операции, заказа, склада, партии и версии записи.
  4. Проверьте, выполнялись ли запросы параллельно или повторялись после timeout.
  5. Найдите первое состояние, где сумма активных резервов превысила доступный остаток.
  6. Определите, создала ли проблему транзакция, кэш, повтор события или задержка внешней системы.
  7. Воспроизведите сценарий на тестовой среде двумя одновременными запросами.

Восстановление уже конфликтующих заказов

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

Тесты после исправления

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

Типичные неправильные исправления

  • Спрятать кнопку покупки при нулевом кэше без серверного резерва.
  • Уменьшить интервал синхронизации, но оставить неатомарную операцию.
  • Поставить глобальную блокировку на весь каталог и резко снизить производительность.
  • Снимать резерв по таймеру без проверки его текущего статуса.
  • Исправлять остаток прямым SQL без связанных заказов и журнала аудита.
  • Считать платеж единственным резервом и оставлять товар свободным до callback.
  • Обрабатывать retry как новый заказ с новым ключом операции.

Как предотвратить повторение

  • Определите единый источник истины для остатка и резерва.
  • Сделайте создание резерва атомарным и идемпотентным.
  • Храните явные статусы, срок действия и историю переходов.
  • Добавьте тесты конкурентного оформления и повторных событий.
  • Мониторьте отрицательный доступный остаток, дубли резервов и задержку синхронизации.
  • Регулярно сверяйте агрегированный остаток с журналом движений и активных резервов.
  • Не позволяйте ручной корректировке обходить бизнес-правила без аудита.

Итог

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

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