Всем привет! Меня зовут Наташа, и я системный аналитик. Сейчас я в поиске работы, сходила на пару собеседований, и хочу описать ответы на некоторые вопросы, которые там встречались — некая рефлексия для меня, и надеюсь, эти короткие статьи будут полезны и еще кому‑то.
Итак, кейс:
Пользователь банка хотел перевести 100р другу, а потом нажал на кнопку «Перевести» нечаянно пять раз — как предотвратить отпра вку 5 раз по 100р?
На собеседовании я почему-то не дала очевидный ответ с синхронным взаимодействием и блокировкой кнопки, сразу пошла рассуждать об асинхронной версии.
Здесь опишу оба варианта, буду использовать для этого диаграммы последовательности - Sequence diagram.
Синхронный вариант
При этом варианте мы в момент отправки запроса блокируем кнопку на интерфейсе и ждем ответ.

Шаги диаграммы:
1. Пользователь нажимает кнопку Перевести
2. Кнопка становится неактивной
Запрос уходит на сервер, интерфейс ждёт ответ
3. Сервер ответил, запрос успешно выполнен, - кнопка снова активна
3а. Если произошла ошибка при выполнении запроса, либо запрос не был выполнен - кнопка снова активна, можно повторить попытку перевода
Таким образом, у пользователя нет возможности ошибочно нажать кнопку дважды.
Разумеется, если он откроет еще одну такую же вкладку в браузере, кнопка будет там активна, и он сможет выполнить перевод еще раз, но как по мне, вряд ли это можно считать случайной ошибкой.
Асинхронный вариант с использованием ключа идемпотентности - стандарт для применения
Наша система, которую пользователь использует для перевода средств, сложная и высоконагруженная, и запросы пользователей в ней обрабатывают несколько экземпляров микросервиса (ноды). Конкретный экземпляр, куда будет передан запрос, выбирает Балансировщик. Также в системе должен быть Redis для сохранения статусов запросов и кэширования данных.
В тот момент, когда пользователь нажимает кнопку Перевести, Web-интерфейс генерит уникальный идентификатор запроса - идемпотентный ключ, и добавляет его в заголовок HTTP-запроса. Время жизни ключа (TTL) обычно составляет один час.
Если пользователь нажимает кнопку повторно, и запрос с таким ключом уже в процессе обработки - система будет ждать завершения первого запроса, и, когда дождется, вернет тот же результат, что и для первого запроса.
Подробно шаги процесса отражены на диаграмме. Синим цветом отмечены шаги, которые выполняются при повторном нажатии кнопки Перевести.

Шаги диаграммы:
Пользователь нажал Перевести
Web-интерфейс генерит уникальный ключ, и передает его в параметрах запроса перевода денег
Запрос поступает в балансировщик - и тот передает его на выполнение на Ноду1.
Выполняется проверка в кэше, новый ли это запрос - в этом случае в кэше не будет информации о запросе с таким ключом идемпотентности
Если запрос новый - сохраняем в кэше информацию о нем, с состоянием "в обработке"
Подтверждается необходимость выполнения запроса и...
Нода1 начинает обрабатывать запрос
Предположим, что в какой-то момент, позже шага 2, пользователь нетерпеливо нажал кнопку Перевести еще раз.
Так как параметры запроса не изменились, в параметрах запроса передается тот же самый уникальный ключ
Новый запрос поступает в балансировщик - и тот передает его на выполнение на Ноду2, которая "не знает", что этот же запрос уже выполняется на Ноде1.
Нода2 по ключу ищет в кэше, есть ли там информация о запросе с таким ключом, и находит запись о том, что такой запрос в обработке
Redis передает ответ на Ноду2 и
Нода2 передает на web-интерфейс ответ, что запрос в работе.
Нода2 ждет
В это время, Нода1 закончила обработку запроса, и записала в кэш результат запроса
а также передала ответ на интерфейс
Просыпается Нода2 - идет в кэш, чтобы проверить, появился ли ответ
Ответ появился, и Нода передает тот же самый ответ на интерфейс
И таким образом, в течение жизни ключа на Redis, запрос не будет выполняться повторно.
Все успехов! :-)
