Идемпотентность в распределённых системах

·4 минут чтения
Идемпотентность в распределённых системах

Идемпотентность в распределённых системах

Распределённые системы ненадёжны. Запросы, ответы и сообщения могут потеряться, задержаться или быть прерваны. В результате вызывающая сторона не всегда может определить, была ли операция успешно завершена.

Представьте, что клиент отправляет запрос на создание платежа. Сервер успешно переводит деньги и фиксирует изменения в базе данных. Однако сетевое соединение обрывается до того, как ответ успевает дойти до клиента.

С точки зрения клиента возможны несколько вариантов:

  • запрос вообще не дошёл до сервера;
  • сервер начал обработку, но завершился с ошибкой;
  • операция успешно завершилась, но ответ был потерян.

Клиент никак не может понять, какой из этих вариантов произошёл на самом деле. Самое безопасное решение — повторить запрос.

Проблема в том, что повторный запрос может привести к повторному выполнению одной и той же логической операции. Для платежа это означает повторный перевод денег. Для заказа — создание двух одинаковых заказов. Для обработчика сообщений — повторную обработку одного и того же сообщения.

Именно эту проблему решает идемпотентность.

Что такое идемпотентность?

Идемпотентность делает повторные попытки безопасными.

Если клиент не уверен, что сервер успешно обработал запрос, он может повторить этот же запрос, используя тот же ключ идемпотентности (Idempotency Key).

Сервер определяет, что оба запроса представляют одну и ту же логическую операцию, выполняет её только один раз и возвращает тот же ответ, который был сформирован при первом выполнении.

С точки зрения клиента результат повторного запроса будет таким же, как если бы ответ на первый запрос никогда не был потерян.

Как это работает?

Клиент генерирует уникальный ключ идемпотентности и отправляет его вместе с запросом.

POST /payments
Idempotency-Key: 9d88c40b-66d4-4c0d-a379-1d8b4ef2d6a2

Когда сервер получает запрос, он начинает транзакцию базы данных и:

  1. Выполняет бизнес-операцию.
  2. Сохраняет запись об идемпотентности вместе с ответом.
  3. Фиксирует транзакцию.

Бизнес-данные и запись об идемпотентности должны сохраняться атомарно. В противном случае сервер может успешно выполнить операцию, но завершиться с ошибкой до сохранения записи об идемпотентности. Тогда повторный запрос приведёт к повторному выполнению операции.

Если позже приходит ещё один запрос с тем же ключом идемпотентности, сервер не запускает бизнес-логику повторно. Вместо этого он возвращает ответ, сохранённый при первом выполнении.

Почему нельзя просто искать одинаковые запросы?

Часто возникает вопрос, почему сервер не может просто сравнивать содержимое запросов.

Представьте, что пользователь сегодня переводит 100 долларов одному человеку, а завтра отправляет ещё один перевод с абсолютно теми же данными.

Хотя оба запроса полностью совпадают, они представляют две разные бизнес-операции.

Только клиент знает, является ли новый запрос повторной попыткой или совершенно новой операцией. Именно поэтому ключ идемпотентности создаёт клиент, а не сервер.

Ключ идентифицирует одну логическую операцию, а не конкретный HTTP-запрос.

Где используется идемпотентность?

Идемпотентность полезна везде, где операция может быть выполнена повторно.

Типичные примеры:

  • обработка платежей;
  • создание заказов;
  • создание счетов;
  • обработка вебхуков;
  • REST API;
  • Kafka Consumer;
  • RabbitMQ Consumer;
  • фоновые задачи;
  • шаги Saga.

Вызывающей стороной может быть HTTP-клиент, фоновая задача или обработчик сообщений. Во всех случаях, если невозможно определить, была ли предыдущая попытка успешной, операция может быть выполнена повторно. Идемпотентность гарантирует, что повторное выполнение одной и той же логической операции не приведёт к дополнительным побочным эффектам.

Основные способы реализации

Существует несколько распространённых способов реализации идемпотентности.

Отдельная таблица идемпотентности

Наиболее распространённый вариант — использовать отдельную таблицу для хранения уже обработанных операций.

Обычно она содержит:

  • ключ идемпотентности;
  • статус операции;
  • сохранённый ответ;
  • время создания;
  • время истечения срока хранения.

Когда приходит повторный запрос, сервер находит существующую запись и возвращает ранее сохранённый ответ.

Уникальный индекс в бизнес-таблице

Иногда ключ идемпотентности хранится прямо в бизнес-таблице и защищается уникальным индексом.

Например, таблица Payments может содержать колонку IdempotencyKey с уникальным индексом. Попытка создать ещё один платёж с тем же ключом завершится ошибкой, после чего сервер сможет вернуть результат уже существующей операции вместо создания нового платежа.

Распространённые ошибки

Путать идемпотентность с поиском дубликатов

Идемпотентность не запрещает пользователю намеренно выполнять одинаковые действия несколько раз.

Если клиент отправит два одинаковых запроса, но с разными ключами идемпотентности, это будут две разные логические операции, даже если их содержимое полностью совпадает.

Сохранять только ключ

Во многих случаях сохранить только ключ недостаточно.

Чтобы вернуть клиенту точно такой же ответ, сервер обычно сохраняет не только ключ, но и сам ответ вместе с записью об идемпотентности.

Неатомарное сохранение данных

Запись об идемпотентности и бизнес-операция должны сохраняться в рамках одной транзакции.

В противном случае частичный сбой может привести к повторному выполнению одной и той же операции.

Заключение

Идемпотентность существует потому, что повторные попытки неизбежны в ненадёжных системах.

Вместо того чтобы пытаться определять одинаковые запросы, клиент присваивает каждой логической операции уникальный ключ идемпотентности. Сервер сохраняет этот ключ вместе с результатом выполнения операции и гарантирует, что любая повторная попытка с тем же ключом вернёт исходный ответ вместо повторного выполнения бизнес-логики.

Другими словами, идемпотентность не устраняет повторные попытки — она делает их безопасными.