Недавно начал изучать Spring и возник такой вопрос. В Spring Boot при выполнении длительного метода, как сделать, чтобы @Transactional метод блокировал (ставил в очередь) запросы на изменение пока длится мой метод?
Как пример могу привести следующее:
@Transactional
public List<User> findUsers() {
Thread.sleep(5000);
return userDao.findAll();
}
В UserService я вызываю данный метод. Параллельно вызываю другой метод на удаление:
@Transactional
public void delete(int id) {
userDao.deleteById(id);
}
Удаление происходит сразу, а читаю я уже меньшее количество юзеров. А нужно, чтобы удаление отработало после завершения моего первого метода.
P.S. это просто пример, а по факту есть определенная логика, которая должна работать как транзакцияия в БД.
UPD: По логике перед покупкой идет проверка наличия необходимого количества товара на складе. Если всех позиций хватает, то идет покупка (уменьшение товара на складе). По идее изменение данных строк / таблиц должно быть недоступно во время всего метода, чтобы другой пользователь в это же время не купил. Как ограничить доступ к изменению таблиц на период всего действия метода? Или, быть может, я смотрю не в ту сторону и реализация должна быть другой?
@Override
@Transactional
public void buyProducts() {
List<ProductOrder> productOrders = showCurrent(); //получение данных из таблицы с помощью dao
for (ProductOrder po : productOrders) { //проверка наличия
if (po.getCount() > po.getProduct().getAmount()) {
return;
}
}
for (ProductOrder po : productOrders) { //уменьшение остатка на складе
po.getProduct().setAmount(po.getProduct().getAmount()-po.getCount());
po.getOrder().setPaid(true);
}
}
UPD 2: проверка Optimistic lock:
Запускаю метод findUsers(). Пока он 5 секунд работает, запускаю changeUsers(). findUsers() не выбрасывает OptimisticLockException, а возвращает результат до изменения.
UserService:
@Override
@Transactional
public List<User> findUsers() throws InterruptedException {
List<User> users = userDao.findAll();
userDao.lock(users);
Thread.sleep(5000);
return userDao.findAll();
}
@Override
@Transactional
public List<User> changeUsers() {
List<User> users = userDao.findAll();
int rand = new Random().nextInt(1000);
for (User user : users) {
user.setMoney(rand);
}
return users;
}
UserDAO:
@Override
public void lock(List<User> users) {
for (User user : users) {
entityManager.lock(user, LockModeType.OPTIMISTIC);
}
}
Блокировать всю таблицу плохо, т.к. это сильно влияет на производительность. Если использовать этот подход, то пользователи не смогут одновременно делать покупку, даже если они покупают совершенно разные товары. Если уж использовать блокировку (а это необязательно, сейчас до этого дойдем), то нужно делать ее более гранулярной, т.е. блокировать не всю таблицу, а отдельные записи.
Сначала вкратце очерчу проблему в общем. Главное за что тут идет борьба это согласованность данных. Т.е. не должно быть ситуации, когда мы отметили товар как проданный несколько раз (когда несколько пользователей параллельно делают покупки) или продали несуществующий товар (когда администратор параллельно удалил его из БД).
Вообще, подходов для решения есть несколько. У каждого есть плюсы и минусы.
Первый это пессимистическая блокировка. Суть в том, что мы блокируем ресурсы, которые собираемся менять, так чтобы другие транзакции не могли этого сделать параллельно. Второй способ это оптимистическая блокировка. Ее суть в том, что во время сохранения изменений мы проверяем не изменился ли ресурс после того того как мы его читали перед началом обработки. Если изменился, то либо пробуем повторить операцию заново (т.е. перечитать ресурс, сделать все проверки и заново сохранить с проверкой модификаций опять же), либо возвращаем ошибку. Тут зависит от природы операции, некоторые можно повторить, а некоторые нельзя.
Рассмотрим сначала пессимистическую блокировку. В данном случае это можно сделать несколькими способами, в зависимости от того, что считать ресурсами. Это решение вы принимаете при проектировании системы и оно часто также влияет на бизнес логику и видимое поведение системы.
Например, можно блокировать всю таблицу товаров. Это решение простое в реализации, но его минус в том, что пользователи не могут выполнять шаг, который вызывает buyProducts
, параллельно. Если у вас система используется не очень активно, т.е. параллельные покупки редкость и увеличение не предвидится, то можно этот вариант и рассмотреть.
Гораздо лучше вариант с пессимистической блокировкой отдельных записей. Это может выглядеть по разному. Например может быть так:
@Override
@Transactional
public void buyProducts() {
List<ProductOrder> productOrders = showCurrent(); //получение данных из таблицы с помощью dao
// блокировка ассоциированных продуктов
dao.lockForUpdate(getProducts(productOrders));
// далее все как раньше
}
Если используете JPA то lockForUpdate
может выглядеть так:
void lock(Collection<?> entities) {
for(Object entity : entities) {
entityManager.lock(entity, LockModeType.PESSIMISTIC_WRITE);
}
Также можно блокировать сущности прямо при запросе, чтобы не делать дополнительные запросы на блокировку.
Если используете JDBC, то такая блокировка делается с помощью запроса SELECT .. FOR UPDATE
.
Плюсом пессимистической блокировки является простота подхода. Минус - это хуже масштабируется. Каждая блокировка занимает ресурсы БД, и она делается независимо от того есть ли реальный конфликт или нет и если модифицируется много записей, то на это может уходить много ресурсов БД.
Если конфликты редки, то оптимистическая блокировка обычно лучше, т.к. она требует дополнительных действий (а значит ресурсов) только, когда конфликт реально случается.
Работает она таким образом, что в каждую сущность, изменения в которой мы хотим контролировать и не давать это делать параллельно, добавляется поле, которое хранит версию. Сохранение теперь делается условным, оно проходит успешно, только если версия сущности, которую мы сохраняем не поменялась. Это обязательно делается атомарно на уровне БД (обычно запрос вида UPDATE table set x=... WHERE id=<id> and version=<version>
).
В JPA для этого используется аннотация Version. Тут сам код buyProducts
не меняется вообще. Сама реализация JPA добавляет необходимые проверки версии на основе аннотации Version
, которую нужно добавить в сущность продукт.
Так же нужно добавить реакцию, если вдруг случится конфликт, т.е. во время сохранения мы обнаружим, что версия изменилась. В простом случае, это просто возврат пользователю ошибки и просьба повторить заново.
Но в многих случаях (и в этом тоже) можно сделать автоматический повтор. Если используем JPA, то мы получим OptimisticLockException
исключение и для данного примера с покупкой, транзакцию можно повторить. Т.е. заново выполнить метод buyProducts
. Он при этом заново прочитает данные из БД (т.е. они уже будут включать изменения, которые сделала параллельная транзакция и из-за которых произошел конфликт) и заново попробует обработать запрос.
Реализовать это удобно аспектом, который нужно навесить на buyProducts
(и вообще на все модифицирующие операции). Этот аспект в случае исключения OptimisticLockException
будет заново вызывать метод buyProducts
. Также важно сконфигурировать порядок аспектов так, чтоб транзакция начиналась внутри этого аспекта, т.е. чтоб при повторе старая (неуспешная) транзакция откатилась (это произойдет автоматически т.к. случилось исключение) и открылась новая транзакция для повторного вызова buyProducts
.
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Пытаюсь сделать запрос на возражение, с использованием LEFT JOINПравильно ли я понимаю, что такой запрос будет работать только, если данные в БД с одной...
Решаю задачу получения строки из таблицы, соответствующей текущему запрошенному урлу