Если вы разрабатываете Веб-приложение или 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();
}
Вот такой вот нехитрый способ сможет уберечь нас от неприятных ошибок.