Концепт-урок Idempotency: как делать retry безопасными. Показывает 4 сценария — наивный POST с двойным charge, спасение через Idempotency-Key + Redis dedup store, conditional update (compare-and-set с version), и counter increment trap с op_id deduplication. Топология: mobile client, payment API, idempotency store (Redis), payments ledger (Postgres), card processor.
Идемпотентность — свойство наблюдаемого результата повторного того же намерения. Она не означает, что код выполнится один раз, сеть доставит сообщение один раз или любой POST безопасно повторять.
Клиент создаёт key один раз на логическую операцию. Сервер атомарно хранит tenant, key, canonical fingerprint запроса, state и стабильный response. Минимальная state machine:
| State | Значение | Допустимое действие повтора |
|---|---|---|
| PENDING | один владелец исполняет операцию | ждать ограниченно или вернуть in-progress |
| SUCCEEDED | terminal result сохранён | вернуть тот же status/body |
| FAILED_FINAL | повтор не изменит исход | вернуть сохранённую ошибку |
| UNKNOWN | внешний эффект мог произойти | reconciliation, не blind retry |
Unique constraint на tenant + key выбирает одного владельца. Fingerprint защищает от случайного использования ключа для другой суммы, объекта или команды. TTL — продуктовая политика, а не часть определения идемпотентности; он должен покрывать максимальное окно повторов и задержанных сообщений.
После сохранённого SUCCEEDED повтор читает authoritative row и не вызывает provider. Redis допустим как cache terminal response, но не как единственный источник истины.
Два одновременных запроса — главный race, который не ловит обычная проверка «GET key, затем SET». Нужны unique insert/compare-and-set и явная политика ожидания.
Тот же ключ с другим fingerprint — 409 или аналогичная client error. Возвращать старый charge для новой суммы опасно.
Timeout после отправки во внешний PSP означает неизвестный исход. Локальная БД и PSP не делят одну транзакцию: сначала сохраняется durable intent, provider получает тот же key, затем reconciler запрашивает исход по key/operation id. Только доказанный not-found разрешает новую попытку по правилам provider.
Локальную мутацию и idempotency row можно коммитить одной DB transaction. Для внешнего эффекта применяют state machine, provider idempotency key, outbox/worker, reconciliation и audit trail. Фраза «exactly once» без границ обычно скрывает повторное выполнение или неизвестный исход.
HTTP PUT и DELETE идемпотентны по семантике RFC 9110; POST — нет по умолчанию. If-Match полезен против lost update, но не заменяет dedup key для внешнего charge.
Введите числа или выберите пресет