Двойное списание случается не потому, что кто-то ПЛОХОЙ ИНЖЕНЕР
Есть одна технология, которую большинство разработчиков начинает уважать только после первого серьёзного инцидента. Это не Kafka. Не Saga. Не распределённые транзакции.
Это идемпотентность.
Пока не работаешь с деньгами, кажется, что это очередной архитектурный термин, который любят спрашивать на собеседованиях. Потом случается первое двойное списание. И отношение резко меняется.
Мой самый дорогой UUID
Я познакомился с идемпотентностью именно так. Делал сервис, который списывал деньги за подписку через внешний API.
В документации было обязательное поле:
И комментарий примерно такой:
Ну случайную так случайную. Сгенерировал UUID. Передал его в запросе. Закрыл задачу. Через несколько дней выяснилось, что это был один из самых дорогих UUID в моей карьере.
Что произошло:
- Пользователь нажал кнопку оплаты.
- Запрос ушёл.
- Ответ где-то потерялся.
- Мой сервис решил: "Похоже, запрос не дошёл. Надо повторить."
- Сервис честно отправил ещё один запрос.
- Платёжная система честно обработала его ещё раз.
- Вместо одной подписки получилось два списания.
Дальше всё по классике:
- Саппорт.
- Возвраты.
- Разбор логов.
- Поиск виноватых (нет, поиск решения, у нас же есть postmortem, но об этом в другой раз).
Retry — зло
После таких историй многие делают неправильный вывод, что retry — это зло.
Нет. Retry вообще ни при чём. Наоборот. Retry — одна из лучших вещей, которые можно сделать для распределённой системы. Сеть падает. Балансировщики рвут соединения. Ответы теряются.
Повторные запросы — это абсолютно нормальная история.
Проблема начинается в тот момент, когда система не умеет понять, что перед ней тот же самый платёж. И вот здесь появляется идемпотентность. Идемпотентность нужна не потому, что запрос должен выполниться один раз. Она нужна потому, что однажды он обязательно придёт повторно.
- Из-за retry.
- Из-за таймаута.
- Из-за мобильного интернета.
- Из-за балансировщика.
- Из-за пользователя, который пять раз нажал кнопку «Оплатить».
Причина вообще не важна. Важно, чтобы деньги списались один раз.
Самые дорогие инциденты часто появляются не из-за плохих решений, а из-за хороших, придуманных хорошими инженерами. Кто-то хотел сделать систему надёжнее. Добавил retry. Настроил автоматические повторы. Сделал всё правильно. Кроме одной детали. Не научил систему понимать, что два одинаковых запроса — это одна и та же бизнес-операция. Иногда именно такие мелочи оказываются самыми дорогими.
Пиши в комментариях, а у тебя было?