【发布时间】:2021-04-09 12:39:34
【问题描述】:
我有一个使用 Spring Boot 编写的 REST 应用程序,其中有一个钱包。钱包有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 本质上都是原子的。
为了检查这一点,我编写了一个 shell 脚本,它可以同时增加和减少一些金额。
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;
}
}
【问题讨论】:
-
我记得在方法上使用它,默认值,(我的意思是:仅限
@Transactional,而不是@Transactional(isolation=...))。另外,我在服务层中使用了它,而不是在控制器中。但它接缝你直接在控制器中调用存储库......无论如何都应该工作。如果您想更轻松地测试它,可以发出请求然后抛出异常并查看结果是否已提交。 -
@ArnaudDenoyelle 我试过这样做,我得到了相同的结果。
标签: java spring spring-boot transactions