У меня есть приложение REST, написанное с использованием Spring Boot, в котором у меня есть кошелек. В кошельке есть такие методы, как addAmount, deductAmount и так далее. Вот код:
WalletController.java
public class WalletController {
public final LoadDatabase loadDatabase;
private final WalletRepository repository;
@Autowired
public WalletController(LoadDatabase loadDatabase, WalletRepository repository) {
this.loadDatabase = loadDatabase;
this.repository = repository;
}
@GetMapping("/addAmount")
@ResponseBody
public void addAmount(@RequestParam Long custId, @RequestParam Long amount){
try{
Wallet wallet = repository.findWalletsByCustId(custId).get(0);
wallet.balance = wallet.balance+amount;
repository.save(wallet);
}catch(IndexOutOfBoundsException e){
//handle exception
}
}
@GetMapping("/deductAmount")
@ResponseBody
public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
try{
Wallet wallet = repository.findWalletsByCustId(custId).get(0);
if (wallet.balance < amount)
return false;
wallet.balance = wallet.balance-amount;
repository.save(wallet);
return true;
}catch(IndexOutOfBoundsException e){
return false;
}
}
// some other methods.
Доступ к нему будет осуществляться одновременно, и, следовательно, я хочу сделать как addAmount, так и deductAmount атомарными по своей природе.
Чтобы проверить это, я написал сценарий оболочки, который одновременно добавляет и вычитает некоторую сумму.
wallet_test.sh
#! /bin/sh
# Get the balance of Customer 201 before.
balanceBefore=$(curl -s "http://localhost:8082/getBalance?custId=201")
echo "Balance Before:" $balanceBefore
sh wa1 & sh wa2
wait
# Get the balance of Customer 201 afterwards.
balanceAfter=$(curl -s "http://localhost:8082/getBalance?custId=201")
echo "Balance After" $balanceAfter
где wa1 и wa2 следующие:
for i in {0..10};
do
# echo "Shell 1:" $i
resp=$(curl -s "http://localhost:8082/addAmount?custId=201&amount=100")
done
for i in {0..10};
do
# echo "Shell 2:" $i
resp=$(curl -s "http://localhost:8082/deductAmount?custId=201&amount=100")
done
Результат, как и ожидалось из-за одновременного доступа, имеет вид:
Balance Before: 10000
Balance After 9900
Balance Before: 10000
Balance After 10300
Balance Before: 10000
Balance After 9600
Ожидаемый результат для меня заключается в том, что баланс до и после должен оставаться неизменным, то есть 10000.
Я прочитал, что, чтобы сделать его атомарным, мы можем использовать аннотацию @Transactional и добавить ее к обоим методам или ко всему классу. Я пробовал делать и то, и другое, но все же не получаю желаемых результатов.
Я добавил его на уровне метода, т.е.
@GetMapping("/deductAmount")
@ResponseBody
@Transactional(isolation = Isolation.SERIALIZABLE)
public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
и то же самое для deductAmount, который не сработал.
Я пробовал добавить его на уровне класса, т.е.
@Controller
@Transactional(isolation = Isolation.SERIALIZABLE)
public class WalletController {
public final LoadDatabase loadDatabase;
private final WalletRepository repository;
и это тоже не сработало.
Разве @Transactional не предназначен для использования таким образом? Должен ли я использовать какой-либо другой механизм блокировки, чтобы достичь того, что я хочу?
Обновлено:
Как уже упоминалось, я также попытался добавить пессимистические блокировки.
import static javax.persistence.LockModeType.PESSIMISTIC_WRITE;
@PersistenceContext
private EntityManager em;
@GetMapping("/addAmount")
@ResponseBody
@Transactional
synchronized public void addAmount(@RequestParam Long custId, @RequestParam Long amount){
try{
Wallet wallet = repository.findWalletsByCustId(custId).get(0);
em.lock(wallet, PESSIMISTIC_WRITE);
wallet.balance = wallet.balance+amount;
repository.save(wallet);
}catch(IndexOutOfBoundsException e){
//handle exception
}
}
@GetMapping("/deductAmount")
@ResponseBody
@Transactional
synchronized public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
try{
Wallet wallet = repository.findWalletsByCustId(custId).get(0);
em.lock(wallet, PESSIMISTIC_WRITE);
if (wallet.balance < amount)
return false;
wallet.balance = wallet.balance-amount;
repository.save(wallet);
return true;
}catch(IndexOutOfBoundsException e){
return false;
}
}
@ArnaudDenoyelle Я пробовал это делать и получаю те же результаты.




Недостаточно установить уровень изоляции. Вы должны использовать оптимистичную или пессимистичную блокировку, чтобы добиться желаемого поведения. Красивое и краткое описание этой стратегии можно найти в этом отвечать.
Я сделал это, но мои результаты все еще неверны. Я добавил свой отредактированный код в исходный вопрос.
Я думаю, вам следует изменить свой подход к блокировке, например, используя аннотацию @Lock в классе репозитория кошелька. В вашем примере кода чтение и блокировка - это отдельные операции, которые могут допускать проблемы с параллелизмом, такие как: поток T1 считывает значение кошелька 1000, и в то же время поток T2 считывает значение кошелька 1000, когда T1 блокирует и обновляет кошелек с 1000-100, когда T2 разблокирует и обновляет свою версию кошелька на 1000 + 100. Чтобы избежать этого, эта строка должна быть заблокирована во время операции выбора. На уровне базы данных это может быть достигнуто с помощью SELECT * ... FOR UPDATE
Я помню, как использовал его в методах со значением по умолчанию (я имею в виду: только
@Transactional, а не@Transactional(isolation=...)). Кроме того, я использовал его на уровне обслуживания, а не в контроллерах. но похоже, что вы вызываете репозитории прямо в контроллерах ... это все равно должно работать. Если вы хотите проверить это проще, вы можете сделать запрос, а затем выбросить исключение и посмотреть, был ли результат зафиксирован.