最早做团购的网站,h5页面设计是什么,网店运营管理与营销推广,万网网站备案流程最近面试问到了银行转账的高并发问题#xff0c;回答的不是很理想#xff0c;小编整理了下#xff0c;题目大概如下#xff1a; 有一张银行账号表#xff08;银行账号字段、金额字段#xff09;#xff0c;A账号要给B账号转账#xff0c;A扣款#xff0c;B收款#x… 最近面试问到了银行转账的高并发问题回答的不是很理想小编整理了下题目大概如下 有一张银行账号表银行账号字段、金额字段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 {Autowiredprivate AccountMapper accountMapper;Autowiredprivate 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 ;ListString msgList new ArrayList();//同步锁也是实现方法之一
// synchronized(this){
//
// }try{//开启Spring事务transactionTemplate.execute(new TransactionCallbackWithoutResult() {protected void doInTransactionWithoutResult(TransactionStatus status) {try {QueryWrapperAccountDto 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(threadNamee.getMessage());msgList.add(threadNamee.getMessage());// 回滚status.setRollbackOnly();}}});}catch (Exception e){
// msgList.add(e.getMessage());}// log.info(threadName结束msgList.toString());return msgList.get(0);}// 更新sql操作public UpdateWrapperAccountDto transferUpdate(double updateMoney,String account){UpdateWrapperAccountDto updateWrapper Wrappers.update();// 修改表中money字段为指定的数据
// updateWrapper.set(money, updateMoney);updateWrapper.setSql(money money updateMoney);if (updateMoney0){// 修改条件为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 {Autowiredprivate 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));ListString 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();}}小编比较菜…欢迎评论区讨论更好的方法❀❀❀