Всем привет! Меня зовут Наташа, и я системный аналитик. Сейчас я в поиске работы, сходила на пару собеседований, и хочу описать ответы на некоторые вопросы, которые там встречались — некая рефлексия для меня, и надеюсь, эти короткие статьи будут полезны и еще кому‑то.

Итак, кейс: 

Пользователь банка хотел перевести 100р другу, а потом нажал на кнопку «Перевести» нечаянно пять раз — как предотвратить отпра вку 5 раз по 100р?

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

Здесь опишу оба варианта, буду использовать для этого диаграммы последовательности - Sequence diagram.

Синхронный вариант

При этом варианте мы в момент отправки запроса блокируем кнопку на интерфейсе и ждем ответ.

Шаги диаграммы:

1. Пользователь нажимает кнопку Перевести

2. Кнопка становится неактивной
    Запрос уходит на сервер, интерфейс ждёт ответ

3. Сервер ответил, запрос успешно выполнен, - кнопка снова активна

3а. Если произошла ошибка при выполнении запроса, либо запрос не был выполнен - кнопка снова активна, можно повторить попытку перевода

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

Асинхронный вариант с использованием ключа идемпотентности - стандарт для применения

 Наша система, которую пользователь использует для перевода средств, сложная и высоконагруженная, и запросы пользователей в ней обрабатывают несколько экземпляров микросервиса (ноды). Конкретный экземпляр, куда будет передан запрос, выбирает Балансировщик. Также в системе должен быть Redis для сохранения статусов запросов и кэширования данных.

В тот момент, когда пользователь нажимает кнопку Перевести, Web-интерфейс генерит уникальный идентификатор запроса - идемпотентный ключ, и добавляет его в заголовок HTTP-запроса. Время жизни ключа (TTL) обычно составляет один час.

Если пользователь нажимает кнопку повторно, и запрос с таким ключом уже в процессе обработки - система будет ждать завершения первого запроса, и, когда дождется, вернет тот же результат, что и для первого запроса.

Подробно шаги процесса отражены на диаграмме. Синим цветом отмечены шаги, которые выполняются при повторном нажатии кнопки Перевести.

Шаги диаграммы: 

  1. Пользователь нажал Перевести

  2. Web-интерфейс генерит уникальный ключ, и передает его в параметрах запроса перевода денег

  3. Запрос поступает в балансировщик - и тот передает его на выполнение на Ноду1.

  4. Выполняется проверка в кэше, новый ли это запрос - в этом случае в кэше не будет информации о запросе с таким ключом идемпотентности

  5. Если запрос новый - сохраняем в кэше информацию о нем, с состоянием "в обработке"

  6. Подтверждается необходимость выполнения запроса и...

  7. Нода1 начинает обрабатывать запрос

  8. Предположим, что в какой-то момент, позже шага 2, пользователь нетерпеливо нажал кнопку Перевести еще раз.

  9. Так как параметры запроса не изменились, в параметрах запроса передается тот же самый уникальный ключ

  10. Новый запрос поступает в балансировщик - и тот передает его на выполнение на Ноду2, которая "не знает", что этот же запрос уже выполняется на Ноде1.

  11. Нода2 по ключу ищет в кэше, есть ли там информация о запросе с таким ключом, и находит запись о том, что такой запрос в обработке

  12. Redis передает ответ на Ноду2 и

  13. Нода2 передает на web-интерфейс ответ, что запрос в работе.

  14. Нода2 ждет

  15. В это время, Нода1 закончила обработку запроса, и записала в кэш результат запроса

  16. а также передала ответ на интерфейс

  17. Просыпается Нода2 - идет в кэш, чтобы проверить, появился ли ответ

  18. Ответ появился, и Нода передает тот же самый ответ на интерфейс

И таким образом, в течение жизни ключа на Redis, запрос не будет выполняться повторно.
Все успехов! :-)