适用场景
一个用户操作同一个接口请求参数一致的情况下只允许处理一次
使用方式
在需要做幂等的接口上添加@AccessLimit注解
测试方式
通过 jmeter 开一个线程组,多个线程同时调用同一个接口,分析结果是否处理了多次
源码分析
@AccessLimit 中现有的字段属性:
seconds 锁多少秒
retryCount 最大重试次数
needLogin 是否需要账号登陆
核心代码
@Around("@annotation(com.youshang.annotation.AccessLimit)")
public Object preHandle(ProceedingJoinPoint joinPoint) throws Throwable {
try {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String url = request.getRequestURL().toString();
//获取当前注解信息
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
//获取注解自定义的参数信息
int seconds = accessLimit.seconds();
int concurrencyCount = accessLimit.retryCount();
boolean login = accessLimit.needLogin();
if (seconds <= 0) {
throw new RuntimeException("加载时间不允许小于0");
}
//获取请求参数 并制作hashCode
String queryString = "";
if (null != request.getQueryString()) {
queryString = request.getQueryString();
}
Object[] args = joinPoint.getArgs();
String jsonStr = "";
if (args != null && args.length > 0) {
jsonStr = JSONObject.toJSONString(args);
}
int requestHashCode = jsonStr.concat(queryString).hashCode();
//获取用户ID
String userId = "";
//key = 请求路径 + 操作用户ID + 请求参数的转String的hashCode值
//key(String) 调用 intern 之后当前锁才会生效
String key = (url + userId + requestHashCode).intern();
synchronized (key) {
Object value = redisTemplate.opsForValue().get(key);
log.info("校验接口幂等:当前用户id信息:{},当前接口信息:{},当前key:{},当前redis-value:{}", userId, url, key, value);
if (null == value) {
//当用户第一次访问时,存储当前用户信息+请求路径
redisTemplate.opsForValue().set(key, 0, seconds, TimeUnit.SECONDS);
} else {
int accessCount = (int) value;
if (concurrencyCount > accessCount) {
log.info("当前幂等接口允许并发操作:key:{},最大并发量:{},目前并发量:{}", key, concurrencyCount, accessCount);
redisTemplate.opsForValue().increment(key, 1);
} else {
Long expire = redisTemplate.opsForValue().getOperations().getExpire(key);
throw new RuntimeException("当前操作正在进行中,请稍等 " + expire + " 秒后再试");
}
}
}
}catch (RuntimeException e){
throw new RuntimeException(e.getMessage());
}catch (Exception e){
log.error("接口幂等拦截失败,异常信息:{}", Throwables.getStackTraceAsString(e));
}
return joinPoint.proceed(joinPoint.getArgs());
}
源码地址
https://gitee.com/liaojuhui/spring-learning.git
项目:spring-boot-idempotent