Идемпотентность: что произойдёт, если запрос придёт дважды
Пользователь нажал кнопку «Оплатить».
Ответ не пришёл.
Он нажал ещё раз.
А сеть решила тоже поучаствовать и повторила первый запрос.
Что должно произойти?
Правильный ответ обычно не такой:
«Будем надеяться, что второй запрос не успеет».
Для таких ситуаций существует идемпотентность.
Если упростить, идемпотентная операция при повторном выполнении приводит систему к тому же конечному состоянию, что и при первом.
Это особенно важно там, где повтор стоит дорого:
— списание денег; — создание заказа; — начисление бонусов; — отправка сообщения; — обработка события из очереди.
Например, клиент отправляет запрос на создание платежа вместе с Idempotency-Key.
Сервис получает ключ, выполняет операцию и сохраняет результат.
Если запрос с тем же ключом приходит повторно, сервис не создаёт ещё один платёж, а возвращает результат уже выполненной операции.
Звучит просто.
Но дальше начинается архитектура.
Нужно решить:
— где хранить ключи; — сколько времени их хранить; — что делать, если первый запрос ещё выполняется; — как проверить, что с тем же ключом пришёл действительно тот же запрос; — какой ответ возвращать после частично выполненной операции; — как пережить сбой между изменением данных и сохранением результата.
Например, недостаточно просто записать ключ после списания денег.
Сервис может успеть провести платёж, упасть до сохранения ключа, а после повтора провести платёж ещё раз.
Поэтому идемпотентность — это не заголовок в HTTP-запросе. Это согласованность ключа, состояния операции и её результата.
Особенно заметна эта проблема в распределённых системах.
Клиент повторяет операцию после тайм-аута.
Сеть повторно доставляет запрос.
Consumer получает одно событие несколько раз.
Формально хочется доставки «ровно один раз». Фактически обеспечить её по всей цепочке обычно намного сложнее, чем нарисовать на архитектурной диаграмме.
Поэтому хорошая система строится не вокруг надежды:
«Сообщение придёт один раз».
А вокруг гарантии:
«Если оно придёт ещё раз, ничего страшного не произойдёт».
Мы не пытаемся сделать распределённую систему идеальной.
Мы проектируем её так, чтобы ожидаемые сбои были безопасными.