Pull to refresh

Защита от повторного запроса с помощью БД

Если вы разрабатываете Веб-приложение или REST-сервис, то рано или поздно столкнётесь с повторными запросами. Что имеется в виду? Объясню на примере Веб-страницы с кнопкой. По нажатию на кнопку, на бэкенд отправляется запрос. Запрос, соответственно, синхронный и пока серверная часть делает какую-то работу, браузер клиента показывает, что загружает страницу. Если это происходит продолжительное время, клиент может подумать, что его запрос завис и нажать кнопку ещё раз. Также повторное нажатие может произойти случайно.

Какая тут может произойти проблема? Если это, например, какой-то запрос данных, то в общем-то проблемы и нет, но если это действие, которое должно отработать только один раз, то тут могут быть весьма неприятные последствия. Для примера в интернет-магазине собрана корзина и создан заказ на оплату (статус REGISTERED), далее по нажатию кнопки "оплатить" с клиента списывают деньги и переводят заказ в статус оплачен (PAID). И если в этом процессе произойдёт двойной запрос, то с клиента могут списать деньги за заказ два раза.

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

Order order = getOrder(payInfo.getOrderId());
if ("REGISTERED".equals(order.getState())) {
    spendMoney();
    order.setState("PAID");
}

Дело в том, что если два запроса придут практически одновременно, то оба они сначала выберут запись из БД и у обеих статус будет REGISTERED, и соответственно оба потока сделают списание денег.

Решим эту проблему с помощью базы данных. Так как БД делает изменения атомарно, то используем это для решения нашей проблемы. Принцип такой: мы пытаемся выполнить в БД запрос на обновление статуса с условием, что запись имеет статус REGISTERED. И ещё запрос должен вернуть количество обновлённых записей. Далее, уже в java коде, нам нужно проверить, сколько записей обновилось. Если 1, то это у нас "истинный" запрос, то есть он смог обновить запись и нужно сделать перечисление денег. Если 0 – запись не обновилась, значит кто-то уже успел её обновить до нас и деньги списывать не нужно.

public int updateState(Long id, String currentState, String newState) {
    return entityManager.createQuery(
           "UPDATE Order SET state=:newState WHERE id=:id AND state=:currentState"
           )
            .setParameter("id", id)
            .setParameter("currentState", currentState)
            .setParameter("newState", newState)
            .executeUpdate();
    }

Далее изменим наш первоначальный код следующим образом:

int updated = updateState(payInfo.getOrderId(), "REGISTERED", "PAID");
if (updated > 0) {
    spendMoney();
}

Вот такой вот нехитрый способ сможет уберечь нас от неприятных ошибок.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.