Внимание! Дистрибутив RK-CMS распространяется исключительно через сайт rk-cms.ru. Загрузка копий со сторонних ресурсов может привести к потере данных или установке вредоносного ПО.

Меню
    300 140

    Когда речь заходит о защите API от повторных запросов, первое, что приходит в голову - использовать идемпотентность. Клиент передаёт уникальный ключ (X-Request-Id, nonce или Idempotency-Key), а сервер проверяет, не выполнялся ли уже запрос с таким ключом. Звучит логично, но на практике это не спасает от гонок (race condition).

    Давайте разберёмся, почему.

    Что такое идемпотентность и как она обычно работает

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

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

    На первый взгляд кажется, что проблема решена: даже если клиент дважды отправит один и тот же запрос, повторная обработка не произойдёт.

    Случай 1: Два разных запроса с разными ключами

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

    Но что происходит, когда два разных запроса (с разными ключами) пытаются выполнить одну и ту же логическую операцию одновременно? Например, два разных пользователя (или два разных потока одного приложения) одновременно хотят списать деньги с одного и того же баланса.

    - Запрос 1 с ключом abc-111 читает баланс: 100 рублей.
    - Запрос 2 с ключом xyz-222 читает баланс: 100 рублей (потому что первый ещё не успел списать).
    - Запрос 1 выполняет списание 100 рублей. Баланс становится 0.
    - Запрос 2 тоже выполняет списание 100 рублей, потому что при проверке баланса он видел 100. Баланс становится -100.

    Идемпотентность здесь не помогает, потому что ключи у запросов разные. Сервер честно проверил: ключ xyz-222 не встречался, значит, можно выполнять операцию. Проблема не в повторной отправке, а в том, что оба запроса прочитали устаревшие данные до того, как кто-либо из них успел их изменить. Это классическая гонка.

    Случай 2: Даже одинаковый UUID может не спасти (если нет атомарности)

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

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

    Посмотрим, как обычно выглядит проверка ключа в коде:

    - Проверить, есть ли запись с ключом abc-123 в таблице processed_requests.
    - Если нет - выполнить операцию (например, списание).
    - Сохранить ключ abc-123 в таблицу processed_requests.

    Если два запроса с одним и тем же ключом приходят одновременно, возможна такая последовательность:

    - Поток A проверяет ключ abc-123 - записи нет.
    - Поток B проверяет ключ abc-123 - записи тоже нет (потому что A ещё не успел её создать).
    - Поток A выполняет списание и сохраняет ключ.
    - Поток B тоже выполняет списание (потому что его проверка была раньше) и тоже пытается сохранить ключ (возможно, получает ошибку уникальности, но операция уже выполнена).

    В результате операция выполнилась дважды, хотя ключ один и тот же. Проблема в том, что проверка наличия ключа и вставка ключа не являются единой (атомарной) операцией. Между ними есть окно, в которое пролезает второй поток.

    Именно поэтому многие опытные разработчики говорят: идемпотентность сама по себе не защищает от race condition. Она решает проблему повторных запросов в штатных ситуациях (когда между запросами есть задержка), но не спасает от одновременных параллельных обращений.

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

    Нужно всегда помнить о параллелизме и проектировать систему так, чтобы критические операции были атомарными.

    В RK-CMS мы это хорошо понимаем и используем комбинацию механизмов, чтобы гарантировать целостность данных даже под высокой нагрузкой.

    Для улучшения работы сайта и его взаимодействия с пользователями мы используем файлы cookie (узнать подробнее).

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