不多说直接上代码,复制粘贴可用
@Aspect
@Component
@Slf4j
public class IdempotencyAspect {
@Resource
StringRedisTemplate stringRedisTemplate;
// 定义切点Pointcut
@Pointcut("@annotation(com.sunjinke.ncmis.service.aop.Idempotency)")
public void executeService() {
}
@Around("executeService()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
if (args.length <= 0) {
return ResponseModel.fail("数据错误");
}
Object arg = args[0];
MethodSignature signature = (MethodSignature) pjp.getSignature();
Idempotency annotation = signature.getMethod().getAnnotation(Idempotency.class);
String[] value = annotation.value();
Long time = annotation.expireTime();
log.info("请求参数:{}", JSON.toJSONString(arg));
String s = ReqDedupHelper.dedupParamMD5(JSON.toJSONString(arg), value);
String KEY = s;
long expireTime = time;// 1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() + expireTime;
String val = "ex@" + expireAt;
// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作
Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime),
RedisStringCommands.SetOption.SET_IF_ABSENT));
log.info("请求去重在redis中的结果={}", firstSet);
if (firstSet != null && firstSet) {
} else {
return ResponseModel.fail("请求过去频繁,稍后再试");
}
Object result = pjp.proceed();
return result;
}
}
/**
* 对参数进行MD5加密 相同参数加密出来的是相同的,利用此特性对请求并发去重
* GG
*/
@Slf4j
public class ReqDedupHelper {
/**
* @param reqJSON 请求的参数,这里通常是JSON
* @param excludeKeys 请求参数里面要去除哪些字段再求摘要
* @return 去除参数的MD5摘要
*/
public static String dedupParamMD5(final String reqJSON, String... excludeKeys) {
String decreptParam = reqJSON;
TreeMap paramTreeMap = JSON.parseObject(decreptParam, TreeMap.class);
if (excludeKeys != null) {
List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
if (!dedupExcludeKeys.isEmpty()) {
for (String dedupExcludeKey : dedupExcludeKeys) {
paramTreeMap.remove(dedupExcludeKey);
}
}
}
String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
String md5deDupParam = jdkMD5(paramTreeMapJSON);
log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
return md5deDupParam;
}
private static String jdkMD5(String src) {
String res = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] mdBytes = messageDigest.digest(src.getBytes());
res = DatatypeConverter.printHexBinary(mdBytes);
} catch (Exception e) {
log.error("", e);
}
return res;
}
}
/**
* 幂等性注解(相同参数在一定时间内不能重复请求)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotency {
/**
* 排除不需要加加密的字段
*
* @return
*/
String[] value() default {};
/**
* 指定过期时间
*
* @return
*/
long expireTime() default 2000;
}
基本原理: 创建一个注解,注解可以设置排除加密的字段和过期时间,利用aop切面编程,对参数进行MD5加密得到唯一值,放入redis中 返回true通过,否者在规定时间重复请求