Search
Write a publication
Pull to refresh

Comments 12

Они исполняются атомарно, то есть никто не сможет обратиться к Redis в процессе выполнения скрипта — сервер заблокирован до конца выполнения: либо вся операция выполнена, либо ничего не произошло

Сомнительная архитектура блокировки в редисе. Если мы в него сложили 100 разных типов сущностей, а для "транзакции" нам нужны только 2, то блокируем мы все равно 100, т.е. в 50 раз больше нужного. Т.е. делаем один глобальный мьютекс с неоправданно большим охватом.

можно отдельный Redis поднять чисто для остатков. Тогда охват будет минимальный

Можно. Но когда возникнет необходимость транзакции между одной из 2х сущностей тут и одной из 98 сущностей во втором редисе - добро пожаловать обратно в микросервисы, распределенную транзакцию и ее сложный откат.
Даже ограничение блокировки по сущностям все равно это много, это как блокировка всей таблицы в классической СУБД. Ниже вон правильно предлагают - блокировать конкретные товары. Потому что они являются разделителями, мало кому нужен резерв на другой товар вместо этого (оговорюсь, товары-заменители сознательно я в сторону откладываю).
Да, в этих системах возможны взаимные блокировки, но это хорошо снижается эвристикой заранее: заказы с непересекающимися товарами мы можем резервировать параллельно, заказы с пересекающимися лучше организовать в очередь.
Первая часть системы отлично масштабируется горизонтально (неперсекающиеся товары), вторая часть - будет аналогична табличной блокировке.

Спасибо за разбор! Согласна, что вариант с глобальным мьютексом в продакшене может быть спорным, и возможно далее мы это пересмотрим в пользу более точечных блокировок по товарам. Это пока компромисс для MVP, было требование не допускать oversell, и нам важно было сразу гарантировать атомарность, даже если это ограничивает параллельность.

Резерв товара это по сути и есть конкуренция за товар. Все равно где-то в конце будет в один поток.

Один из способов - это построчная блокировка каждого товара. В my sql например есть select for update. Тогда заказы с разным набором товаров не будут друг другу мешать. Там будет всякие deadlock, они будут по тайм-ауту отваливаться. И в итоге окажется, что два заказа отвалились, но один из них смог бы обработаться, если повторно запустить. Поэтому просто сверху полируем retry policy и в целом все начинает работать. И чем больше линий в заказе тем он проблемней.

Чуть измененный вариант - построить на пессимистичной блокировке - добавить версию строки и обновлять. Если не совпадает - откат транзакции и retry.

Второй концептуальный способ - зная, что там в один поток, это сделать очередь (не важно на чем - кафка, раббит, таблица в бд, редис) и разбирать ее в один поток. С этого момента основной целью станет - выжать максимум из железа и субд. Так как мы уже знаем, что будет один поток, то блокировки не нужны, и можно что-нибудь придумать, например снизить уровень изоляции или даже вообще отказаться от транзакций. Из минусов - ордера с разными товарами, которые могли бы обработаться параллельно - не будут.

Там уже можно использовать более сложные ключи группировки очередей, чем просто product_id. Например, сделать product_id:warehouse_id; или же product_id:category_id и параллелить еще глубже. Можно сделать руками теггирование через админку и особо нагруженные продукты обрабатывать в отдельной очереди.

В принципе все заказы обрабатываются настолько параллельно, насколько это возможно, при учете того, что главный конкурирующий ресурс здесь это сток продукта. Причем прием заказов максимально конкурентный и это главное. Тут тоже можно оптимизировать, и например смотреть, что если запас товара на скаде большой, а покупатель берет всего пару единиц, можно оптимистично ему вернуть, что заказ успешно принят, потому что вероятнее всего это так. И если уже все-таки не удалось забукать - переводим флаг "оптимистичное бронирование данного товара" в false и ждем в ручном режиме, и звоним покупателю. Такое будет случаться только с какими-нибудь "лабубу", для такого типа товара флаг "оптимистичное бронирование данного товара" всегда должен быть false. Соответственно заказ будет отмечен, как принятый, когда все товары, у которых "оптимистичное бронирование данного товара" = false пройдут полноценное бронирование.

В общем я тут на ходу изобретаю, комбинации оптимистик + пессимистик можно придумать очень разные :)

или же product_id:category_id и параллелить еще глубже

а вот это не выйдет. Т.к. категория - это более сводный классификатор над продуктом. Поэтому он не добавит никакого дополнительного разделения. Полезно лишь добавление таких сущностей, которые более детально делят остатки товаров.

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

По warehouse_id да, если заранее был выбран конкретный склад (например по сроку или стоимости доставки до конечного клиента и в том числе по остаткам). По category_id не думаю. Можно закодить машрутизатор на уровне приложения - который будет работать в несколько потоков, но класть заказы с пересекающимися товарами в один поток. Но зачем, если это сложнее, а пессимистичная или оптимистичная блокировка по сути делают то же самое. Можно просто вывести кол-во потоков в конфигурацию и экспериментальным образом подобрать нужное значение.

И есть как минимум три категории систем - на стороне продаж, на стороне склада (WMS) и OMS (order management system), которые скачивают заказы из первых, выбирают оптимальный склад и отправляют в WMS. И у всех трех есть свои особенности. Сложнее всего, пожалуй с OMS - когда они делают резерв, это еще не значит, что этот резерв реально создан для конкретного склада в WMS.

Попутал немного - вариант 1а это пессимистичная блокировка, вариант 1б оптимистичная

Для MVP вполне сойдет, но только такая архитектура впринципе не может мосштабироваться, кроме как вертикально. А с условием кол-ва кода на Lua выглядит, что удобнее было бы использовать тарантул. Из дополнительных плюшек - при последующей миграции сможете любым мкскульным драйвером подключиться и перенести данные как из обычной БД. И реализовав идеоматичный API использовать как полноценный сервис, а не размазывая логику между прилкой и хранилищем.

На нагрузках в десятки K rps даже заморачиваться не придется, а дальше поковырять в сторону экосистемных библиотек для шардирования.

Спасибо за фидбек! Тарантул действительно выглядит интересным, особенно при высоких нагрузках! Мы выбрали Redis, потому что он уже использовался в проекте, и его возможностей хватало для MVP. В будущем возможно сделаем отдельный сервис для управления остатками товаров, если нагрузка вырастет.

Sign up to leave a comment.

Articles