I have a REST application written using Spring Boot, in which I have a wallet. The wallet has methods like addAmount
, deductAmount
and so on. Here's the code:
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.
This is going to be concurrently accessed and hence, I want to make both addAmount
and deductAmount
atomic in nature.
To check this, I wrote a shell script which adds and deducts some amount concurrently.
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
where wa1
and wa2
are as follows:
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
The output, as expected due to concurrent access, is something of the form:
Balance Before: 10000
Balance After 9900
Balance Before: 10000
Balance After 10300
Balance Before: 10000
Balance After 9600
The expected output for me is that the balance before and after should remain the same i.e 10000.
Now, I've read that to make it atomic, we can make use of the @Transactional
annotation and that we can add it to both the methods, or to the entire class. I tried doing both, and yet, I am not getting the results I desire.
I added it at the method level i.e
@GetMapping("/deductAmount")
@ResponseBody
@Transactional(isolation = Isolation.SERIALIZABLE)
public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
and same for deductAmount, which didn't work.
I tried adding it at the class level i.e
@Controller
@Transactional(isolation = Isolation.SERIALIZABLE)
public class WalletController {
public final LoadDatabase loadDatabase;
private final WalletRepository repository;
and this didn't work either.
Is @Transactional
not meant to be used this way? Should I use some other locking mechanism in order to accomplish what I want?
EDIT:
As mentioned, I tried adding Pessimistic locks as well.
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;
}
}