最近开发微服务商城用户账户余额扣减发生的问题,比如说一个用户的账户同时扣减买东西的时候在高并发情况下会出现余额少扣的情况。
其实解决方案可以使用悲观锁去只让一个线程去实现,但是我想做并发量并不是很大感觉极限情况下最多也就10qps 悲观锁阻塞线程其实有点浪费性能,所以采用了乐观锁在并发量不高的情况下即保证余额的安全性又可以保证用户大概率情况下可以实现扣减操作
这部分代码就是serice层方面具体业务逻辑就是先查询用户余额有足够的余额扣减,在进行扣减
乐观锁用数据库实现很简单 cas 就是记录一个版本号在每次修改的时候对比一下版本号是否和之前查询的一致,如果不一致可以抛出异常,这里可以重试等下在解释。
因为持久层框架使用的JPA,第一次使用JPA之前一直再用mybatis,很多东西都是现查的
起初sql语句实我自己编写的,sql语句本身应该是没有问题的,但是我在sql中修改了version导致一直无法修改提示找不到version字段,但是查询的时候是能查到的,令我很不解,在无数次修改sql以后我放弃了,于是我上网开始查找原因,原因其实没有查到,感觉关于JPA方面的博客很少,我感觉JPA很好用的不用自己写sql大部分sql都能替你实现,最终查到了一个注解@version,我豁然开朗,原来已经提供了乐观锁的实现方式。
使用了@version注解以后果然一切都豁然开朗。
我兴致勃勃开启了10个线程准备大展身手,结果又出现了一个问题。
问题是用户信息我是存放在threadlocal中,为了线程隔离,我在test类中使用了before和after注解用来清理和创建用户信息。
但是我开启的线程获取不到用户信息了,我很疑惑,后来思考了一下 md我这样写是test这个线程存储了 我开启的线程没有向其中存储啊,于是我又改了一下
这下好了 我开是信心满满去测试,结果发现10个线程只有几个成功其他的会直接抛出异常。虽然和预期的不一样但是至少扣减的钱数是没有多扣或者少扣。
想一下怎么能够让他重试呢
第一反应是在service中加一个死循环,当不抛出异常的时候就跳出循环结束方法调用。
这个方法我实验了一下,最后还是有问题而且是一点也不美观。
我又开始查询文档想看看大佬们有没有什么解决方案。
最后找到的解决方案是使用aop!
在这之前我一直以为aop的主要作用就是加一下日志打印信息,实在是我学识短浅了
解决方案就是使用@Around,然后把我的那while(true)添加里面。good good 这样一看就高大上了起来
动手实现
定义一个注解这样可以写在方法上
@Slf4j @Aspect @Component public class RetryAspect implements Ordered { //重试次数 public static final int MAX_TRY_COUNT = 3; //设定优先级 @Override public int getOrder() { return 1; } @Pointcut("@annotation(com.imooc.ecommerce.annotation.IsTryOnFailure)") public void retryOnFailure() { } @Around("retryOnFailure()") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int retryCount = 0; do { retryCount++; try { pjp.proceed(); } catch (Exception e) { if (e instanceof ObjectOptimisticLockingFailureException || e instanceof StaleObjectStateException) { if (retryCount == MAX_TRY_COUNT) { throw new RuntimeException("重试次数已经达到"); }else{ log.info("重试次数"+retryCount); continue; } } } break; } while (retryCount < MAX_TRY_COUNT); return null; } }
@Around就是将这个方法包围起来,这里其实代理模式看的很明显了,我理解为ProceedingJoinPoint就是类似于代理可以在对这个方法进行一些其他操作。
测试一下
控制台也只有两个线程没有修改成功,这种情况只要重试次数增加,成功率也会增加