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

Если отправитель не получил ответ, он может повторить запрос — даже когда первая попытка уже записала данные. Идемпотентная загрузка не даёт этому повтору создать второй заказ или повторно начислить сумму. Разберём механизм на PostgreSQL 16: где возникает гонка, что должна гарантировать база и почему дедупликация не равна доставке «ровно один раз».
Откуда берутся повторы
Загрузчик редко контролирует всю цепочку. Событие проходит через отправителя, сеть, очередь, обработчик и базу. Если соединение прервалось после записи, но до подтверждения, отправитель не знает, успел ли получатель сохранить данные. Безопасная для него стратегия — отправить событие повторно.
Есть и другие причины: очередь повторяет сообщение после тайм-аута, процесс падает при обработке пачки, а оператор запускает импорт ещё раз. Поэтому надёжная система обычно допускает повторную доставку и делает повторную обработку безопасной.
Идемпотентность — не обещание, что событие придёт один раз. Это гарантия, что повтор одного и того же события не создаст второй эффект.
Важно определить, что именно считается «тем же событием». Обычно отправитель передаёт устойчивый идентификатор: например, ID события в своей системе. Хеш содержимого может помочь сравнить данные, но не всегда подходит как идентификатор: одинаковые по полям события могут быть двумя самостоятельными операциями.
Как уникальный ключ закрывает гонку
Проверка вида «сначала поискать ID, потом записать» не защищает от параллельной обработки. Два обработчика могут одновременно не найти запись и оба продолжить работу. Проверка и запись должны опираться на атомарное ограничение уникальности в хранилище.
В PostgreSQL 16 для этого удобно завести таблицу входящих событий с составным первичным ключом: источник плюс ID события. Ключ не даёт одному источнику случайно столкнуться с ID другого. Команда INSERT ... ON CONFLICT пытается вставить событие, а при конфликте выполняет заданное действие вместо ошибки.
CREATE TABLE ingestion_inbox (
source text NOT NULL,
event_id text NOT NULL,
payload jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (source, event_id)
);
BEGIN;
INSERT INTO ingestion_inbox (source, event_id, payload)
VALUES ('billing', 'evt-2025-0001', '{"amount": 1250}'::jsonb)
ON CONFLICT (source, event_id) DO NOTHING
RETURNING event_id;
COMMIT;
Если команда вернула строку, запись добавлена. Если не вернула — такой ключ уже занят. В рабочем обработчике прикладное изменение, например создание заказа, нужно выполнить в той же транзакции и только при новой вставке. Если транзакция откатится, откатятся и отметка о событии, и изменение заказа.
При двух одновременных вставках уникальный индекс становится арбитром: обе операции не смогут зафиксировать один ключ. Одна вставит запись, другая дождётся разрешения конфликта и выполнит ветку DO NOTHING. Это надёжнее проверки в памяти приложения, где два процесса не видят общий результат до записи.
Какой ключ выбрать и что хранить
Ключ должен переживать повторы: если при каждой попытке генерировать новый UUID, база будет видеть новое событие. Обычно его выдаёт система-источник. Пространство ключей стоит ограничить источником, а сам ID — хранить в том же формате на протяжении всего срока обработки.
| Подход | Что даёт | Цена и риск |
|---|---|---|
| Уникальный ID источника | Повтор распознаётся независимо от изменений в представлении данных. | Нужно договориться о стабильном ID и пространстве имён. |
| Хеш полезной нагрузки | Позволяет обнаружить совпадающее содержимое без отдельного ID. | Изменение сериализации меняет хеш; два одинаковых по содержимому события могут оказаться разными операциями. |
| Проверка в приложении до записи | Проста для прототипа. | Не защищает от параллельной гонки и нескольких экземпляров обработчика. |
Полезно хранить не только ключ, но и саму нагрузку либо её устойчивое представление. Тогда при повторе с тем же ID можно проверить, совпадает ли содержимое. Если ID прежний, а данные отличаются, это не обычный дубль: возможны ошибка источника, переиспользование идентификатора или нарушение контракта. Молча отбросить такое событие — значит скрыть проблему.
Сноска. Идемпотентный ключ — идентификатор, по которому обработчик распознаёт повтор конкретной операции. Он не должен меняться между повторными попытками одной операции.
Граница транзакции важнее самой дедупликации
Запись в inbox и изменение бизнес-данных должны фиксироваться вместе. Иначе возможны два сбоя: отметка о событии записалась, а заказ — нет; либо заказ создался, а отметка не сохранилась, и повтор создаст его снова. В обоих случаях одна лишь таблица дедупликации не спасает.
Внешние действия — отправка письма, вызов платёжного сервиса, публикация в другую очередь — обычно не входят в транзакцию PostgreSQL. Нельзя считать их атомарными вместе с записью в базе. Частый подход — сохранить намерение отправить событие в отдельную таблицу в той же транзакции, а отправку выполнить отдельно с повторными попытками. Получателю всё равно нужна защита от дублей.
Идемпотентность также не гарантирует порядок. Два разных события могут прийти не по очереди, а обработка одного события может повториться после уже обработанного следующего. Если важна последовательность, нужны отдельное правило порядка и проверка версии или номера события. Это самостоятельная задача, а не свойство уникального ключа.
Где подход ломается и что измерять
Дедупликация действует только пока система помнит ключ. Если удалить запись раньше, чем источник прекратит повторы, старое событие снова пройдёт как новое. Срок хранения выбирают по реальному окну повторной доставки и восстановления; универсального значения нет.
- Ключ меняется при ретрае. Проверьте, что ID создаёт отправитель один раз на операцию, а не обработчик при каждом запросе.
- Разные источники используют одинаковые ID. Включите источник в уникальный ключ.
- Повтор с изменённой нагрузкой исчезает без следа. Сравнивайте сохранённое содержимое и отправляйте конфликт в журнал или очередь разбора.
- Внешний эффект выполняется до фиксации. Сбой после отправки письма, но до коммита может привести к повторной отправке. Разделяйте транзакционную запись и внешний вызов.
- Таблица растёт без контроля. Следите за размером inbox, частотой конфликтов и сроком хранения ключей; очистка не должна открывать окно для поздних дублей.
Сравнивайте долю повторов и конфликтов по источникам, задержку вставки при конкуренции, число событий с тем же ID, но другой нагрузкой, а также отставание очереди отправки внешних эффектов. Для оценки производительности фиксируйте размер пачки, число параллельных обработчиков, объём таблицы и долю конфликтов. Без этих условий сравнение «до и после» мало что говорит.
Что делать дальше
Сначала зафиксируйте контракт с отправителем: кто выдаёт ID, сколько он живёт и может ли повтор прийти с изменённым содержимым. Затем проверьте на тесте два сценария: один и тот же ключ одновременно обрабатывают два воркера; транзакция падает между записью события и бизнес-изменением. После этого измеряйте частоту конфликтов и срок хранения ключей в рабочей системе. Для PostgreSQL 16 проверьте детали поведения ON CONFLICT и транзакций в документации именно этой версии.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: PostgreSQL 16: INSERT, PostgreSQL 16: Transaction Isolation
Частые вопросы
Идемпотентность означает, что событие доставят ровно один раз?
Можно ли обойтись проверкой ID в приложении?
Что делать, если повтор пришёл с тем же ID, но другими данными?
Нужно ли хранить ключи навсегда?
Подойдёт ли этот пример для другой версии PostgreSQL?
Как защитить отправку письма или вызов внешнего API от повторов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.