Как избежать продажи отсутствующего товара: стратегии резервирования
Продвинутое4 минРедакция Innovialabs
Коротко
Для большинства магазинов надежный вариант по умолчанию — резервировать товар при оформлении заказа с коротким таймаутом, списывать остаток атомарно в базе и держать один учет остатков для всех каналов продаж.
Короткий ответ
Продажа отсутствующего товара (overselling) случается, когда двое покупателей получают одну последнюю единицу или маркетплейс показывает товар, который уже продан на сайте. Защищают от этого три вещи:
- Модель резервирования — определяет, в какой момент единица перестает быть доступной другим.
- Атомарные операции с остатками — два параллельных запроса не могут одновременно списать одну единицу.
- Единый учет остатков — все каналы читают и пишут в одно место.
Для большинства магазинов сбалансированный вариант — резерв при оформлении с таймаутом 10-20 минут. Точное значение зависит от того, сколько реально длится ваша оплата.
Три модели резервирования
| Модель | Когда товар удерживается | Плюсы | Минусы |
|---|---|---|---|
| Резерв в корзине | При добавлении в корзину | Покупатель не потеряет товар на оплате | Брошенные корзины блокируют остаток, легко злоупотребить |
| Резерв при оформлении | При начале checkout | Баланс: держим товар только для серьезных покупателей | Нужны таймауты и задача освобождения |
| Резерв после оплаты | После подтверждения оплаты | Нет заблокированных остатков, простая логика | Двое могут оплатить одну единицу, одному придется вернуть деньги |
Резерв в корзине подходит для флеш-распродаж и лимитированных дропов, где потерять товар на последнем шаге недопустимо. Используйте короткий таймаут корзины и лимит количества на покупателя.
Резерв при оформлении подходит большинству каталожных магазинов: товар держится, пока покупатель вводит адрес и платит.
Резерв после оплаты подходит для больших остатков, цифровых товаров и товаров под заказ, где коллизии редки. Но план на редкую двойную продажу все равно нужен: автоматический возврат и сообщение покупателю.
Таймауты резерва
Резерв без срока действия — это медленная утечка остатков. Каждому резерву нужны:
- Поле
expires_at, которое задается при создании резерва. - Механизм освобождения: фоновая задача, снимающая просроченные резервы, или ленивая очистка при чтении остатка.
- Правила продления: если покупателя перенаправили на платежный шлюз, продлите резерв так, чтобы он пережил сессию шлюза.
- Правило поздней оплаты: что делать, если подтверждение оплаты пришло после истечения резерва. Обычно пробуют зарезервировать заново, а если товара нет — делают автоматический возврат.
Храните резервы отдельными записями, а не как счетчик, который уменьшили и забыли. Тогда видно, кто что держит, и снимать резерв можно точечно.
Конкурентный доступ
Классическая ошибка: прочитать остаток (осталась 1 штука), проверить, потом записать. Два запроса одновременно читают «1» и оба продают. Решать это нужно на уровне базы данных.
Самый простой надежный способ — условное атомарное обновление:
UPDATE stock
SET available = available - :qty,
reserved = reserved + :qty
WHERE sku = :sku AND available >= :qty;
-- 0 затронутых строк = товара недостаточно
Другие варианты:
- Блокировка строк (
SELECT ... FOR UPDATE) внутри транзакции, если перед записью нужно несколько проверок. Блокируйте строки в одном порядке, чтобы избежать дедлоков. - Оптимистичная блокировка через колонку версии: при изменении версии — повтор.
- Атомарные операции в Redis (например, Lua-скрипт) для очень высокой нагрузки, а база остается надежным хранилищем.
Сделайте создание резерва и обработку платежных колбэков идемпотентными. Шлюзы повторяют вебхуки, и повторный колбэк не должен списать остаток дважды.
Остатки в нескольких каналах
Когда вы продаете на сайте, в офлайн-магазине и на маркетплейсах, у каждого канала своя копия остатков с задержкой синхронизации. Чтобы они не расходились:
- Один учет — источник правды: ваша база, ERP или 1С, но не маркетплейс.
- Отправляйте изменения при каждом движении, а не раз в сутки. Используйте API обновления остатков маркетплейсов и очередь.
- Держите страховой буфер по каналам: на каналах с медленной синхронизацией показывайте чуть меньше реального остатка.
- Распределяйте дефицит: для редких товаров выделяйте каналам фиксированные квоты вместо общего пула.
- Быстро забирайте заказы: заказ с маркетплейса должен как можно скорее создавать резерв в вашем учете.
Типичные ошибки
- Проверка остатка в коде приложения без атомарной записи.
- Резервы без срока действия, которые постепенно «замораживают» каталог.
- Учет товаров «в корзине» как проданных в отчетах.
- Синхронизация остатков с маркетплейсами раз в день для быстро продающихся товаров.
- Отсутствие сценария для оплаты, пришедшей после истечения резерва.