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

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

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

1.1 基于 Token

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

  1. 客户端发送请求,从服务端获取一个 Token 令牌,每次请求获取到的都是一个全新的令牌。
  2. 客户端发送请求的时候,携带上第一步的令牌,处理请求之前,先校验令牌是否存在,当请求处理成功,就把令牌删除掉。

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

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

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

1.2 基于请求参数校验

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

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

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

2. 基于请求参数的校验

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

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

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

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

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

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

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

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

public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {@Overridepublic 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<String, Object> 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 地址+参数 来判断幂等性条件是否满足:

@Component
public 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";@Autowiredprivate RedisCache redisCache;@SuppressWarnings("unchecked")@Overridepublic 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<String, Object> nowDataMap = new HashMap<String, Object>();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<String, Object> sessionMap = (Map<String, Object>) 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<String, Object> nowMap, Map<String, Object> preMap) {String nowParams = (String) nowMap.get(REPEAT_PARAMS);String preParams = (String) preMap.get(REPEAT_PARAMS);return nowParams.equals(preParams);}/*** 判断两次间隔时间*/private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> 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;}
}

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

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

好啦,做完这一切,最后我们再来配置一下拦截器即可:

@Configuration
public class WebConfig implements WebMvcConfigurer {@AutowiredRepeatSubmitInterceptor repeatSubmitInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");}
}

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

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

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

处理接口幂等性的两种常见方案相关推荐

  1. Redis 处理接口幂等性的两种方案

    前言:接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题.对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的 ...

  2. 「Spring Boot」接口幂等性的4种实现方案

    一.什么是幂等性 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同. 在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执 ...

  3. 两种常见的内存管理方法:堆和内存池

    在程序运行过程中,可能产生一些数据,例如,串口接收的数据,ADC采集的数据.若需将数据存储在内存中,以便进一步运算.处理,则应为其分配合适的内存空间,数据处理完毕后,再释放相应的内存空间.为了便于内存 ...

  4. android内存池,两种常见的内存管理方法:堆和内存池

    描述 本文导读 在程序运行过程中,可能产生一些数据,例如,串口接收的数据,ADC采集的数据.若需将数据存储在内存中,以便进一步运算.处理,则应为其分配合适的内存空间,数据处理完毕后,再释放相应的内存空 ...

  5. 移动Web开发图片自适应两种常见情况解决方案

    本文主要说的是Web中图片根据手机屏幕大小自适应居中显示,图片自适应两种常见情况解决方案.开始吧 在做配合手机客户端的Web wap页面时,发现文章对图片显示的需求有两种特别重要的情况,一是对于图集, ...

  6. 一文带你掌握OBS的两种常见的鉴权方式

    OBS提供了REST(Representational State Transfer)风格API,支持您通过HTTP/HTTPS请求调用.在调用OBS的API前,需要了解OBS的鉴权认证方式.本文就将 ...

  7. CSS初始化的两种常见方法

    CSS初始化的两种常见方法 两种CSS初始化的常见方案,都是通用类的 CSS reset Eric Meyer写的 CSS reset,源码不是很长: /* http://meyerweb.com/e ...

  8. 使用定制的NSDictionary的方法,对NSArray进行排序(附:数组排序两种常见方法)

    NSArray中存放的是NSDictionary,可以使用策略的方法对NSDictionary进行定制,增加比较的方法.然后调用NSArray的sortUsingSelector方法对数组进行排序,这 ...

  9. 两种常见的周期性特征,时序必知强特

    来源:kaggle竞赛宝典 在时间序列问题中,周期特征是异常重要的,例如: 地铁流量预测中的周期性,每周一到周五的早上地铁流量就特别大,但是到了周末人就比较少: 股票涨跌的预测问题中,在节假日之前,例 ...

最新文章

  1. Spark的安装和使用
  2. blob html 预览_Blob | HTML 5 API | JavaScript 权威指南
  3. python——文件和数据格式化
  4. 重庆大学微型计算机基础实验,计控课程方案设计书.doc
  5. Oracle中的AWR,全称为Automatic Workload Repository
  6. 培训ui设计要学编程吗?
  7. 修改user-agent爬取数据 遇到的问题
  8. 串口波特率自适应算法(仿真通过)
  9. 计算机26字母代码表,电脑打字学习:26个汉语拼音字母攻略
  10. Windows10的右键菜单添加“管理员取得所有权”
  11. 图片情感研究现状与思考
  12. datadog的数据流转
  13. IDEA设置背景图片
  14. win10计算机拒绝访问,Win10文件访问被拒绝如何解决?
  15. 使用POI读取大量数据EXCEL文件,并解析成自定义javaBean
  16. ubuntu 挂载文件后,文件权限属于root,只能读不能写解决办法
  17. 【转载】透视“专利恶霸”系列之二 蜕变后的苹果有了新玩法
  18. 阿里云搭建wordpress生产级CMS网站实践
  19. 龙星镖局 | 2015 机器学习颁奖礼
  20. 2021-10-08 2021年中式烹调师(初级)新版试题及中式烹调师(初级)免费试题

热门文章

  1. WAV 文件格式分析
  2. MVC 和Spring MVC
  3. 安卓接入微信php处理,PHP对接微信公众号实现简单自动回复
  4. JavaScript高级编程设计(第三版)——第二章:在html中使用javaScript
  5. mysql cast()与convert() 函数
  6. 如何破解超级用户密码?
  7. 【单镜头反光相机】影调、反差、光比、宽容度;光质(硬光、软光)、硬调、软调、高调、低调、中间调...
  8. Centos7、Redhat7 重置root密码
  9. springboot2整合redis使用lettuce连接池(解决lettuce连接池无效问题)
  10. php yii2.0框架下载,yii2.0下载|yii2.0(php框架) v2.0.10官方版 附安装教程 - 121下载站...