Перейти к содержимому
DedicatedPHP Контакт

Идемпотентность в PHP при таймаутах и повторных попытках

Проектируйте повторяемые PHP-операции при таймаутах, конкуренции и потерянных ответах, не дублируя платежи, заказы и внешние вызовы.

Редакционная схема PHP API, использующего ключ идемпотентности для контроля повторных попыток, статусов и внешних вызовов

Таймаут не означает, что операция завершилась неудачей: он лишь подтверждает, что клиент не получил ответ в установленный срок. Сервер мог уже создать заказ, платёжный провайдер мог принять списание, или асинхронный процесс мог продолжать выполняться. Если клиент выполняет повторную попытку без контроля, одно и то же бизнес-намерение может привести к дублирующим эффектам.

Идемпотентность в PHP превращает техническое повторение либо в запрос результата, либо в возврат уже полученного результата. Она не состоит в игнорировании всех дубликатов и не сводится к надежде, что пользователь не нажмёт кнопку дважды. Это явный контракт между клиентом, API, хранилищем и, когда применимо, внешними системами.

Проблема: ответ потерян, но эффект сохраняется

Проблема: ответ потерян, но эффект сохраняется — guía visual de DedicatedPHP

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

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

  • Бизнес-намерение: «я хочу подтвердить именно эту покупку».
  • Технический запрос: HTTP-отправка с заголовками, телом и контекстом аутентификации.
  • Попытка выполнения: каждая внутренняя обработка, повторная попытка из очереди или вызов провайдера.

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

Каким операциям нужна идемпотентность, а каким — нет

Отдавайте приоритет операциям, которые создают, подтверждают, списывают, отправляют, резервируют, уведомляют или изменяют ресурс со значимыми последствиями. Явными кандидатами являются POST /payments, подтверждение заказа или приём webhook. То же относится к задаче очереди, которая может быть доставлена более одного раза.

Чистому чтению обычно не нужен ключ идемпотентности. Обновление может иметь другую семантику: установка желаемого состояния, например PUT /profiles/42, может быть идемпотентной по своей конструкции, если одно и то же представление оставляет ресурс неизменным. Напротив, действие вроде «увеличить баланс» не становится идемпотентным только из-за использования определённого глагола.

Ключ также не следует использовать как замену другим правилам. Чтобы предотвратить два резервирования, конкурирующих за ограниченные запасы, нужны инварианты предметной области, контроль конкуренции и политика резервирования. Чтобы выполнить задачу ровно один раз в распределённой среде, фактическая доставка обычно происходит как минимум один раз; потребитель должен быть устойчив к дубликатам.

Проектирование ключа и постоянной записи

Клиент должен генерировать непрозрачный и достаточно непредсказуемый ключ в момент возникновения бизнес-намерения, сохранять его, пока возможны повторные попытки, и передавать, например, в Idempotency-Key. Если сервер генерирует его при каждом получении, он не сможет связать последующее повторение. Во внутренних потоках ключ может быть получен из стабильного идентификатора бизнес-события.

Его область действия должна включать пользователя или субъекта либо тенант и операцию. Одна и та же строка не должна конфликтовать между двумя учётными записями или между «создать заказ» и «оформить возврат». Определите срок хранения в соответствии с фактическим периодом повторных попыток и рисками предметной области. Слишком раннее удаление записи вновь открывает путь к дублированию; бессрочное хранение увеличивает стоимость и требует политики конфиденциальности и удаления.

Минимальная модель хранения включает:

  • область безопасности или тенант, имя операции и идемпотентный ключ;
  • криптографический хеш нормализованной полезной нагрузки;
  • статус: processing, completed, failed или pending, когда внешнее подтверждение неопределённо;
  • код и тело ответа, которые будут возвращаться повторяемо;
  • идентификаторы созданного ресурса, внутреннюю корреляцию и идентификатор внешнего провайдера;
  • даты создания, обновления и истечения срока.

Хеш предотвращает важную ошибку: повторное использование одного ключа с разными данными. В такой ситуации отвечайте конфликтом и не обрабатывайте новую полезную нагрузку. Чтобы сравнение было надёжным, нормализуйте поля, порядок которых не имеет значения, и исключайте изменяющиеся метаданные, не являющиеся частью намерения.

PHP-поток: резервируйте до создания эффекта

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

