【问题标题】:How do you make a method atomic in Java when using Spring Boot?使用 Spring Boot 时如何在 Java 中使方法原子化?
【发布时间】:2021-04-09 12:39:34
【问题描述】:

我有一个使用 Spring Boot 编写的 REST 应用程序,其中有一个钱包。钱包有addAmountdeductAmount等方法。代码如下:

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.
 

这将被同时访问,因此,我想让addAmountdeductAmount 本质上都是原子的。

为了检查这一点,我编写了一个 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

其中wa1wa2如下:

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


【解决方案1】:

仅设置隔离级别是不够的。您应该使用乐观或悲观锁定来实现您想要的行为。可以在answer 中找到对这种策略的简短描述。

【讨论】:

  • 我这样做了,但我的结果仍然不正确。我已在原始问题中添加了我编辑的代码。
  • 我认为您应该修改您的锁定方法,例如在您的 Wallet 存储库类中使用 @Lock 注释。在您的代码示例中,读取和锁定是单独的操作,可能会出现并发问题,例如:线程 T1 读取钱包的值 1000,同时线程 T2 在 T1 锁定时读取钱包的值 1000,并使用 1000-100 更新钱包,当 T2 解锁和用 1000+100 更新它的钱包版本。为避免此行应在选择操作期间被锁定。在数据库级别可以使用 SELECT *... FOR UPDATE
猜你喜欢
  • 2020-07-10
  • 2020-09-27
  • 2020-10-08
  • 1970-01-01
  • 1970-01-01
  • 2016-08-05
  • 1970-01-01
  • 2020-08-23
  • 2019-12-16
相关资源
最近更新 更多