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

Проблема

Представьте, что вы продаете 100 билетов на рок-концерт, но каким-то образом их покупают 110 человек. Похоже на кошмар с этими 10 разгневанными клиентами, не так ли?

Вот в чем была проблема. У нас есть товары, которые продаются как горячие пирожки, и мы не хотели оказаться на другом конце телефонного звонка, указанного выше. Без поточно-ориентированного метода обработки параллельных транзакций в распределенной системе можно было пойти дальше и купить больше товаров, чем было на складе. Я объясню, как мы создали систему для лучшего управления запасами.

Оригинальная архитектура

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

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

  1. Размещены два заказа, и количество этого товара на складе уменьшается до -1 :(
  2. Один из заказов не прошел для одного из клиентов после, который она произвела платеж, что привело к разочарованию покупателя. ›.‹

Обе эти проблемы были большими, и мы не могли с ними мириться.

Решение

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

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

Нам нужно было что-то, чтобы разделять состояние между машинами, и мы решили, что Redis может быть хорошим инструментом для этого. С веб-сайта Redis:

Redis - это хранилище структур данных в памяти с открытым исходным кодом (под лицензией BSD), используемое в качестве базы данных, кеша и брокера сообщений.

Мы уже активно используем Redis для кэширования наших запросов API (и мне это нравится!). Наше решение заключалось в том, чтобы сохранить количество на ключ itemID в Redis и уменьшать его всякий раз, когда кто-то заходил на платежные шлюзы. Если количество меньше 0, покупатель не может перейти к оплате. Поскольку Redis является однопоточным, мы можем быть уверены, что каждый отдельный вызов Redis является атомарной операцией и всегда будет возвращать правильные данные.

Однако вскоре мы поняли, что нам нужно выполнить набор вызовов Redis внутри одной транзакции, чтобы иметь возможность успешно удерживать элемент для клиента. На помощь нам пришли сценарии Lua. Redis позволяет выполнять сценарии, написанные на Lua, что по сути делает их одной атомарной функцией. Итак, проверка того, достаточно ли у нас запасов для увеличения срока хранения, выполняется в одном атомарном блоке. Эта функция в среднем занимает менее пары миллисекунд, поэтому мы не беспокоились о производительности.

Мы сохранили срок действия этой клавиши 5 минут, по сути, позволяя удерживать время 5 минут.

Итак, теперь, если два клиента хотят купить один и тот же товар, у которого осталось только одно количество, и они нажимают на оплату, происходит несколько вещей:

  1. Заказчик 1 обращается к Redis, который решает, что может забронировать товар для этого клиента на 5 минут, и сокращает количество товара до 0.
  2. Клиент 2 обращается к Redis, который видит количество товара и решает, что не может пропустить этого пользователя.
  3. Теперь покупатель 1 может завершить платеж и купить товар. В случае, если они не могут этого сделать, срок действия ключа Redis истекает через 5 минут удержания, и клиент 2 может продолжить процесс оплаты.

Это дает нам единообразие в бэкэнде управления запасами и улучшает взаимодействие с пользователем.