处理接口幂等性的两种常见方案

在上周发布的 TienChin 项目视频中,我和大家一共梳理了六种幂等性解决方案,接口幂等性处理算是一个非常常见的需求了,我们在很多项目中其实都会遇到。今天我们来看看两种比较简单的实现思路。

1. 接口幂等性实现方案梳理

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

1.1 基于 Token

基于 Token 这种方案的实现思路很简单,整个流程分两步:

  • 客户端发送请求,从服务端获取一个 Token 令牌,每次请求获取到的都是一个全新的令牌。
  • 客户端发送请求的时候,携带上第一步的令牌,处理请求之前,先校验令牌是否存在,当请求处理成功,就把令牌删除掉。
  • 大致的思路就是上面这样,当然具体的实现则会复杂很多,有很多细节需要注意,松哥之前也专门录过这种方案的视频,小伙伴们可以参考下,录了两个视频,一个是基于拦截器处理的,还有一个是基于 AOP 切面处理的:

    基于拦截器处理(视频一):

    基于 AOP 切面处理(视频二):

    1.2 基于请求参数校验

    最近在 TienChin 项目中使用的是另外一种方案,这种方案是基于请求参数来判断的,如果在短时间内,同一个接口接收到的请求参数相同,那么就认为这是重复的请求,拒绝处理,大致上就是这么个思路。

    相比于第一种方案,第二种方案相对来说省事一些,因为只有一次请求,不需要专门去服务端拿令牌。在高并发环境下这种方案优势比较明显。

    所以今天我就来和大家聊聊第二种方案的实现,后面在 TienChin 项目视频中也会和大家细讲。

    2. 基于请求参数的校验

    首先我们新建一个 Spring Boot 项目,引入 Web 和 Redis 依赖,新建完成后,先来配置一下 Redis 的基本信息,如下:

    spring.redis.host=localhostspring.redis.port=6379spring.redis.password=123

    为了后续 Redis 操作方便,我们再来对 Redis 进行一个简单封装,如下:

    @Componentpublic class RedisCache { @Autowired public RedisTemplate redisTemplate; public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } public T getCacheObject(final String key) { ValueOperations operation = redisTemplate.opsForValue(); return operation.get(key); }}

    这个比较简单,一个存数据,一个读数据。

    接下来我们自定义一个注解,在需要进行幂等性处理的接口上,添加该注解即可,将来这个接口就会自动的进行幂等性处理。

    @Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RepeatSubmit { /** * 间隔时间(ms),小于此时间视为重复提交 */ public int interval() default 5000; /** * 提示消息 */ public String message() default “不允许重复提交,请稍候再试”;}

    这个注解我们通过拦截器来进行解析,解析代码如下:

    public abstract class RepeatSubmitInterceptor implements HandlerInterceptor { @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 annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request, annotation)) { Map map = new HashMap(); map.put(“status”, 500); map.put(“msg”, annotation.message()); response.setContentType(“application/json;charset=utf-8”); response.getWriter().write(new ObjectMapper().writeValueAsString(map)); return false; } } return true; } else { return true; } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 * * @param request * @return * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);}

    这个拦截器是一个抽象类,将接口方法拦截下来,然后找到接口上的 @RepeatSubmit 注解,调用 isRepeatSubmit 方法去判断是否是重复提交的数据,该方法在这里是一个抽象方法,我们需要再定义一个类继承自这个抽象类,在新的子类中,可以有不同的幂等性判断逻辑,这里我们就是根据 URL 地址+参数 来判断幂等性条件是否满足:

    @Componentpublic class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public final String REPEAT_PARAMS = “repeatParams”; public final String REPEAT_TIME = “repeatTime”; public final static String REPEAT_SUBMIT_KEY = “REPEAT_SUBMIT_KEY”; private String header = “Authorization”; @Autowired private RedisCache redisCache; @SuppressWarnings(“unchecked”) @Override public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) { String nowParams = “”; if (request instanceof RepeatedlyRequestWrapper) { RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; try { nowParams = repeatedlyRequest.getReader().readLine(); } catch (IOException e) { e.printStackTrace(); } } // body参数为空,获取Parameter的数据 if (StringUtils.isEmpty(nowParams)) { try { nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap()); } catch (JsonProcessingException e) { e.printStackTrace(); } } Map nowDataMap = new HashMap(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放cache的key值) String url = request.getRequestURI(); // 唯一值(没有消息头则使用请求地址) String submitKey = request.getHeader(header); // 唯一标识(指定key + url + 消息头) String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + submitKey; Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); if (sessionObj != null) { Map sessionMap = (Map) sessionObj; if (compareParams(nowDataMap, sessionMap) && compareTime(nowDataMap, sessionMap, annotation.interval())) { return true; } } redisCache.setCacheObject(cacheRepeatKey, nowDataMap, annotation.interval(), TimeUnit.MILLISECONDS); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map nowMap, Map preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map nowMap, Map preMap, int interval) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 – time2) < interval) { return true; } return false; }}

    我们来看下具体的实现逻辑:

  • 首先判断当前的请求对象是不是 RepeatedlyRequestWrapper,如果是,说明当前的请求参数是 JSON,那么就通过 IO 流将参数读取出来,这块小伙伴们要结合上篇文章共同来理解,否则可能会觉得云里雾里的,传送门JSON 数据读一次就没了,怎么办?。
  • 如果在第一步中,并没有拿到参数,那么说明参数可能并不是 JSON 格式,而是 key-value 格式,那么就以 key-value 的方式读取出来参数,并将之转为一个 JSON 字符串。
  • 接下来构造一个 Map,将前面读取到的参数和当前时间存入到 Map 中。
  • 接下来构造存到 Redis 中的数据的 key,这个 key 由固定前缀 + 请求 URL 地址 + 请求头的认证令牌组成,这块请求头的令牌还是非常重要需要有的,只有这样才能区分出来当前用户提交的数据(如果是 RESTful 风格的接口,那么为了区分,也可以将接口的请求方法作为参数拼接到 key 中)。
  • 接下来就去 Redis 中获取数据,获取到之后,分别去比较参数是否相同以及时间是否过期。
  • 如果判断都没问题,返回 true,表示这个请求重复了。
  • 否则返回说明这是用户对这个接口第一次提交数据或者是已经过了时间窗口了,那么就把参数字符串重新缓存到 Redis 中,并返回 false,表示请求没问题。
  • 好啦,做完这一切,最后我们再来配置一下拦截器即可:

    @Configurationpublic class WebConfig implements WebMvcConfigurer { @Autowired RepeatSubmitInterceptor repeatSubmitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor) .addPathPatterns(“/**”); }}

    如此,我们的接口幂等性就处理好啦 在需要的时候,就可以直接在接口上使用啦:

    @RestControllerpublic class HelloController { @PostMapping(“/hello”) @RepeatSubmit(interval = 100000) public String hello(@RequestBody String msg) { System.out.println(“msg = ” + msg); return “hello”; }}

    好啦,公众号后台回复 RepeatSubmit 可以下载本文源码哦。

    郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
    用户投稿
    上一篇 2022年6月21日 09:05
    下一篇 2022年6月21日 09:05

    相关推荐

    • 抖音只说了封禁没说时长(抖音封禁一般多长时间)

      最近是不是发现抖音监管和封号的力度大了很多,我常能看到,有同学吐槽,为什么又被封了,我也没有违规呀!! 那么今天,我就列出4大容易被封的原因,帮你避开90%的坑。学会了就点赞吧。 …

      2022年11月27日
    • 分享4条发微商朋友圈的方法(微商朋友圈应该怎么发)

      对于微商朋友来说,朋友圈的重要性不言而喻了。 那么微商的朋友圈到底该怎么发呢? 为什么同样是经营一个朋友圈,有的微商看起来逼格满满,实际效果也不错;而有的却动都不动就被屏蔽甚至拉黑…

      2022年11月27日
    • 30个无加盟费的项目(茶颜悦色奶茶店加盟费多少)

      茶颜悦色又爆了,8月18日,茶颜悦色南京门店正式开业,开张不到半小时,门店就人满为患,消费者的购买热情十分高涨,而由于人流量过大造成拥堵,茶颜悦色也不得不暂停营业。 当然,这里面排…

      2022年11月27日
    • 短视频策划内容的3个要点(短视频策划内容怎么做)

      短视频在制作时,内容框架非常重要。如果直奔主题,然后结束,聚卓告诉你,这样的短视频已经过时了。现在的短视频需要框架的,但不是任何框架,它需要一种易于理解和消化的框架。而且,现在大多…

      2022年11月27日
    • 存储过程语法(sql server存储过程语法)

      今天小编给各位分享存储过程语法的知识,其中也会对sql server存储过程语法进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧! oracle存储过程基本语法…

      2022年11月26日
    • 净利润率越高越好吗(净利润率多少合适)

      一、持续增收不增利,平均净利润率首次跌入个位数 2021年,增收不增利依旧是行业主流。具体来看,大部分企业营业收入呈增长态势,E50企业平均同比增速达到17.3%,但是利润增速则明…

      2022年11月26日
    • 个人怎么做抖音带货(个人做抖音带货能赚钱吗)

      抖音如今是大家很熟悉的短视频平台,不过现在的抖音却不只是短视频那么简单,它的功能非常丰富,其中一个就是可以带货,相信很多小伙伴都有在抖音上买过东西,抖音如今的变现能力也是不容小觑的…

      2022年11月25日
    • 全民K歌升级新版本7.0之后,有哪些隐藏功能?

      作者:高百烈来源:知乎 这个功能,旧版并没有,要升级到全新的全民K歌7.0版本才能发现。 作为朋友圈当代K歌之王,我费了不少功夫才抢到内测版本。有一说一,全民K歌的路子真的很野,新…

      2022年11月25日
    • 《宝可梦朱紫》梦特性怎么获得?隐藏特性获取方法推荐

      宝可梦朱紫里有很多宝可梦都是拥有梦特性会变强的宝可梦,很多玩家不知道梦特性怎么获得,下面就给大家带来宝可梦朱紫隐藏特性获取方法推荐,感兴趣的小伙伴一起来看看吧,希望能帮助到大家。 …

      2022年11月25日
    • 《宝可梦朱紫》奇鲁莉安怎么进化?奇鲁莉安进化方法分享

      宝可梦朱紫中的奇鲁莉安要怎么进化呢?很多玩家都不知道,下面就给大家带来宝可梦朱紫奇鲁莉安进化方法分享,感兴趣的小伙伴一起来看看吧,希望能帮助到大家。 奇鲁莉安进化方法分享 奇鲁莉安…

      2022年11月25日

    联系我们

    联系邮箱:admin#wlmqw.com
    工作时间:周一至周五,10:30-18:30,节假日休息