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 бессмысленен).

  1. Клиент генерирует UUID один раз и сохраняет локально (localStorage, SQLite, память).
  2. Сервер при первом запросе ставит lock (Redis SET NX), выполняет операцию, сохраняет результат, снимает lock.
  3. При повторном запросе с тем же ключом - сервер возвращает сохранённый результат без повторного выполнения.
  4. При параллельных запросах с одним ключом - второй получает 409 и должен ждать и повторить.
  5. Ключ имеет 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 для операции, которая состоит из нескольких шагов (создать заказ -> зарезервировать товар -> списать деньги)?

Связанные уроки

  • db-13-transactions
Idempotency

0

1

Войти