当前位置:   article > 正文

Java高并发场景(银行转账问题)_java如何保证多比转账成功

java如何保证多比转账成功

最近面试问到了银行转账的高并发问题,回答的不是很理想,小编整理了下,题目大概如下:
有一张银行账号表(银行账号字段、金额字段),A账号要给B账号转账,A扣款,B收款,在多线程高并发情况下,A账户的金额不能小于0,问如何设计架构比较合理?
我一开始脑抽地回答了两个方案:
方案一:事务+同步锁/分布式锁(更新sql控制扣款update的账户金额要大于扣款金额)
方案二:将数据库缓存于redis,通过lua语句去执行查询判断扣款和收款,然后保证异步通知数据库更新

先来看第一个方案哈,同步锁在单节点的情况下确实可以解决问题,但是首先颗粒度大(不管哪个转账都得排队),且复杂度高的情况下就效率慢,其次若是多节点集群的情况下,同步锁就不适用,那我们看分布式锁(redis),分布式锁确实可以降低颗粒度,可以控制到A账户作为key锁,但是面试官提到了一个概念,redis脑裂(可能产生数据丢失),因此可能出现假锁的情况,因为面试的这家公司是做数字银行的,对于风险把控很严格,因此对于这类情况风险他们对这个方案也pass掉,不过我后面补充的这个扣款时sql需要增加当前账户金额需要大于扣款金额才能扣款,这个其实是可行的,这个后续代码会演示。

再来看第二个方案哈,这个缓存于redis的方案,其实我当时为什么会这么直接想到这个方案呢,首先是因为redis的单机命令操作,以及lua能保证多语句的执行,若账户金额不够扣款则不会进行转账,但是其实金额数据一般是不会缓存在redis中的,有一定的风险性且增加了系统复杂度,若数据库异常或其他情况导致的缓存数据不一致,金额这方面无法保证。

面试官还提到关于数据库隔离级别能否解决问题,其实我验证后关于可串行化其实也是只是对当前sql语句执行进行加锁,开启事务时可串行化也并非是对事务进行加锁,依然可能出现金额问题。(查询金额存在多个一样的情况)(但是其实可串行化在我之前了解的资料里理论上应该是可行的,有强制事务串行化,即按顺序提交

代码验证

锁机制在一定程度上可以,sql条件扣款时控制也是可以

@Service
@Slf4j
public class OperateAccountImpl implements OperateAccount {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    /**
     * 处理账户转账方案总结:
     * 方案一:事务+同步锁(颗粒度大,逻辑复杂效率慢,只适用单机)/分布式锁(可能出现脑裂假锁的情况)
     * 方案二:事务+sql条件控制(账户金额需大于等于扣款金额,但是查询时可能出现一次可扣款数据)
     */
    @Override
//    @Transactional(rollbackFor= Exception.class)
    public String transfer(String accountFrom, String accountTo, double amount) {
        // 设置隔离级别为串行化(这里测试出来会死锁,按理解串行化应该是事务串行化,不应该抢锁才对)
      //transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);

        //线程标记
        String threadName = "【"+Thread.currentThread().getName()+"】";

        String msg = "";

        List<String> msgList = new ArrayList<>();

        //同步锁也是实现方法之一
//        synchronized(this){
//
//        }

        try{
            //开启Spring事务
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    try {
                        QueryWrapper<AccountDto> queryWrapper =new QueryWrapper<>();
                        queryWrapper.select("account","money").eq("account",accountFrom);
                        // 查询金额是否够扣
                        AccountDto accountDto = accountMapper.selectOne(queryWrapper);
//                        AccountDto accountDto = accountMapper.selectByForUpdate(accountFrom);
                        if(null == accountDto){
                            throw new Exception("账户"+accountFrom+"不存在");
                        }
                        log.info("{} 查询到扣款账户{} 余额:{}",threadName,accountFrom,accountDto.getMoney());
                        if (accountDto.getMoney() < amount) {
                            throw new Exception("余额不足,账户"+accountFrom+"只剩:"+accountDto.getMoney());
                        }

                        // 扣款
                        int count1 = accountMapper.update(null,transferUpdate(-1 * amount,accountFrom));
                        if (count1 == 0) {
                            throw new Exception("账户"+accountFrom+"扣款失败,检查余额");
                        }

                        // 收款
                        int count2 = accountMapper.update(null,transferUpdate(amount,accountTo));
                        if (count2 == 0) {
                            throw new Exception("账户"+accountTo+"收款失败");
                        }

                        log.info(threadName+"转账成功");
                        msgList.add(threadName+"转账成功");

                    }catch (Exception e){
                        log.info(threadName+e.getMessage());
                        msgList.add(threadName+e.getMessage());
                        // 回滚
                        status.setRollbackOnly();
                    }
                }
            });
        }catch (Exception e){
//            msgList.add(e.getMessage());
        }

