赞
踩
最近面试问到了银行转账的高并发问题,回答的不是很理想,小编整理了下,题目大概如下:
有一张银行账号表(银行账号字段、金额字段),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(); } }
小编比较菜…欢迎评论区讨论更好的方法❀❀❀
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。