ToB企服应用市场:ToB评测及商务社交产业平台

标题: 处理接口幂等性的两种常见方案 [打印本页]

作者: 惊落一身雪    时间: 2022-6-26 03:54
标题: 处理接口幂等性的两种常见方案
在上周发布的 TienChin 项目视频中,我和大家一共梳理了六种幂等性解决方案,接口幂等性处理算是一个非常常见的需求了,我们在很多项目中其实都会遇到。今天我们来看看两种比较简单的实现思路。
1. 接口幂等性实现方案梳理

其实接口幂等性的实现方案还是蛮多的,我这里和小伙伴们分享两种比较常见的方案。
1.1 基于 Token

基于 Token 这种方案的实现思路很简单,整个流程分两步:
大致的思路就是上面这样,当然具体的实现则会复杂很多,有很多细节需要注意,松哥之前也专门录过这种方案的视频,小伙伴们可以参考下,录了两个视频,一个是基于拦截器处理的,还有一个是基于 AOP 切面处理的:
基于拦截器处理(视频一):
基于 AOP 切面处理(视频二):
1.2 基于请求参数校验

最近在 TienChin 项目中使用的是另外一种方案,这种方案是基于请求参数来判断的,如果在短时间内,同一个接口接收到的请求参数相同,那么就认为这是重复的请求,拒绝处理,大致上就是这么个思路。
相比于第一种方案,第二种方案相对来说省事一些,因为只有一次请求,不需要专门去服务端拿令牌。在高并发环境下这种方案优势比较明显。
所以今天我就来和大家聊聊第二种方案的实现,后面在 TienChin 项目视频中也会和大家细讲。
2. 基于请求参数的校验

首先我们新建一个 Spring Boot 项目,引入 Web 和 Redis 依赖,新建完成后,先来配置一下 Redis 的基本信息,如下:
  1. spring.redis.host=localhost
  2. spring.redis.port=6379
  3. spring.redis.password=123
复制代码
为了后续 Redis 操作方便,我们再来对 Redis 进行一个简单封装,如下:
  1. @Component
  2. public class RedisCache {
  3.     @Autowired
  4.     public RedisTemplate redisTemplate;
  5.     public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
  6.         redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
  7.     }
  8.     public <T> T getCacheObject(final String key) {
  9.         ValueOperations<String, T> operation = redisTemplate.opsForValue();
  10.         return operation.get(key);
  11.     }
  12. }
复制代码
这个比较简单,一个存数据,一个读数据。
接下来我们自定义一个注解,在需要进行幂等性处理的接口上,添加该注解即可,将来这个接口就会自动的进行幂等性处理。
  1. @Inherited
  2. @Target(ElementType.METHOD)
  3. @Retention(RetentionPolicy.RUNTIME)
  4. @Documented
  5. public @interface RepeatSubmit {
  6.     /**
  7.      * 间隔时间(ms),小于此时间视为重复提交
  8.      */
  9.     public int interval() default 5000;
  10.     /**
  11.      * 提示消息
  12.      */
  13.     public String message() default "不允许重复提交,请稍候再试";
  14. }
复制代码
这个注解我们通过拦截器来进行解析,解析代码如下:
  1. public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
  2.     @Override
  3.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4.         if (handler instanceof HandlerMethod) {
  5.             HandlerMethod handlerMethod = (HandlerMethod) handler;
  6.             Method method = handlerMethod.getMethod();
  7.             RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
  8.             if (annotation != null) {
  9.                 if (this.isRepeatSubmit(request, annotation)) {
  10.                     Map<String, Object> map = new HashMap<>();
  11.                     map.put("status", 500);
  12.                     map.put("msg", annotation.message());
  13.                     response.setContentType("application/json;charset=utf-8");
  14.                     response.getWriter().write(new ObjectMapper().writeValueAsString(map));
  15.                     return false;
  16.                 }
  17.             }
  18.             return true;
  19.         } else {
  20.             return true;
  21.         }
  22.     }
  23.     /**
  24.      * 验证是否重复提交由子类实现具体的防重复提交的规则
  25.      *
  26.      * @param request
  27.      * @return
  28.      * @throws Exception
  29.      */
  30.     public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
  31. }
