Если зарезервированный товар остается доступным для другого заказа, магазин может продать одну единицу двум покупателям. Обычно проблема возникает не в отображении карточки товара, а в разрыве между проверкой остатка и созданием резерва, неверной формуле доступного количества или задержке синхронизации между сайтом и складом.
Исправление должно начинаться с восстановления фактического остатка и поиска конкретного сценария гонки. Простое скрытие кнопки покупки или более частое обновление кэша не гарантирует защиту: финальная проверка и атомарное резервирование должны выполняться на стороне системы, которая владеет остатком.
Как проявляется ошибка резерва
- Последняя единица одновременно попадает в два оформленных заказа.
- В карточке показан доступный товар, хотя в складской системе он уже зарезервирован.
- После оплаты заказ переходит в статус «нет в наличии».
- Один канал продаж видит резерв, а другой продолжает продавать остаток.
- При одновременном оформлении оба запроса успешно проходят предварительную проверку.
- Резерв создается в журнале, но агрегированный доступный остаток не уменьшается.
- После повторной обработки события количество резерва увеличивается или уменьшается дважды.
Сначала ограничьте последствия
- Определите товары и склады с отрицательным либо спорным доступным остатком.
- Временно остановите продажу только затронутых SKU или включите безопасный буфер.
- Сохраните журнал заказов, резервов, платежей и складских событий до ручных корректировок.
- Сверьте физический остаток, активные резервы, собранные и уже отгруженные заказы.
- Не удаляйте конфликтующие резервы, пока не определена принадлежность каждой единицы.
Определите владельца остатка
В системе должен быть один источник, принимающий окончательное решение о доступности. Это может быть складская база, ERP или сервис inventory. Витрина, мобильное приложение и маркетплейс могут показывать копию данных, но не должны независимо подтверждать продажу последней единицы.
- Запишите, где хранится фактический on hand и где создается резерв.
- Определите, какой сервис подтверждает возможность оформить заказ.
- Проверьте, не ведут ли сайт и учетная система параллельные независимые резервы.
- Уточните приоритет ручной корректировки, приемки, перемещения и возврата.
- Зафиксируйте единый идентификатор SKU, склада, партии и единицы измерения.
Проверьте формулу доступного количества
Базовая идея выглядит как «фактический остаток минус активные резервы», но в реальной системе учитываются статусы, склады, партии, поврежденный товар, заказы на сборке и ожидаемые перемещения. Главная ошибка — вычитать не все действующие резервы или считать отмененные и просроченные активными.
- On hand должен относиться к тому же складу и SKU, что и резерв.
- В доступный остаток не включаются карантин, брак и уже переданные в сборку единицы.
- Статусы резерва должны однозначно определять, влияет ли запись на доступность.
- Количество хранится в одной нормализованной единице измерения.
- Компоненты комплектов учитываются согласованно с готовым товаром.
- Возврат становится доступным только после фактической приемки, если так устроен процесс.
Почему проверка перед записью создает гонку
Опасный сценарий выглядит так: запрос A читает остаток 1, запрос B тоже читает 1, затем каждый отдельно создает резерв на одну единицу. Обе проверки были верными в момент чтения, но итоговое состояние стало невозможным. Между проверкой и изменением должна существовать гарантия атомарности.
- Проверка доступности только в браузере не защищает от параллельных запросов.
- Обычная последовательность SELECT, затем UPDATE без транзакционной защиты оставляет окно гонки.
- Кэш нельзя использовать как единственное подтверждение последней единицы.
- Распределенная блокировка без корректного срока и владельца сама может создать зависший резерв.
- Повтор HTTP-запроса после timeout не должен создавать второй резерв.
Как сделать резервирование атомарным
Конкретный механизм зависит от базы и архитектуры, но операция должна либо полностью подтвердить нужное количество, либо отказать без частичного изменения. Часто используют условное обновление остатка, блокировку строки в транзакции или версионирование записи с повторной проверкой.
- Начните операцию с идентификатором заказа и уникальным ключом идемпотентности.
- Получите актуальное состояние из системы-владельца остатка.
- Защитите проверяемую строку либо выполните условное атомарное изменение.
- Создайте запись резерва и свяжите ее с заказом в той же согласованной операции.
- Если доступного количества недостаточно, отмените всю операцию и верните понятный результат.
- После подтверждения опубликуйте событие для витрины и других систем без повторного изменения резерва.
Идемпотентность защищает от повторных резервов
Платежный callback, очередь или клиент могут повторить запрос. Один и тот же бизнес-запрос должен возвращать ранее созданный резерв, а не уменьшать остаток повторно. Для этого нужен стабильный ключ, связанный с заказом и операцией.
- Создайте уникальное ограничение на бизнес-идентификатор резерва.
- Различайте повтор того же запроса и новую попытку с измененным количеством.
- Храните результат операции, чтобы безопасно отвечать после сетевого timeout.
- Обрабатывайте повторные события очереди без повторного списания.
- Не используйте случайный новый ключ при каждом автоматическом retry.
Статусы и срок жизни резерва
Резерв обычно проходит состояния: создан, ожидает оплату, подтвержден, передан в сборку, отменен или истек. Переходы должны быть явными и повторяемыми. Один флаг active быстро становится неоднозначным при сбоях и повторных событиях.
- Срок резерва задается по бизнес-сценарию и хранится как точное время истечения.
- Продление выполняется контролируемой операцией, а не неограниченным обновлением по активности пользователя.
- Истечение и подтверждение оплаты конкурируют через атомарный переход состояния.
- Освобождение количества выполняется один раз при подтвержденном переходе в неактивный статус.
- Фоновая очистка обрабатывает партии и не снимает уже подтвержденные резервы.
Проверьте кэш и витрину
Даже при правильной базе витрина может показывать старое количество. Это влияет на ожидания покупателя, но не должно позволять завершить второй заказ: сервер обязан повторно подтвердить резерв перед оплатой или финальным оформлением.
- Ключ кэша должен включать SKU, склад, регион и другие измерения остатка.
- Событие изменения остатка должно инвалидировать правильную запись, а не только общую карточку.
- Не увеличивайте TTL ради снижения нагрузки без оценки риска oversell.
- Показывайте пользователю результат серверной проверки, если последняя единица уже занята.
- После освобождения резерва обновляйте витрину тем же надежным механизмом.
Синхронизация между сайтом, складом и маркетплейсами
При нескольких каналах продаж нулевая задержка недостижима, поэтому нужен либо центральный сервис резервов, либо распределение квот по каналам. Обмен только периодическими полными остатками создает окно двойной продажи.
- Передавайте изменения с уникальным id события и версией остатка.
- Не применяйте более старое событие поверх нового состояния.
- Отделяйте подтвержденное количество от расчетного остатка витрины.
- Настройте очередь повторов и отдельный список необработанных событий.
- Контролируйте задержку синхронизации и расхождения по каждому каналу.
- Для дефицитного товара используйте безопасный буфер или централизованное подтверждение.
Как найти конкретную причину
- Выберите один конфликтующий SKU и два заказа, претендующие на одну единицу.
- Постройте временную шкалу чтения остатка, создания резерва, оплаты и складских событий.
- Сравните идентификаторы операции, заказа, склада, партии и версии записи.
- Проверьте, выполнялись ли запросы параллельно или повторялись после timeout.
- Найдите первое состояние, где сумма активных резервов превысила доступный остаток.
- Определите, создала ли проблему транзакция, кэш, повтор события или задержка внешней системы.
- Воспроизведите сценарий на тестовой среде двумя одновременными запросами.
Восстановление уже конфликтующих заказов
- Не меняйте очередность заказов вручную без зафиксированного бизнес-правила.
- Определите приоритет по подтвержденному времени резерва или оплаты.
- Свяжитесь с затронутым клиентом до автоматической отмены, если заказ уже оплачен.
- Возврат и отмену проводите через штатные операции, чтобы финансовые и складские документы совпали.
- После корректировки пересчитайте доступный остаток и сохраните аудит изменений.
- Проверьте связанные бонусы, доставку, чеки и уведомления.
Тесты после исправления
- Два параллельных заказа на последнюю единицу: резерв получает только один.
- Повтор того же запроса не создает дополнительный резерв.
- Timeout клиента и retry возвращают результат первой операции.
- Истечение резерва освобождает количество ровно один раз.
- Оплата в момент истечения приводит к одному допустимому конечному статусу.
- Отмена и повторная доставка события не меняют остаток дважды.
- Другой склад или партия не участвуют в расчете ошибочно.
- Витрина обновляется, а финальная серверная проверка блокирует устаревший остаток.
Типичные неправильные исправления
- Спрятать кнопку покупки при нулевом кэше без серверного резерва.
- Уменьшить интервал синхронизации, но оставить неатомарную операцию.
- Поставить глобальную блокировку на весь каталог и резко снизить производительность.
- Снимать резерв по таймеру без проверки его текущего статуса.
- Исправлять остаток прямым SQL без связанных заказов и журнала аудита.
- Считать платеж единственным резервом и оставлять товар свободным до callback.
- Обрабатывать retry как новый заказ с новым ключом операции.
Как предотвратить повторение
- Определите единый источник истины для остатка и резерва.
- Сделайте создание резерва атомарным и идемпотентным.
- Храните явные статусы, срок действия и историю переходов.
- Добавьте тесты конкурентного оформления и повторных событий.
- Мониторьте отрицательный доступный остаток, дубли резервов и задержку синхронизации.
- Регулярно сверяйте агрегированный остаток с журналом движений и активных резервов.
- Не позволяйте ручной корректировке обходить бизнес-правила без аудита.
Итог
Если зарезервированный товар доступен другому заказу, ищите разрыв между проверкой и атомарным созданием резерва, ошибку формулы доступности или задержку межсистемной синхронизации. Надежное решение требует единого владельца остатка, идемпотентных операций, явных статусов и проверки конкурентных сценариев.
Если нужно устранить двойные продажи, я могу разобрать журнал заказов и резервов, воспроизвести гонку, исправить транзакционную логику и синхронизацию, а затем добавить тесты и мониторинг расхождений.