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

Когда несколько обработчиков одновременно получают один и тот же ключ события, загрузка может замедлиться ещё до того, как упрётся в CPU: транзакции ждут разрешения конфликта в уникальном индексе. В PostgreSQL 16 идемпотентная запись защищает от дублей, но не делает повтор бесплатным — под нагрузкой первыми обычно страдают горячие ключи, время ожидания и пропускная способность записи.
Защита от дублей тоже конкурирует за ресурсы
Типичная схема — передавать с событием устойчивый ключ, например пару «источник + ID события», и закрепить её уникальным ограничением в базе. Повтор с тем же ключом не создаёт вторую строку. Это надёжнее проверки вида «сначала SELECT, потом INSERT»: между проверкой и вставкой другой процесс успевает записать тот же ключ.
Но уникальность — это не очередь и не кэш. Если сто запросов одновременно пытаются записать один ключ, PostgreSQL должен проверить уникальный индекс и согласовать результат с незавершёнными транзакциями. Конкурирующие операции могут ждать. При этом разные ключи обычно дают базе больше возможностей выполнять работу параллельно.
Идемпотентность — свойство операции, при котором повтор с теми же входными данными не меняет итог так, будто операцию выполнили несколько раз. На практике важно определить, что именно считается «теми же данными»: один только ключ или ключ вместе с содержимым события.
Что ломается первым
1. Ключ становится горячим
Если все повторы бьют в одну запись, ограничение работает правильно, но запросы выстраиваются вокруг одной точки конфликта. Особенно неприятно, если обработчик на конфликте не просто пропускает запись, а обновляет строку: тогда повторы могут создавать лишние версии строки, генерировать WAL и требовать последующей очистки таблицы.
В PostgreSQL 16 для простого случая можно вставлять событие с ON CONFLICT DO NOTHING. Ниже — пример для подготовленной таблицы; в приложении значения передаются как параметры, а не вставляются в SQL конкатенацией.
CREATE TABLE ingest_events (
source_id text NOT NULL,
event_id text NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (source_id, event_id)
);
INSERT INTO ingest_events (source_id, event_id, payload)
VALUES ('billing', 'evt-1042', '{"status":"paid"}'::jsonb)
ON CONFLICT (source_id, event_id) DO NOTHING
RETURNING source_id, event_id;
Первое выполнение вернёт строку, повтор с той же парой ключей — нет. Это позволяет понять, вставилась ли новая запись, но само по себе не сообщает, совпадает ли содержимое повтора с исходным событием. Если одинаковый ключ с другим payload — ошибка для вашего домена, такую ситуацию нужно обнаруживать и обрабатывать отдельно.
2. Повтор превращается в перезапись
ON CONFLICT DO UPDATE удобен, когда новая версия записи должна заменить старую. Но для событийного потока безусловное обновление опасно: запоздавший повтор может затереть более свежие данные. Нужны явные правила порядка — например, версия события или время источника, если ему можно доверять. Без них база гарантирует выполнение SQL, но не правильность бизнес-результата.
3. Транзакция держит конфликт слишком долго
Уникальный индекс — только часть работы транзакции. Если обработчик после вставки вызывает внешнюю систему, делает тяжёлые вычисления или ждёт сеть, он может дольше удерживать блокировки и соединение. Для конкурирующего повтора это означает более долгое ожидание; для всей системы — занятые соединения и растущую очередь запросов.
Сначала фиксируйте необходимое состояние в короткой транзакции, а внешние действия выносите в отдельный надёжный процесс. Если отправка письма или платёжного уведомления должна происходить ровно один раз, одной уникальности в таблице событий недостаточно: нужно отдельно проектировать передачу и повтор внешнего эффекта.
Что измерять, а не угадывать
Без замеров нельзя честно назвать порог нагрузки: он зависит от железа, схемы, размера событий, числа соединений и доли повторов. Сравнивайте сценарии на одинаковом стенде: обычные новые ключи, повторы распределённых ключей и повторы одного горячего ключа. Фиксируйте входную скорость, долю конфликтов и задержки на уровнях p50, p95 и p99.
| Сигнал | Что может означать | Что проверить |
|---|---|---|
| Растут p95 и p99 записи | Очередь ожидания или перегруженный диск | Время ожидания блокировок, ввод-вывод и длительность транзакций |
| Много ожиданий блокировок | Повторные запросы сходятся на одних ключах или строках | Распределение ключей и действия на конфликте |
| Растут WAL и объём таблицы при стабильном потоке событий | Повторы обновляют строки или транзакции создают лишнюю запись | Долю INSERT и UPDATE, частоту очистки и размер индексов |
| Заполнен пул соединений | Запросы или транзакции ждут дольше, чем освобождаются соединения | Время транзакций и ожидания в pg_stat_activity |
В PostgreSQL 16 начните с pg_stat_activity: там видны активные запросы и состояние ожидания. Сопоставьте это с метриками приложения и графиками диска. Если включён pg_stat_statements, используйте его для сравнения времени и числа вызовов SQL; сначала уточните, установлен ли модуль и включён ли он в вашей конфигурации.
Уникальный ключ не устраняет нагрузку от повторов. Он не даёт повторам испортить данные — а цену конкуренции нужно измерить отдельно.
Где этот подход ломается
Идемпотентная запись отвечает на вопрос «создавать ли эту строку ещё раз», но не решает автоматически порядок событий, согласованность с внешними системами и бесконечный поток уникальных ключей. На границах эти различия становятся критичными:
- Повтор с другим содержимым. Сравнивайте payload или его надёжный отпечаток и определите, считать ли расхождение ошибкой. Не позволяйте молча принять новый смысл под старым ключом.
- События приходят не по порядку. Храните правило определения актуальной версии и не полагайтесь на время получения запроса как на порядок событий.
- Ключи растут без срока хранения. Удаление старых ключей может снова разрешить повторную обработку. Срок хранения и границы повторной доставки должны быть согласованы.
- Запись и внешний эффект расходятся. Транзакция PostgreSQL не делает атомарными запись в базе и вызов стороннего API. Нужен отдельный механизм надёжной доставки и повторов.
- Одна команда содержит дубликаты ключа. Для
ON CONFLICT DO UPDATEPostgreSQL не позволяет одной команде изменить одну и ту же существующую строку несколько раз. Дубликаты внутри пачки нужно обнаружить или свести до отправки команды.
Что делать дальше
Сначала зафиксируйте контракт: из чего состоит ключ, что считать конфликтом содержимого и как долго хранить запись о принятом событии. Затем проведите нагрузочный тест с отдельным сценарием «много запросов на один ключ» и сравните его с равномерно распределёнными ключами. Если задержка растёт именно на конфликте, рассмотрите объединение повторов до базы или разбиение горячего потока по независимым ключам — но только если это не меняет требуемую семантику.
После теста наблюдайте в проде за p95/p99 записи, ожиданиями блокировок, занятостью пула и долей конфликтов. Инвалидация старых ключей и сквозная доставка внешних эффектов — отдельные задачи; их нельзя считать решёнными одной уникальной записью.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: PostgreSQL 16: INSERT, PostgreSQL 16: мониторинг статистики
Частые вопросы
Достаточно ли добавить уникальный индекс, чтобы повторы не создавали проблем?
Почему повторный запрос долго выполняется, если он ничего не вставляет?
Можно ли использовать этот пример в PostgreSQL другой версии?
Что делать, если одинаковый ключ пришёл с другим payload?
Уникальность в базе гарантирует, что внешнее действие выполнится один раз?
Как понять, что узкое место именно в конфликтах ключей?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.