Real-Time Backend
Idempotency
В 2012 году у Knight Capital из-за retry без идемпотентности за 45 минут сгорело USD 440 млн - алгоритм дважды отправил ордера на покупку акций. Idempotency - это не академическая концепция, это защитный слой между retry-логикой и деньгами.
- Stripe требует `Idempotency-Key` заголовок для всех POST-запросов на списание - UUID генерируется на клиенте один раз и кешируется 24 часа, предотвращая двойные платежи при network timeout.
- Kafka idempotent producer использует пару `producerId + sequenceNumber` - брокер отбрасывает сообщения с sequenceNumber <= lastSeen, обеспечивая exactly-once запись без дубликатов.
- AWS SNS FIFO-топики принимают `MessageDeduplicationId` - сообщения с одинаковым ID в 5-минутном окне доставляются подписчикам только один раз, даже при параллельных publish.
- Uber обрабатывает поездки с idempotency keys - если запрос на завершение поездки ушёл дважды из-за мобильного приложения, водитель получает оплату один раз.
Что такое идемпотентность
Операция называется **идемпотентной**, если её повторное выполнение с теми же параметрами даёт тот же результат, что и первое. Формально: `f(f(x)) = f(x)`. Сетевые запросы теряются и повторяются - это не баг протокола, а физическая реальность. Без идемпотентности каждый retry - потенциальный двойной платёж, дублированный заказ или испорченное состояние.
HTTP-семантика как первый ориентир
HTTP-спецификация уже закодировала идемпотентность: GET, PUT, DELETE - идемпотентны по определению. POST - нет. PUT /users/42 {name: 'Alice'} можно отправить 100 раз - состояние ресурса не изменится после первого успеха. POST /orders каждый раз создаёт новый заказ.
Idempotency - это контракт между клиентом и сервером: «если запрос дойдёт дважды, эффект будет как от одного». Клиент получает право на бесплатные retry, сервер берёт на себя ответственность за дедупликацию.
Какой HTTP-метод НЕ является идемпотентным по спецификации?
Стратегии дедупликации
Дедупликация - механизм, который позволяет серверу распознать повторный запрос и вернуть закешированный результат вместо повторного выполнения. Стратегии различаются по тому, что именно служит «отпечатком» запроса.
- **Content hash** - хеш тела запроса. Работает, если одинаковые данные всегда означают одну и ту же операцию. Слабость: два разных платежа на одну сумму от одного пользователя неразличимы.
- **Client-generated key** - UUID, который клиент генерирует один раз и сохраняет локально. Stripe, Adyen, PayPal используют именно этот подход. Самый надёжный.
- **Sequence numbers** - монотонно растущий счётчик на клиенте. Kafka idempotent producer присваивает каждому сообщению `producerId + sequenceNumber`. Брокер отбрасывает дубликаты по этой паре.
- **Natural business key** - `orderId`, `transactionRef`. Работает только если бизнес-ключ уникален по определению задачи.
- **Request fingerprint + TTL** - хеш (метод + URL + тело + userId) + окно времени (5-30 мин). AWS SNS MessageDeduplicationId живёт 5 минут.
- Content hash — Просто реализовать, но ложные совпадения при одинаковых данных
- Client UUID key — Надёжно, клиент контролирует границы операции, требует хранения на клиенте
- Sequence numbers — Компактно, встроено в Kafka, требует stateful producer
Kafka idempotent producer использует для дедупликации пару значений. Что это за пара?
Idempotency Keys на практике
Idempotency key - это UUID (или другой уникальный токен), который клиент генерирует один раз перед отправкой запроса, передаёт в заголовке, и сохраняет локально для retry. Сервер использует этот ключ как cache key для результата операции.
Stripe принимает `Idempotency-Key` в заголовке для всех `POST`-запросов. Ключ привязывается к паре (API key + endpoint + key value) - один ключ нельзя переиспользовать для другого endpoint или с другими параметрами. Результат кешируется 24 часа.
AWS SNS поддерживает `MessageDeduplicationId` для FIFO-топиков. Сообщения с одинаковым ID в течение 5-минутного окна дедупликации доставляются только один раз. Если `ContentBasedDeduplication` включён - SNS считает хеш тела сам, без явного ID.
Клиент отправил POST /payments с Idempotency-Key: abc123 и параметрами amount=100. Затем переотправил тот же ключ, но с amount=200. Что вернёт Stripe?
Серверная реализация
Серверная реализация idempotency layer строится вокруг трёх операций: check (есть ли ключ в store?), lock (предотвратить параллельное выполнение), store (сохранить результат). Redis - стандартный выбор для хранения из-за TTL и атомарных операций.
5xx-ответы НЕ кешируются - если сервер упал в середине обработки, клиент должен иметь право на retry с тем же ключом. Кешировать нужно только финальные состояния: 2xx (успех) и 4xx (ошибка бизнес-логики, retry бессмысленен).
- Клиент генерирует UUID один раз и сохраняет локально (localStorage, SQLite, память).
- Сервер при первом запросе ставит lock (Redis SET NX), выполняет операцию, сохраняет результат, снимает lock.
- При повторном запросе с тем же ключом - сервер возвращает сохранённый результат без повторного выполнения.
- При параллельных запросах с одним ключом - второй получает 409 и должен ждать и повторить.
- Ключ имеет TTL (Stripe - 24ч, обычная практика - от 1ч до 7 дней в зависимости от операции).
Idempotency key гарантирует exactly-once выполнение бизнес-логики
Idempotency key гарантирует exactly-once с точки зрения клиента - логика может выполниться один или более раз, но клиент увидит один результат
Если сервер упал после записи в БД, но до сохранения в idempotency store - операция выполнилась, но при retry выполнится снова. Полная exactly-once гарантия требует транзакционной атомарности между основной БД и idempotency store (2PC или outbox pattern).
Почему 5xx-ответы не следует сохранять в idempotency cache?
Ключевые идеи
- Идемпотентная операция: `f(f(x)) = f(x)` - повторное выполнение с теми же параметрами не меняет результат. HTTP GET/PUT/DELETE идемпотентны по спецификации, POST - нет.
- Idempotency key - UUID, который клиент генерирует один раз, передаёт в заголовке при каждом retry, сервер использует как cache key для результата операции (TTL обычно 24ч-7 дней).
- 5xx-ответы не кешируются в idempotency store - операция могла не завершиться, клиент должен иметь право на retry с тем же ключом.
- Полная exactly-once гарантия требует атомарности между основной БД и idempotency store - outbox pattern или 2PC.
Связанные темы
Idempotency пересекается с несколькими фундаментальными паттернами распределённых систем:
- Outbox Pattern — Обеспечивает атомарность между записью в БД и публикацией события - без него idempotency store и основная БД могут рассинхронизироваться при crash.
- Retry и Circuit Breaker — Retry без idempotency keys - источник дублирования. Circuit breaker ограничивает количество retry, но не решает проблему дублей - нужен именно idempotency layer.
- Exactly-Once Delivery — Idempotency keys - один из механизмов достижения exactly-once семантики в связке с at-least-once delivery гарантиями брокеров сообщений.
Вопросы для размышления
- Какой TTL для idempotency key выбрать для операции оформления заказа - 5 минут, 24 часа или 7 дней? От чего зависит этот выбор?
- Что произойдёт с idempotency гарантией, если Redis (idempotency store) недоступен - стоит ли пропускать запрос или блокировать его?
- Как реализовать idempotency для операции, которая состоит из нескольких шагов (создать заказ -> зарезервировать товар -> списать деньги)?