复制代码
这个拦截器是一个抽象类,将接口方法拦截下来,然后找到接口上的 @RepeatSubmit 注解,调用 isRepeatSubmit 方法去判断是否是重复提交的数据,该方法在这里是一个抽象方法,我们需要再定义一个类继承自这个抽象类,在新的子类中,可以有不同的幂等性判断逻辑,这里我们就是根据 URL 地址+参数 来判断幂等性条件是否满足:
  1. @Component
  2. public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
  3.     public final String REPEAT_PARAMS = "repeatParams";
  4.     public final String REPEAT_TIME = "repeatTime";
  5.     public final static String REPEAT_SUBMIT_KEY = "REPEAT_SUBMIT_KEY";
  6.     private String header = "Authorization";
  7.     @Autowired
  8.     private RedisCache redisCache;
  9.     @SuppressWarnings("unchecked")
  10.     @Override
  11.     public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {
  12.         String nowParams = "";
  13.         if (request instanceof RepeatedlyRequestWrapper) {
  14.             RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
  15.             try {
  16.                 nowParams = repeatedlyRequest.getReader().readLine();
  17.             } catch (IOException e) {
  18.                 e.printStackTrace();
  19.             }
  20.         }
  21.         // body参数为空,获取Parameter的数据
  22.         if (StringUtils.isEmpty(nowParams)) {
  23.             try {
  24.                 nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap());
  25.             } catch (JsonProcessingException e) {
  26.                 e.printStackTrace();
  27.             }
  28.         }
  29.         Map<String, Object> nowDataMap = new HashMap<String, Object>();
  30.         nowDataMap.put(REPEAT_PARAMS, nowParams);
  31.         nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
  32.         // 请求地址(作为存放cache的key值)
  33.         String url = request.getRequestURI();
  34.         // 唯一值(没有消息头则使用请求地址)
  35.         String submitKey = request.getHeader(header);
  36.         // 唯一标识(指定key + url + 消息头)
  37.         String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + submitKey;
  38.         Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
  39.         if (sessionObj != null) {
  40.             Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
  41.             if (compareParams(nowDataMap, sessionMap) && compareTime(nowDataMap, sessionMap, annotation.interval())) {
  42.                 return true;
  43.             }
  44.         }
  45.         redisCache.setCacheObject(cacheRepeatKey, nowDataMap, annotation.interval(), TimeUnit.MILLISECONDS);
  46.         return false;
  47.     }
  48.     /**
  49.      * 判断参数是否相同
  50.      */
  51.     private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
  52.         String nowParams = (String) nowMap.get(REPEAT_PARAMS);
  53.         String preParams = (String) preMap.get(REPEAT_PARAMS);
  54.         return nowParams.equals(preParams);
  55.     }
  56.     /**
  57.      * 判断两次间隔时间
  58.      */
  59.     private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {
  60.         long time1 = (Long) nowMap.get(REPEAT_TIME);
  61.         long time2 = (Long) preMap.get(REPEAT_TIME);
  62.         if ((time1 - time2) < interval) {
  63.             return true;
  64.         }
  65.         return false;
  66.     }
  67. }
复制代码
我们来看下具体的实现逻辑:
好啦,做完这一切,最后我们再来配置一下拦截器即可:
  1. @Configuration
  2. public class WebConfig implements WebMvcConfigurer {
  3.     @Autowired
  4.     RepeatSubmitInterceptor repeatSubmitInterceptor;
  5.     @Override
  6.     public void addInterceptors(InterceptorRegistry registry) {
  7.         registry.addInterceptor(repeatSubmitInterceptor)
  8.                 .addPathPatterns("/**");
  9.     }
  10. }
复制代码
如此,我们的接口幂等性就处理好啦~在需要的时候,就可以直接在接口上使用啦:
  1. @RestController
  2. public class HelloController {
  3.     @PostMapping("/hello")
  4.     @RepeatSubmit(interval = 100000)
  5.     public String hello(@RequestBody String msg) {
  6.         System.out.println("msg = " + msg);
  7.         return "hello";
  8.     }
  9. }
复制代码
好啦,公众号后台回复 RepeatSubmit 可以下载本文源码哦。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4