Чтобы интеграция не создавала дублирующие счета, заказы или платежи, каждой бизнес-операции нужен стабильный идентификатор, который сохраняется при всех повторных попытках. Получатель должен атомарно зарегистрировать этот идентификатор, заблокировать повторное выполнение и при повторном запросе вернуть уже полученный результат. Одного ключа идемпотентности в заголовке API недостаточно: защита должна охватывать базу данных, параллельные запросы, очереди сообщений и внешние сервисы. Практически надежное решение сочетает ключ идемпотентности, ограничение уникальности в базе данных, сохраненный статус операции и регулярную сверку операций с неопределенным результатом.
Что такое идемпотентность в интеграции
Идемпотентной называется операция, при которой повторение одного и того же запроса не меняет ожидаемый результат после первого успешного выполнения. Например, если запрос на создание заказа из-за сетевого сбоя отправлен три раза, в системе все равно должен остаться один заказ. В ответ на повторные попытки можно возвращать идентификатор ранее созданного заказа вместо создания новых записей.
Семантика методов HTTP сама по себе не гарантирует такой результат. RFC 9110 определяет некоторые методы как идемпотентные, однако бизнес-побочные эффекты конкретной системы все равно необходимо проектировать корректно. В свою очередь, POST по умолчанию не является идемпотентным, но конкретную конечную точку POST можно сделать безопасной для повторных запросов с помощью ключа идемпотентности и серверного контроля.
Важно отличать технический запрос от бизнес-операции. Если пользователь намеренно создает два одинаковых заказа, это две операции. Если клиент повторяет один запрос после тайм-аута, это одна операция с несколькими попытками доставки. Механизм идемпотентности должен уметь различать эти случаи.
Почему возникают дубликаты
Обычно дубликат возникает не потому, что система намеренно выполняет команду дважды. Более распространенная причина — неопределенный результат. Клиент отправляет запрос, сервер создает счет, но ответ теряется. Клиент фиксирует тайм-аут и повторяет попытку, не зная, что первое действие уже завершено.
Аналогичную ситуацию могут вызвать:
- автоматическая повторная попытка со стороны HTTP-клиента или прокси-сервера;
- повторная доставка сообщения после ошибки потребителя;
- два параллельно работающих обработчика или фоновых задания;
- повторный щелчок пользователя;
- повторное получение события webhook;
- возобновление процесса после сбоя сервера;
- ручной запуск зависшего задания без проверки предыдущего состояния.
Поэтому запрет повторных попыток не является надежной стратегией. В распределенной системе повторы часто необходимы для преодоления временных сбоев. Нужно сделать так, чтобы повторная попытка была контролируемой и не меняла бизнес-результат.
Внедрение идемпотентности на шести уровнях
1. Назначьте идентификатор в начале бизнес-операции
Ключ идемпотентности следует создавать в той системе, где возникает намерение выполнить операцию. Это может быть случайно сгенерированное значение или стабильный бизнес-идентификатор, если его уникальность четко определена. Главное требование: во всех повторных попытках одной операции должен использоваться один и тот же ключ.
Если при каждой повторной попытке генерировать новый ключ, получатель увидит новые запросы и не сможет распознать дубликат. Область действия ключа также следует определить точно. Безопаснее оценивать его вместе с идентификатором клиента или арендатора и типом операции, например uzņēmums + izveidot-rēķinu + atslēga. Это не позволит ключу одного клиента случайно заблокировать действие другого.
2. Свяжите ключ с неизменным содержимым запроса
Сервер должен сохранять не только ключ, но и цифровой отпечаток значимых входных данных либо поля, которые можно сравнить в канонической форме. Если тот же ключ повторно получен с другой суммой, валютой, составом заказа или получателем, запрос нельзя без уведомления считать успешно повторенным.
В такой ситуации следует вернуть сообщение о конфликте и зарегистрировать диагностическую информацию. Иначе клиент может решить, что выполнена новая версия операции, хотя сервер вернул результат первой. Необходимо также определить нормализацию данных, поскольку семантически одинаковые документы JSON могут различаться порядком полей или несущественными деталями форматирования.
3. Атомарно зарезервируйте операцию в базе данных
Перед выполнением бизнес-побочного эффекта сервер пытается создать запись идемпотентности с уникальным ключом. Основной защитой от состояния гонки должно служить ограничение уникальности в базе данных, а не только предварительный запрос SELECT.
Два параллельных процесса могут одновременно проверить, что записи нет, и оба продолжить работу. Уникальный индекс или ограничение позволяет только одному из них получить право начать операцию. PostgreSQL INSERT ... ON CONFLICT — один из технических инструментов для такого резервирования, однако после конфликта все равно нужно прочитать существующую запись, сравнить содержимое и оценить ее статус.
4. Сохраняйте статус и возвращайте результат при повторном запросе
В записи идемпотентности полезно хранить как минимум ключ операции, область действия, цифровой отпечаток входных данных, статус, время создания и ссылку на результат. Типичными статусами могут быть processing, succeeded, failed и unknown, однако их значение в конкретной системе следует определить однозначно.
Если операция уже завершена, в ответ на повторный запрос возвращают сохраненный результат или ссылку на созданный объект. Если обработка еще продолжается, сервер может сообщить, что результат не готов, или предоставить конечную точку для проверки статуса. Это безопаснее, чем параллельно запускать второе выполнение.
Не каждую ошибку следует сохранять как окончательную. Ошибка валидации до резервирования отличается от ошибки после выполнения внешнего платежа. Модель статусов должна показывать, можно ли повторить операцию или сначала нужно выяснить ее фактический результат.
5. Защитите сам бизнес-объект
Таблица идемпотентности не должна быть единственным уровнем защиты. В бизнес-таблице также необходимо ограничение уникальности, соответствующее смыслу процесса. Для счета это может быть сочетание исходного заказа и типа счета, а для заказа — идентификатор заказа во внешней системе вместе с источником. Точная модель зависит от того, допускаются ли для одного заказа несколько частичных счетов, исправления или отдельные поставки.
Такое ограничение защищает и в тех случаях, когда из-за ошибки обходится уровень идемпотентности API. Однако слишком широкое условие уникальности может заблокировать законную повторную покупку. Поэтому уникальный бизнес-ключ должен основываться на правилах процесса, а не на предположении, что одинаковые сумма и клиент обязательно означают дубликат.
6. Передавайте тот же идентификатор внешним сервисам
Если локальная система вызывает платежный сервис, сервис выставления счетов или службу доставки, идемпотентность должна сохраняться и на этой границе. Если сервис поддерживает ключи идемпотентности, при повторном вызове следует использовать тот же ключ. Документация Stripe API — один из общедоступных примеров такого подхода к запросам POST.
Если внешний сервис не поддерживает идемпотентность, необходимо использовать его бизнес-идентификаторы, проверку статуса и процесс сверки. После тайм-аута нельзя автоматически считать, что действие не произошло. Сначала нужно проверить статус во внешней системе. Если определить его невозможно, локальная операция должна оставаться в неопределенном состоянии, а не сразу создавать следующий платеж или счет.
Практический пример создания счета
Предположим, CRM отправляет в ERP команду создать счет для заказа A-1048. Перед первой попыткой CRM создает ключ операции и сохраняет его вместе с заданием. ERP получает запрос и в одной транзакции базы данных резервирует запись идемпотентности и создает счет либо надежное поручение на его создание.
Если ответ теряется, CRM повторяет запрос с тем же ключом. ERP находит завершенную запись и возвращает идентификатор уже созданного счета. Если два запроса поступают одновременно, ограничение уникальности позволяет только одному из них начать выполнение.
Дополнительной защитой служит уникальная связь между исходным заказом и конкретной ролью счета. Ее нужно моделировать достаточно точно, чтобы не мешать созданию авансовых, окончательных или корректирующих документов, если они предусмотрены процессом. Идемпотентность определяет поведение повторной технической команды, но сама по себе не устанавливает правила нумерации или исправления счетов компании.
Очереди сообщений, outbox и inbox
Потребитель очереди сообщений может завершить бизнес-операцию, но аварийно остановиться до подтверждения получения. Тогда посредник доставит сообщение повторно. Документация RabbitMQ о подтверждениях потребителей объясняет механизм, из-за которого приложение должно быть готово к повторной доставке.
Подход outbox позволяет в одной локальной транзакции базы данных сохранить и бизнес-изменение, и событие, которое нужно отправить. Отдельный процесс публикует событие и может повторять попытки. Inbox или таблица дедупликации на стороне получателя регистрирует идентификаторы обработанных сообщений и не выполняет одно и то же действие повторно.
Outbox не решает все проблемы автоматически. Публикация и получение по-прежнему могут повторяться, но повтор становится безопасным. Кроме того, идентификатор одного события не всегда защищает бизнес-операцию: два разных события могут попытаться создать один счет. Поэтому необходимы как дедупликация сообщений, так и уникальность на уровне бизнеса.
Что проверить перед внедрением
Идемпотентность нельзя подтвердить одним успешным тестом API. Проверки должны имитировать неопределенность и параллельное выполнение:
- Отправьте один запрос несколько раз подряд с одним ключом и проверьте, что создан только один объект.
- Отправьте несколько одновременных запросов с одним ключом и проверьте ограничение уникальности в базе данных.
- Повторно используйте ключ с измененной суммой или получателем и ожидайте однозначного сообщения о конфликте.
- Прервите соединение до получения ответа, но после выполнения операции сервером, а затем повторите запрос.
- Аварийно остановите потребителя после транзакции базы данных, но до подтверждения сообщения.
- Имитируйте тайм-аут внешнего сервиса и проверьте, выясняет ли система сначала статус предыдущей попытки.
- Проверьте операцию после истечения срока хранения записи дедупликации.
- Запустите ручной резервный процесс и убедитесь, что он использует те же правила уникальности.
Срок хранения нельзя выбирать только исходя из стоимости базы данных. Он должен охватывать возможный период повторных попыток, задержанной доставки и сверки. Если удалить запись слишком рано, старая повторная попытка снова будет выглядеть как новая операция. В то же время более длительное хранение увеличивает объем данных и может потребовать минимизации чувствительных полей.
Наблюдение и сверка
При мониторинге полезно отдельно учитывать успешные повторные запросы, конфликты ключей и содержимого, надолго зависшие записи processing, операции со статусом unknown и расхождения между локальной и внешней системами. Эти показатели помогают отличить нормальный повтор от дефекта интеграции или ошибочного поведения клиента.
Для процессов, связанных с деньгами и документами, необходимо задание сверки, которое сопоставляет записи обеих систем по стабильной внешней ссылке. Идемпотентность снижает вероятность дубликатов, но не может доказать, что удаленная операция выполнена, если недоступны ответ внешней системы и запрос ее статуса.
Ограничения и риски
Идемпотентность не делает ошибочную команду правильной. Если исходная система по ошибке создает две разные операции с разными ключами, технический механизм может выполнить обе. Такие проблемы предотвращают бизнес-валидация, правила подтверждения и подходящий уникальный ключ.
Ключ идемпотентности не заменяет аутентификацию и авторизацию. Сервер по-прежнему должен проверять, имеет ли отправитель право выполнять действие и получать доступ к ранее сохраненному результату. Ключ нельзя использовать как секретное подтверждение права доступа.
Следует также избегать слишком узкой или слишком широкой области действия. Один ключ для всех операций клиента заблокировал бы законные действия, а ключ, хранящийся только в памяти одного процесса, не обеспечил бы защиту после перезапуска сервера. Состояние дедупликации должно находиться в постоянном хранилище, доступном всем обработчикам конкретной конечной точки.
Наиболее сложны побочные эффекты за пределами локальной транзакции: электронное письмо могло быть отправлено, платеж принят или документ создан непосредственно перед сбоем. Здесь необходимы идемпотентность внешнего сервиса, проверка статуса или сверка. Обещание абсолютного однократного выполнения не обосновано, если у всех участвующих систем нет общей атомарной границы.
Рекомендуемые внутренние ссылки
Следующим шагам читателя соответствуют материалы о документировании границ процесса в статье biznesa-procesa-kartesana-pirms-automatizacijas, выборе вида интеграции в статье api-integracija-vai-vienkarsa-datu-apmaina и проектировании ручного резервного процесса в статье automatizacijas-kludu-apstrade-manualais-rezerves-process.
Заключение
Надежная идемпотентность начинается не с запрета повторных попыток, а с распознавания одной бизнес-операции по всей цепочке интеграции. Стабильный ключ должен доходить до базы данных, очереди и внешнего сервиса, а ограничения уникальности и сохраненный статус должны предотвращать параллельное выполнение. Если внешний результат определить невозможно, система должна это признать и направить операцию на проверку статуса или сверку, а не вслепую создавать следующее действие.