Идемпотентность

9 июня 2026 · ~12 мин чтения

концепция распределённые-системы api http базы-данных

Идемпотентность

Операция идемпотентна, если её повторное применение к тому же объекту не меняет результат по сравнению с однократным применением. Формально: f(f(x)) = f(x).

Звучит как абстракция из учебника по алгебре. На практике это различие между «деньги списались один раз» и «деньги списались пять раз из-за таймаута сети». Идемпотентность — фундамент любой надёжной распределённой системы.

История

Понятие пришло из математики. «Idempotent» (от латинских idem — тот же самый, potens — сильный) использовалось ещё в XIX веке в абстрактной алгебре для описания операций, повторение которых ничего не меняет. Например, умножение числа на 0: 0 × 0 × 0 = 0. Или операция «взять максимум из двух чисел»: max(max(5, 3), 3) = max(5, 3) = 5.

В программирование термин плотно вошёл в 2000 году вместе с диссертацией Роя Филдинга (Roy Fielding), в которой он описал архитектурный стиль REST. Филдинг формализовал свойства HTTP-методов: GET, HEAD, PUT, DELETE — идемпотентны, POST — нет. Это не случайные слова, это проектное решение, которое определяет, как кешировать, как обрабатывать сбои и как строить клиентов.

Формально свойства HTTP-методов закреплены в спецификации RFC 7231 (2014 год), которая заменила оригинальный RFC 2616 от 1999 года. В разделе 4.2.2 написано буквально: «A request method is considered "idempotent" if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.»

Настоящий взрыв практического интереса произошёл примерно в 2013–2014 году, когда Stripe ввёл заголовок Idempotency-Key в своём API. Это был прямой ответ на проблему платёжных дублей при сетевых таймаутах. Решение оказалось настолько удачным, что скопировали почти все платёжные и финансовые API: PayPal, Braintree, Adyen, российские банки в своих B2B API.

Что это такое

Разберём через конкретику. Допустим, ты нажал «Оплатить» в интернет-магазине. Запрос ушёл на сервер, сервер начал обработку, но соединение прервалось. Ты не знаешь: платёж прошёл или нет? Если нажать ещё раз — что случится?

Вот здесь всё и разделяется:

Неидемпотентная система (обычный POST): второй запрос создаёт второй платёж. У тебя списали деньги дважды. Такое реально происходило у крупных компаний — один из известных случаев в 2010-х был у авиакомпаний, когда мобильное приложение при нестабильном Wi-Fi покупало билет несколько раз.

Идемпотентная система (с ключом идемпотентности): сервер видит, что запрос с таким же Idempotency-Key уже обрабатывался, возвращает прежний результат. Списание происходит ровно один раз.

Важно разграничить схожие понятия:

HTTP-методы по свойствам:

Метод Безопасный Идемпотентный
GET Да Да
HEAD Да Да
OPTIONS Да Да
PUT Нет Да
DELETE Нет Да
POST Нет Нет
PATCH Нет Нет (обычно)

PATCH — интересный случай. Если PATCH /user означает «установить поле name в 'Иван'», это идемпотентно. Если означает «добавить 10 к счёту» — нет.

Аналогии из жизни

Выключатель света

Если свет выключен и ты нажимаешь «выключить» ещё раз — ничего не происходит. Идемпотентно. Но: если у тебя диммер с накоплением нажатий — каждое нажатие ещё больше гасит свет. Уже не идемпотентно. Аналогия ломается именно тут: кажущаяся «одинаковость» операции не гарантирует идемпотентность — важна семантика, а не форма.

Кнопка лифта

Нажал кнопку 7-го этажа — лифт едет на 7-й. Нажал ещё пять раз — лифт всё равно едет на 7-й, не на 12-й. Идемпотентно. Где ломается: кнопка вызова на этаже работает иначе в некоторых системах — каждый новый вызов может переставлять лифт в очереди. Это уже накапливающаяся операция.

Печать «уже прочитано»

Пометил письмо как прочитанное — статус изменился. Пометил снова — ничего не изменилось, письмо уже прочитано. Идемпотентно. Ломается тут: «отметить как непрочитанное» — тоже идемпотентная операция сама по себе, но комбинация «прочитано → непрочитано → прочитано» требует отслеживания порядка. Порядок операций важен, идемпотентность каждой отдельной — не достаточна для общей корректности.

Идемпотентность — свойство операции, не системы

Одна и та же система может иметь одни идемпотентные операции и другие — нет. GET /orders/123 идемпотентен, POST /orders/123/pay — нет. Это проектное решение на уровне каждого эндпоинта.

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

