通过拦截器 + 自定义注解 + redis,解决接口幂等性
一、为什么会出现接口幂等性?
接口幂等性是指一个接口在多次请求的情况下,其执行结果始终保持一致,不会因多次请求而产生不同的结果。
解决接口幂等性问题的方法主要有以下几种:
1.使用Token或令牌:每次请求时,服务器生成一个唯一的Token或令牌,然后将该Token或令牌返回给客户端。客户端在后续的请求中,需要携带该Token或令牌。服务器在接收到请求后,会检查请求中的Token或令牌是否有效。如果无效,则拒绝请求;如果有效,则继续处理请求。这样可以保证每个请求都是唯一的,从而实现接口的幂等性。
2.使用锁:在处理请求时,可以使用锁来保证同一时间只有一个请求在运行。其他请求在检测到锁存在时会等待,直到锁释放后再进行处理。这样可以保证每个请求都只会被执行一次,从而实现接口的幂等性。
3.使用消息队列:在处理请求时,可以将请求放入消息队列中。然后,服务器会从消息队列中取出请求并处理。由于消息队列的特性,可以保证每个请求都只会被处理一次,从而实现接口的幂等性。
4.使用版本号 :在处理请求时,可以为每个请求分配一个版本号。然后在处理请求时,会检查请求的版本号是否有效。如果无效,则拒绝请求;如果有效,则继续处理请求。这样可以保证每个请求都只会被执行一次,从而实现接口的幂等性。
5.使用缓存:在处理请求时,可以将请求的结果缓存起来。然后在处理请求时,会检查缓存中是否有对应的结果。如果有,则直接返回缓存中的结果;如果没有,则继续处理请求并将结果缓存起来。这样可以保证每个请求都只会被处理一次,从而实现接口的幂等性。
二、这里使用 通过拦截器+自定义注解+Redis,解决接口幂等性
在需要处理的请求上打上自定义注解,拦截器获取该请求的接口是否存在自定义注解,根据缓存的接口和请求数据进行拦截(也可以根据注解自己写一个切面类,自定义实现,原理类似,只是获取请求参数方法差异)
2.1 创建自定义注解
代码如下(示例):
/**
* 自定义注解防止重复请求
*
* @author liu
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 默认指定100ms
*/
int intervalTime() default 100;
/**
* 时间单位 默认毫秒
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
2.2 编写拦截器
代码如下(示例):
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//获取该方法是否带RepeatSubmit 注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit .class);
if (annotation != null) {
if (this.isRepeatSubmit(annotation, request)) {
Result result = Result.error("不允许重复提交,请稍后再试");
ServletUtils.renderString(response, JSONUtil.toJsonStr(result));
return false;
}
}
return true;
} else {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
/**
* 验证是否重复提交
*/
public boolean isRepeatSubmit(RepeatSubmit annotation, HttpServletRequest request) throws IOException {
String nowParams = "";
try {
//从自定义的可重复读取HttpServletRequest获取输入流
//使用了hutool的IoUtil、SecureUtil、JSONUtil
nowParams = IoUtil.readUtf8(request.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
if(StrUtil.isEmpty(nowParams)){
nowParams = JSONUtil.toJsonStr(request.getParameterMap());
}
String submitKey = SecureUtil.md5(nowParams);
// 请求地址
String url = request.getRequestURI();
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + submitKey;
//工具类封RedisUtils装了RedissonClient里的getBucket 和setBucket
String key = RedisUtils.getCacheObject(cacheRepeatKey);
//使用Redisson 也可以使用 redisTemplate.opsForValue();步骤类似
RBucket<Object> bucket = redissonClient.getBucket(cacheRepeatKey);
if (bucket.get() == null) {
if (bucket.setIfAbsent(cacheRepeatKey)){
bucket.expire(annotation.intervalTime(),annotation.timeUnit());
return false;
}
}
return true;
}
}
因为设置了过期时间,无需手动释放
3. 实现可重复读取HttpServletRequest输入流
参考该链接进行实现:https://blog.csdn.net/LSL1618/article/details/112907142
注意:该拦截器顺序应该在接口拦截器之前执行
4.配置Redis
配置Redisson,参考链接:https://blog.csdn.net/feiying0canglang/article/details/126524833
配置redis,参考链接: https://blog.csdn.net/penanut/article/details/115913922
压力测试
使用ab命令执行压力测试 模拟用户请求,可以看到只有一次输出