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;
    }
}

通过多线程并发执行测试

测试结论:
方案一:事务+同步锁(颗粒度大,逻辑复杂效率慢,只适用单机)/分布式锁(可能出现脑裂假锁的情况)
方案二:事务+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();
    }

}

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

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谦风(Java)

一起学习,一起进步(✪ω✪)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值