Рассмотрим два механизма: серверный и клиентский.

Ключ идемпотентности (Idempotency Key)

Это самый распространённый паттерн. Клиент генерирует уникальный идентификатор запроса (UUID или любой случайный строковый ключ) и включает его в запрос:

POST /v1/charges
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{
  "amount": 5000,
  "currency": "rub",
  "source": "card_token_abc"
}

Сервер:
1. Получает запрос, смотрит на Idempotency-Key
2. Ищет в базе: был ли уже запрос с таким ключом?
3. Если нет — обрабатывает, сохраняет результат вместе с ключом
4. Если да — возвращает сохранённый результат без повторной обработки

Ключ обычно хранится ограниченное время (Stripe хранит 24 часа). После этого тот же ключ будет обработан заново. Это защита от случайных совпадений ключей через большой промежуток времени.

Важная деталь: сервер должен хранить результат атомарно с его привязкой к ключу. Иначе два одновременных запроса с одним ключом могут пройти «в щель» между проверкой и записью (race condition).

-- Безопасная атомарная вставка (PostgreSQL)
INSERT INTO idempotency_keys (key, response, created_at)
VALUES ($1, $2, NOW())
ON CONFLICT (key) DO NOTHING;

-- Проверяем что именно записалось
SELECT response FROM idempotency_keys WHERE key = $1;

Если вставка не прошла (ON CONFLICT DO NOTHING) — значит запрос уже обрабатывался, читаем ранее сохранённый ответ.

Идемпотентные операции в базе данных

-- Не идемпотентно: каждый раз вставляет новую строку
INSERT INTO payments (user_id, amount) VALUES (42, 1000);

-- Идемпотентно: вставляет или ничего не делает
INSERT INTO payments (external_id, user_id, amount)
VALUES ('pay_abc123', 42, 1000)
ON CONFLICT (external_id) DO NOTHING;

-- Идемпотентно: вставляет или обновляет (upsert)
INSERT INTO user_settings (user_id, theme)
VALUES (42, 'dark')
ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme;

Второй и третий варианты можно запускать сколько угодно раз — результат будет одинаковым. Это особенно важно при репликации данных и при повторных запусках миграций.

Outbox pattern + идемпотентность

В распределённых системах часто используют паттерн «transactional outbox» (транзакционный ящик исходящих): сообщение сначала записывается в базу данных в той же транзакции, что и бизнес-данные, а потом отдельный воркер читает outbox и доставляет сообщения.

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

Где встречается в обычной жизни

Кнопка «Оплатить» в интернет-магазине. Если страница зависла и ты нажал три раза — магазин должен создать ровно один заказ. Правильно реализованные e-commerce сайты блокируют кнопку после первого нажатия и/или используют токен формы как idempotency key.

Банковские переводы. Когда ты переводишь деньги через мобильный банк и приложение зависает — банк должен либо подтвердить перевод, либо сообщить об ошибке, но не провести дважды. Большинство банков ассоциируют транзакцию с device + timestamp + sum и дедуплицируют.

SMS с кодом подтверждения. Если ты нажал «Отправить код» несколько раз подряд — нормальная система либо возвращает тот же код, либо ждёт таймаут перед новым. Это защита и от дублей, и от накрутки SMS-расходов.

Обновление ПО. Патч, применённый дважды к тем же файлам, не должен сломать систему. Большинство менеджеров пакетов (apt, brew, pip) идемпотентны: установить уже установленный пакет = ничего не делать.

Где встречается в IT и бизнесе

REST API. Используй PUT когда заменяешь ресурс целиком (PUT user/42 с полным объектом), PATCH — для частичного обновления конкретных полей, POST — только для создания новых ресурсов. Это не просто семантика — это контракт с клиентом: на PUT можно безопасно делать retry при таймауте, на POST — нет.

Очереди сообщений. Kafka, RabbitMQ, SQS работают в режиме «at-least-once delivery» (доставка хотя бы один раз). Это значит, что сообщение может прийти дважды при сбоях. Consumer должен быть идемпотентным, иначе дубли приводят к ошибкам бизнес-логики.

CI/CD и Terraform. terraform apply идемпотентен: запусти его 10 раз — состояние инфраструктуры будет таким, каким ты описал в конфиге, а не в 10 раз «больше». Ansible-плейбуки по той же логике должны быть идемпотентными: «установить nginx» — это не «запустить установку nginx», а «убедиться что nginx установлен».

Финансовые операции. Любой API, связанный с деньгами, обязан поддерживать идемпотентность. Ошибки сети — это не баг, это норма. Клиент должен иметь возможность безопасно повторить запрос.

