← На главную

Shopify перевёл резервирование товаров с Redis на MySQL: SKIP LOCKED

08.08.2026 22:32 · hackernews

На распродажах Shopify каждая покупка упирается в остатки на складе. Если не зарезервировать товар в момент оплаты, два покупателя могут купить последнюю единицу — и продавцу придётся отменять заказ. Или наоборот: покажем «нет в наличии», хотя товар есть, и потеряем продажу. На пике Black Friday 2025 через платформу прошло $5,1 млн продаж в минуту, каждая транзакция задевает склад.

Раньше резервирование жило в Redis. Операции reserve и claim выполнялись в разных системах: Redis хранил резервы, а MySQL — складской учёт. Нельзя было атомарно списать товар и почистить резерв, из-за этого возникали oversell и undersell. Плюс Redis не понимал мультисклад.

MySQL 8 с фичей SKIP LOCKED позволил перейти на схему «одна строка — одна единица товара». Вместо строки с количеством у товара на 10 единиц — 10 строк, а резерв трёх — это выбор и перемещение трёх строк в одной транзакции. Так резерв и учёт оказались в одной базе с ACID. Идею подглядели у 37signals.

Но если на каждый товар держать строки на все единицы, таблица раздуется: 50 000 единиц на 10 складах — это 500 000 строк. Поэтому сделали ограниченный пул доступных строк, максимум 1 000 на связку товар/склад. Когда пул заканчивается, прямо во время резерва запускается пополнение, с локом, чтобы не было стада.

Дальше — технические грабли. С автоинкрементным первичным ключом InnoDB брала две блокировки на строку. Перешли на составной ключ shop_id, inventory_item_id, inventory_group_id, id — стало одна. Транзакции перевели с REPEATABLE READ на READ COMMITTED, иначе gap-локи блокировали пополнение пула. Устранили дедлоки, унифицировав порядок блокировок: сначала DELETE из таблицы юнитов, потом INSERT в резервы. А для корзин с несколькими позициями запросы резервирования объединили через UNION ALL.

Но главный сюрприз был не в запросах. В проде упёрлись в потолок: CPU не загружен, P90 резервирования ок, а MySQL душится. Оказалось, дело в соединениях. Добавили теги на каждый SQL через комментарии (/* conn_tag:checkout_completion */), а ProxySQL агрегировал, кто и сколько держит коннект. Выяснилось: резервы ни при чём — другие части чекаута держали соединения слишком долго. Почистили checkout: убрали 50% чтений и 33% транзакций с основной базы. Заодно пересмотрели консервативный лимит InnoDB thread concurrency, который не трогали годами.

Переезд шёл через «shadow mode»: параллельно писали в Redis и MySQL, Redis оставался источником истины. Когда MySQL подтвердил корректность, источник переключили, с возможностью отката через kill switch. Раскатывали постепенно, под подом, начиная с низконагруженных.

Вывод: MySQL с SKIP LOCKED тянет то, что раньше казалось задачей для Redis или Kafka. Но настоящий бутылочный горлышек — в соединениях, а не в движке. Если цифры не сходятся — инструментируйте весь путь, ответ часто в сантехнике, а не в базе.

Читать оригинал →