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

Обработчик записал платёж в базу, затем упал до подтверждения сообщения. Очередь доставит событие снова — и без защиты конвейер может создать второй платёж. Идемпотентность нужна не для того, чтобы исключить повторы, а чтобы повторная обработка одной операции не меняла итоговый эффект. Ниже — устройство такой защиты для конвейеров событий; на примере Kafka речь о документации версии 3.9, без привязки к конкретному языку или API.
Повтор — штатный сценарий, а не сбой очереди
В конвейере обычно есть источник, обработчик и приёмник: например, очередь событий, сервис преобразования и база данных. Между ними нет единого мгновенного действия. Обработчик может получить сообщение, изменить данные, а затем не успеть подтвердить получение. Для очереди операция не завершена, поэтому она отправит сообщение снова.
У приёмника та же проблема: запись могла пройти, но ответ потерялся по сети. Отправитель не знает, применилось ли изменение, и вынужден повторить запрос. Система доставки выбирает надёжность ценой возможных повторов; требование «получить ровно один раз» нельзя считать гарантией для всей цепочки только потому, что его обещает отдельный компонент.
Идемпотентность относится к эффекту операции. Если обработать одно и то же логическое событие повторно, состояние приёмника должно остаться таким, каким оно было бы после одной обработки. Это не означает, что код физически запустился один раз: он может выполнить те же шаги несколько раз, но не создать дубликат результата.
Надёжный конвейер не пытается угадать, случился ли повтор. Он узнаёт логическую операцию и не применяет её эффект второй раз.
Из чего складывается защита
1. Стабильная идентичность события
Каждому логическому действию нужен идентификатор, который сохраняется при повторной доставке: например, идентификатор заказа вместе с типом и версией операции. Номер попытки, время получения или случайный идентификатор новой доставки не подойдут: на повторе они изменятся, и приёмник посчитает событие новым.
Ключ должен обозначать именно операцию, а не просто запись. Один заказ может законно породить несколько изменений одного типа; если использовать только ID заказа, второе изменение ошибочно примется за дубликат. Слишком широкий ключ пропускает повтор, слишком узкий — подавляет новое действие.
2. Проверка и применение эффекта
Приёмник хранит, какие операции уже применил. Для реляционной базы распространённый вариант — таблица обработанных идентификаторов с уникальным ограничением. Обработчик в одной транзакции проверяет идентификатор, применяет бизнес-изменение и записывает факт обработки. Если транзакция откатится, не останется ни отметки, ни частичного эффекта; если завершится — повтор упрётся в уже занятый ключ.
Критично слово «одной». Если обновление заказа и запись идентификатора выполняются отдельными транзакциями, процесс может упасть между ними. Записали заказ, но не отметку — повторит изменение. Поставили отметку, но не записали заказ — повтор будет отброшен, хотя работа не завершилась. Две записи в разных системах нельзя сделать атомарными обычным порядком вызовов.
3. Подтверждение после надёжной записи
Сообщение стоит подтверждать после того, как приёмник надёжно зафиксировал эффект. Тогда падение между записью и подтверждением приводит к повтору, который защита распознаёт. Если подтвердить сообщение заранее, а затем упасть до записи результата, событие может потеряться без возможности автоматического повтора.
Термин: атомарность — свойство транзакции, при котором связанные изменения фиксируются вместе либо не фиксируется ни одно. Граница атомарности проходит внутри конкретного хранилища; она не охватывает автоматически очередь, базу и внешний платёжный сервис.
Где заканчивается гарантия «ровно один раз»
В Kafka 3.9 транзакционный производитель может связать запись выходных сообщений с фиксацией смещений входных сообщений. Это помогает получить exactly-once-обработку внутри сценария чтения из Kafka и записи обратно в Kafka: повтор обработки не должен породить второй видимый результат для потребителя, который читает только подтверждённые транзакции.
Но внешняя база или API платёжного сервиса не становятся частью транзакции Kafka. Если обработчик списал деньги во внешней системе, а затем не зафиксировал смещение, Kafka может доставить исходное событие снова. Транзакция брокера не отменит первое списание. Здесь нужен механизм на стороне получателя — например, поддержка ключа идемпотентности самим API или таблица обработанных операций, согласованная с бизнес-изменением.
Уведомления, электронные письма и другие необратимые действия требуют отдельного решения. База не может откатить письмо, уже принятое почтовым сервером. Частый подход — записать намерение отправить уведомление в той же транзакции, что и бизнес-изменение, а отдельному процессу поручить доставку с повторной проверкой идентификатора. Это сокращает окно несогласованности, но внешний сервис всё ещё может обработать повтор самостоятельно.
Подходы и их цена
| Подход | Что защищает | Цена и граница |
|---|---|---|
| Уникальный ключ и транзакционная запись в базе | Повторное применение события к данным в этой базе | Нужны стабильный ключ и атомарность; внешние эффекты остаются за границей |
| Идемпотентный API приёмника | Повторный вызов конкретной операции у внешнего сервиса | Приёмник должен поддерживать и правильно трактовать ключ; срок хранения ключей может быть ограничен |
| Транзакции Kafka 3.9 | Связь чтения и записи внутри Kafka | Не охватывают произвольную внешнюю базу или сервис; требуют корректной настройки и обработки транзакционных сообщений |
Хеш содержимого иногда используют как идентификатор, но это не универсальная замена ID операции. Два законных события могут иметь одинаковые данные, а изменение формата или порядка полей — дать другой хеш для того же смысла. Если содержимое может меняться при повторе, приёмнику полезно различать два случая: известный ключ с теми же параметрами — повтор, известный ключ с другими параметрами — конфликт, который нужно расследовать, а не молча игнорировать.
Что ломается на границах
- Отметка хранится меньше, чем возможен повтор. После удаления старого ключа поздний повтор снова выполнит эффект. Срок хранения выбирают по реальным окнам повторной доставки и переигрывания данных, а не только по обычной задержке очереди.
- Повтор пришёл в изменённом виде. Если ключ совпал, а сумма или другие важные поля отличаются, без проверки содержимого система может скрыть ошибку источника. Сравнивайте значимые параметры операции и поднимайте конфликт.
- События обработаны не по порядку. Идемпотентность сама по себе не решает задачу порядка. Старое обновление может затереть новое; нужны версия сущности, контроль последовательности или подходящая модель обновления.
- Ключ общий для разных действий. Например, один ID заказа используется и для создания, и для возврата. Разделяйте идентификаторы по типу логической операции.
- Внешний эффект не поддерживает повторы. Если вызов платёжного или почтового API повторился после сетевого тайм-аута, локальная отметка в базе не доказывает, что внешний эффект не случился. Проверяйте возможности конкретного сервиса и проектируйте сверку неопределённых результатов.
Как проверить конвейер без выдуманных цифр
Универсального замера производительности здесь нет: результат зависит от числа событий, конкуренции за ключи, хранилища и длительности транзакций. Вместо чужих цифр измерьте свою систему при одинаковом потоке и одинаковой конфигурации. Сравните обычную обработку с повторной доставкой, отдельно зафиксируйте задержку, пропускную способность и число конфликтов уникальных ключей.
Главный тест — прерывание процесса в каждой опасной точке: после получения сообщения, после записи бизнес-данных, до отметки об обработке и после фиксации транзакции, но до подтверждения сообщения. После перезапуска проверьте не только успешный ответ обработчика, но и конечное состояние приёмника: один ли платёж, заказ или другой эффект появился.
В проде наблюдайте за долей повторов, конфликтами ключей, временем обработки, задержкой очереди и размером хранилища отметок. Резкий рост повторов может указывать на тайм-ауты или сбои подтверждения; рост конфликтов — на нестабильную идентичность событий либо повторное использование ключей. Перед включением очистки отметок проверьте, можно ли после неё безопасно переиграть архивные события.
Что делать дальше
Нарисуйте границы конвейера и отметьте для каждого шага, где фиксируется результат, где подтверждается сообщение и кто отвечает за внешний эффект. Затем выберите один критичный сценарий и прогоните тест с падением между записью результата и подтверждением. Если эти точки невозможно описать однозначно, сначала исправьте границы транзакций и идентификаторы, а уже потом настраивайте повторы.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Apache Kafka 3.9 — Message Delivery Semantics, AWS Builders’ Library — Making retries safe with idempotent APIs
Частые вопросы
Идемпотентность означает, что обработчик запустится только один раз?
Хватит ли транзакций Kafka, если конвейер пишет в PostgreSQL?
Что делать, если событие повторилось с тем же ключом, но другой суммой?
Какой срок хранить идентификаторы обработанных событий?
Что проверить, если после падения всё равно появились дубликаты?
Отличается ли поведение в другой версии Kafka?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.