Рекомендуемый поток — атомарно создать резервирование. Если вставка успешна, этот процесс становится первоначальным владельцем выполнения. При конфликте уникальности считывается существующая запись, проверяется хеш и выполняется действие согласно её статусу. Завершённый результат возвращает в точности сохранённый ответ; операция в процессе может вернуть ожидающий статус или ждать только ограниченный интервал перед повторным чтением.

begin transaction
insert idempotency_records(scope, operation, key, payload_hash, status)
values (?, 'create_order', ?, ?, 'processing')
-- уникальное ограничение определяет владельца
commit

if reservation_was_created:
    result = execute_business_operation()
    persist_completed_response(result)
else:
    record = load_existing_record()
    assert_same_payload_hash(record)
    return replay_or_pending(record)

Не держите транзакцию или блокировку строки открытыми во время медленного вызова провайдера. Это снижает пропускную способность и может привести к длительным блокировкам. Вместо этого резервируйте и подтверждайте локальный статус в коротких транзакциях. Если внешний эффект и локальная запись должны координироваться, дополнительно сохраните сообщение для отправки в транзакционной таблице и обрабатывайте его отдельно. Этот паттерн не устраняет повторные попытки, но позволяет восстановить незавершённую работу, не теряя зафиксированного намерения.

Конкуренция, таймауты и неопределённые состояния

Два запроса с одним ключом могут поступить с разницей в миллисекунды. Уникальное ограничение определяет, какой из них резервирует операцию. Второй не должен запускать другой внешний эффект. Он может вернуть 202, пока статус равен processing или pending, включая идентификатор для запроса результата; если контракт требует синхронного ответа, он может выполнить ограниченное ожидание и повторно прочитать запись.

Сбой до запуска какого-либо эффекта позволяет установить failed с воспроизводимой ошибкой. Однако таймаут при вызове внешней системы создаёт неопределённость: нельзя автоматически помечать операцию как неудачную или просто повторно отправлять распоряжение. Сохраните идентификатор отправленного запроса, если он существует, запросите информацию у провайдера, используя этот идентификатор, и сверяйте результат. Пока подтверждения нет, сохраняйте pending и сообщайте, что результат ещё не является окончательным.

Внешнему вызову также нужен стабильный идентификатор. Если провайдер поддерживает собственный идемпотентный ключ, передавайте ключ, связанный с тем же намерением. Если он его не поддерживает, используйте идентификаторы продавца, последующее чтение, периодическую сверку и операционные процедуры для неоднозначных случаев. Ни одна локальная транзакция не может сделать атомарными запись в базе данных и вызов независимого удалённого API.

Чего не решает ключ идемпотентности

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

Также не путайте исправление с повторной попыткой. Если пользователь меняет адрес, сумму или товары после ошибки, возникает новое намерение и следует использовать новый ключ. Повторное использование прежнего ключа с другой полезной нагрузкой должно приводить к конфликту, а не молча обновлять исходную операцию.

Тестирование, наблюдаемость и чек-лист

Тестирование, наблюдаемость и чек-лист — guía visual de DedicatedPHP

Тестируйте не только счастливый путь. Прервите ответ после сохранения результата, параллельно повторите один и тот же ключ, перезапустите worker после резервирования записи и сымитируйте таймаут после отправки внешнего запроса. Убедитесь, что существует только один бизнес-ресурс, повторный ответ сохраняет тот же результат, а другая полезная нагрузка с тем же ключом не принимается.

Логируйте, не раскрывая чувствительные данные, ключ или полученный из него безопасный идентификатор, область действия, статус, корреляцию и внешний идентификатор. Метрики конфликтов ключей, операций, слишком долго находящихся в ожидании, и неразрешённых сверок помогают службам поддержки и эксплуатации отличать обычную повторную попытку от инцидента.

  • Представляет ли ключ бизнес-намерение и имеет ли определённую область действия?
  • Есть ли уникальное ограничение, предотвращающее два одновременных резервирования?
  • Сравнивается ли хеш полезной нагрузки и отклоняются ли изменения намерения?
  • Сохраняется ли ответ или результат, который можно согласованно повторить?
  • Позволяют ли неопределённые состояния выполнить запрос и сверку до повторной попытки?
  • Есть ли у каждого внешнего эффекта идентификатор, восстановление и операционная альтернатива?
  • Протестированы ли дубликаты, сбои, повторные попытки очереди и реальная конкуренция?

При таком применении идемпотентность не обещает надёжности сети. Она делает неизбежные сбои управляемыми, трассируемыми и согласованными с бизнесом.

Хотите применить эти идеи в своем проекте?Давайте обсудим вашу PHP-платформу.
Посмотреть связанные услуги