Миграции баз данных. Хорошая миграция идемпотентна: если запустить её на базе, где колонка уже существует, она не упадёт с ошибкой, а спокойно пропустит шаг. CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS — стандартные инструменты.

PATCH без ключа — скрытая опасность

PATCH /orders/123 {"status": "shipped"} выглядит идемпотентно, но PATCH /wallets/42 {"amount": "+500"} — нет. Никогда не используй относительные изменения в PATCH-запросах без защиты от дублей. Либо ставь абсолютные значения, либо добавляй idempotency key.

Кто пользуется

Stripe — пионер в платёжной индустрии. Заголовок Idempotency-Key появился в их API около 2014 года и стал де-факто стандартом. Stripe обрабатывает миллиарды транзакций в год, и без идемпотентности это невозможно.

Amazon AWS. SQS стандартно гарантирует at-least-once доставку (хотя есть и FIFO-очереди с exactly-once за отдельную плату). Lambda-функции и другие потребители должны быть написаны с учётом возможных дублей.

Google Pub/Sub, Apache Kafka. Та же история: at-least-once по умолчанию, exactly-once — отдельная опция с оверхедом.

Брокеры и агрегаторы недвижимости. Синхронизация базы объектов между застройщиком и агрегатором — классическая задача, где идемпотентность критична: при сбое синхронизации нужно просто повторить весь пакет, не задумываясь о дублях.

Любой обработчик Telegram-вебхуков. Telegram гарантирует доставку вебхука «хотя бы один раз». Если сервер ответил не 200 OK — Telegram повторит попытку. Обработчик должен дедуплицировать по update_id.

Альтернативы и конкуренты

Exactly-once semantics (семантика ровно однажды)

Плюсы: устраняет необходимость в идемпотентности на стороне потребителя. Kafka версии 0.11+ поддерживает это для producer → topic.

Минусы: значительный оверхед (latency, throughput), работает только внутри одной системы, не поможет при HTTP-таймаутах между сервисами.

Distributed transactions / 2PC (двухфазная фиксация)

Плюсы: транзакционность через несколько систем «из коробки».

Минусы: блокировки, отказ координатора = зависание всей цепочки, ужасная производительность при масштабировании. В 2020-х практически не используется в новых системах.

Optimistic locking (оптимистичная блокировка)

Плюсы: лёгкая реализация через поле version в строке, хорошо работает при редких конфликтах.

Минусы: решает проблему конкурентных обновлений одного ресурса, но не решает проблему ретраев через сеть. Это другая плоскость.

Idempotency через версионирование ресурса (ETag/If-Match)

Плюсы: встроено в HTTP, работает без дополнительной логики.

Минусы: только для обновлений существующих ресурсов, не помогает при создании (POST).

Когда НЕ стоит использовать

Счётчики и аналитика событий. Если тебе нужно считать каждый клик или каждый просмотр — ты намеренно хочешь, чтобы повторное событие изменяло состояние. Idempotency key здесь вреден: он превратит «5 просмотров одной страницы» в «1 просмотр». Для аналитики используют deduplicated counts или обрабатывают дубли отдельно на стадии агрегации.

Append-only лог событий. Если архитектура построена на «каждое событие — неизменяемая запись в лог», тебе нужна at-least-once доставка и дедупликация при чтении/проекции, а не идемпотентная запись.

Операции с явно накопительной семантикой. «Добавить товар в корзину» — пользователь хочет добавить второй экземпляр того же товара. Idempotency key на таком эндпоинте сломает ожидание. Либо разделяй операции («установить количество = N» vs «добавить один»), либо явно не применяй дедупликацию.

Практическое правило

Прежде чем добавлять POST-эндпоинт: спроси себя «что произойдёт, если клиент вызовет этот метод дважды?» Если ответ «плохое» — либо сделай эндпоинт идемпотентным (PUT/upsert), либо добавь обязательный Idempotency-Key. Это значительно дешевле, чем чинить последствия дублей в продакшне.

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера в BrendBot (сервис квиз-лендингов для застройщиков) добавляли функциональность: при появлении нового Telegram-посетителя — отправлять его user_id в Make-Connect API с задержкой 20 минут. Проблема: webhook мог быть отправлен несколько раз при сетевых сбоях или перезапуске воркера. Решение — функция tg_idem_key(), которая генерирует уникальный ключ из идентификатора посетителя и проекта, плюс логика дедупликации в outbox-таблице: при повторной постановке в очередь старая запись отменяется, что гарантирует ровно одну доставку без дублей.

Краткое резюме