//        log.info(threadName+"结束:"+msgList.toString());

        return msgList.get(0);
    }

    // 更新sql操作
    public UpdateWrapper<AccountDto> transferUpdate(double updateMoney,String account){
        UpdateWrapper<AccountDto> updateWrapper = Wrappers.update();
        // 修改表中money字段为指定的数据
//        updateWrapper.set("money", updateMoney);
        updateWrapper.setSql("money = money + "+ updateMoney);
        if (updateMoney<0){
            // 修改条件为account=?且大于等于扣款金额的数据
            updateWrapper.eq("account", account).and(wq ->wq.ge("money",-1 * updateMoney));
        }else{
            // 修改条件为account=?的数据
            updateWrapper.eq("account", account);
        }

//        //若使用事务隔离级别为最高级,测试出来的结果加锁的是sql并不是事务,因此查询值依然没有顺序之分,分开sql依旧会出现问题(实际上不会扣款扣多,但是会死锁,因为会抢占sql的锁)
//        updateWrapper.eq("account", account);

        return updateWrapper;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

通过多线程并发执行测试

测试结论:
方案一:事务+同步锁(颗粒度大,逻辑复杂效率慢,只适用单机)/分布式锁(可能出现脑裂假锁的情况)
方案二:事务+sql条件控制(账户金额需大于等于扣款金额,但是查询时可能出现一次可扣款数据)

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private OperateAccount operateAccount;

    //设置固定线程池
    ExecutorService executorService = Executors.newFixedThreadPool(10);

    @RequestMapping("/transfer")
    public String transfer(HttpServletRequest request){

        //创建同步计数器(10个一起跑)
        CountDownLatch countDownLatch = new CountDownLatch(10);

        //用于堵塞线程等待全部结果
        CountDownLatch countDownLatch1 = new CountDownLatch(10);

        //扣款账户
        String accountFrom = request.getParameter("accountFrom");
        //收款账户
        String accountTo = request.getParameter("accountTo");
        //转账金额
        double amount = Double.parseDouble(request.getParameter("amount"));

        List<String> msgList = new ArrayList<>();

        try {
            // 模拟转账操作
            for (int i = 0; i < 10; i++) {
                executorService.submit(() -> {
                    try {
                        countDownLatch.await();//统一等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String msg = operateAccount.transfer(accountFrom, accountTo, amount);
                    msgList.add(msg);
                    countDownLatch1.countDown();
                });
                //处理完同步计数器线程数减一,待计数器为0统一执行所有转账操作
                countDownLatch.countDown();
            }
            countDownLatch1.await();
            executorService.shutdown();
        }catch (Exception e){
            e.printStackTrace();
        }

//        log.info("转账结果:{}",msgList.toString());
        return msgList.toString();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

小编比较菜…欢迎评论区讨论更好的方法❀❀❀

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/574605
推荐阅读
相关标签
  

闽ICP备14008679号