awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Серверный state machine: четыре состояния idempotency key

Суть Когда запрос с Idempotency-Key приходит на сервер, он проходит четыре состояния: new, in-flight, replay, mismatch. Fingerprint запроса ловит переиспользование ключа с другим намерением.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

POST с Idempotency-Key K1 прилетает на сервер. Через две секунды тот же ключ прилетает снова — но поле amount изменилось. Проиграть старый ответ? Обработать новое списание? Отказать? Неправильный выбор — либо потерянное изменение, либо двойное списание.

Четыре состояния

Когда запрос с Idempotency-Key приходит, сервер ищет ключ в кеше дедупликации (таблица Postgres или запись Redis). Ровно четыре исхода:

1. New — строки для этого ключа нет. Сервер вставляет (key, fingerprint, status=in_progress, expires_at) в той же транзакции, что бизнес-логика, обрабатывает работу, затем обновляет строку: (response_body, response_status, status=completed).

2. In-flight — строка с status=in_progress существует. Другой запрос уже обрабатывает этот ключ конкурентно. Сервер возвращает 409 Conflict с {"error": "idempotency key in use"} немедленно. Клиент откатывается и ретраит.

3. Replay — строка с status=completed и сохранённым ответом. Сервер возвращает кешированный body и status code без повторного запуска бизнес-логики. Клиент получает одно подтверждение. Клиент списан один раз.

4. Mismatch — строка для этого ключа есть, но fingerprint входящего запроса не совпадает. Клиент переиспользовал ключ с другим намерением (другая сумма, другой получатель). Сервер возвращает 422 Unprocessable Entity. Клиент должен сгенерировать новый ключ для новой операции.

СостояниеУсловиеОтвет сервераБизнес-логика запускается?
NewСтроки для ключа нет200/201 (после обработки)Да
In-flightСтрока, status=in_progress409 ConflictНет
ReplayСтрока, status=completed, fingerprint совпадает200 (кеш)Нет
MismatchСтрока, fingerprint отличается422 Unprocessable EntityНет

Почему fingerprint запроса важен

Fingerprint — это хеш тела запроса (и опционально метода, пути и ключевых заголовков). Stripe хеширует body; платёжные API, зависящие от Stripe-Account, включают его явно.

Без fingerprint клиент, случайно переиспользовавший ключ для другой суммы, молча получит исходный ответ — новая сумма потеряна навсегда. Fingerprint превращает это в явную ошибку (422) вместо молчаливой потери данных.

Стоимость: одна SHA-256 на запрос — микросекунды. Дешёвая страховка.

Где хранить кеш

Таблица Postgres (простая, durable):

CREATE TABLE idempotency_keys (
  key          TEXT PRIMARY KEY,
  fingerprint  TEXT NOT NULL,
  response_body JSONB,
  response_status INT,
  status       TEXT NOT NULL DEFAULT 'in_progress',
  expires_at   TIMESTAMPTZ NOT NULL
);
CREATE INDEX ON idempotency_keys (expires_at);

Дневная cleanup-задача удаляет строки с истёкшим expires_at. Stripe v1 держал ключи 24 часа; v2 расширил до 30 дней.

Redis с TTL (высокий throughput): SETNX key value EX ttl_seconds — атомарный set-if-not-exists. Redis быстрее, но рискует потерять записи при async-fsync сбое. Платёжные API с юридической ответственностью держат авторитетную запись в Postgres и используют Redis как hot-path read-through cache.

Почему это работает

Почему 409 для in-flight вместо блокировки до завершения первого запроса? Блокировка занимает серверное соединение на всё время потенциально медленного внешнего API-вызова (десятки-сотни миллисекунд). 409 дешевле: клиент ретраит после короткого backoff, к тому времени первый запрос обычно уже завершился и строка перешла в replay-состояние.

Викторина

POST в /charge с Idempotency-Key K1 завершился успешно. Клиент ретраит K1 с другим полем amount. Что должен вернуть сервер?

Викторина

Почему сервер возвращает 409, а не блокируется, когда видит ключ со status=in_progress?

Викторина

Клиент ретраит Idempotency-Key K1 с тем же body через 26 часов после первой попытки (Stripe v1 TTL = 24 часа). Что делает сервер?

Вспомните перед уходом
  1. 01
    Зачем нужен fingerprint запроса (хеш body) наряду с idempotency key, и что ломается без него?
  2. 02
    Проследи POST /charge, который завершился успешно, потом ретраится с тем же ключом и body. Что происходит на каждом шаге?
  3. 03
    Когда хранить idempotency key в Postgres, а когда в Redis, и какой компромисс по durability?
Итог

Каждый lookup idempotency key разрешается в одно из четырёх состояний: new (вставить и обработать), in-flight (409 для backoff), replay (вернуть кешированный ответ), mismatch (422 — fingerprint изменился). Fingerprint запроса — SHA-256 body — делает replay безопасным: без него переиспользованный ключ с другой суммой молча вернул бы неправильный ответ. Stripe v1 держит ключи 24 часа; v2 расширил до 30 дней для compliance. После истечения строка удалена, сервер обрабатывает запрос как новый.

Связанные уроки
встречается в179
Продолжить восхождение ↑Стратегии retry: backoff, jitter и thundering herd
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.