1024技术干货 ~ Java如何防止接口重复提交
正如本文标题所言,今天我们来聊一聊在Java应用系统中如何防止接口重复提交;简单地讲,这其实就是“重复提交”的话题,本文将从以下几个部分展开介绍:
1.“重复提交”简介与造成的后果
2.“防止接口重复提交”的实现思路
3.“防止接口重复提交”的代码实战
一、“重复提交”简介与造成的后果
对于“重复提交”,想必各位小伙伴都知晓它的意思,简单的理解,它指的是前端用户在间隔很短的时间周期内对同一个请求URL发起请求,导致前端开发者在很短的时间周期内将同一份数据(请求体)提交到后端相同的接口 多次,最终数据库出现多条主键ID不一样而其他业务数据几乎一毛一样的记录;
仔细研究上述整个过程,会发现如果发起的多次请求的时间间隔足够短,即时间趋向于无穷小 时,其过程可以归为“多线程并发导致并发安全”的问题范畴;而对于“并发安全”的话题,debug早在此前自己录制的课程以及之前的文章中介绍过多次了,在此不再赘述;
上述在对“重复提交”的介绍中隐约也提及它所带来的的后果:
(1)数据库DB出现多条一毛一样的数据记录;
(2)如果重复发起的请求足够多、请求体容量足够大,很可能会给系统接口带来极大的压力,导致其出现“接口不稳定”、“DB负载过高”,严重点甚至可能会出现“系统宕机”的情况;
因此,我们需要在一些很可能会出现“重复提交”的后端接口中加入一些处理机制(附注:前端其实也需要配合一同处理的,其处理方式在本文就不做介绍了~);
二、“防止接口重复提交”的实现思路
对于“重复提交”,想必各位小伙伴都知晓它的意思,简单的理解,它指的是前端用户在间隔很短的时间周期内对同一个请求URL发起请求,导致前端开发者在很短的时间周期内将同一份数据(请求体)提交到后端相同的接口 多次,最终数据库出现多条主键ID不一样而其他业务数据几乎一毛一样的记录;
仔细研究上述整个过程,会发现如果发起的多次请求的时间间隔足够短,即时间趋向于无穷小 时,其过程可以归为“多线程并发导致并发安全”的问题范畴;而对于“并发安全”的话题,debug早在此前自己录制的课程以及之前的文章中介绍过多次了,在此不再赘述;
上述在对“重复提交”的介绍中隐约也提及它所带来的的后果:
(1)数据库DB出现多条一毛一样的数据记录;
(2)如果重复发起的请求足够多、请求体容量足够大,很可能会给系统接口带来极大的压力,导致其出现“接口不稳定”、“DB负载过高”,严重点甚至可能会出现“系统宕机”的情况;
因此,我们需要在一些很可能会出现“重复提交”的后端接口中加入一些处理机制(附注:前端其实也需要配合一同处理的,其处理方式在本文就不做介绍了~);
值得一提的是,绝大部分情况下,只有POST/PUT/DELETE的请求方式才会出现“重复提交”的情况,而对于GET请求方式,只要不是出现人为的意外情况,那么它就具有“幂等性”,谈不上“重复提交”现象的出现,因此,在实际项目中,出现“重复提交”现象比较多的一般是POST请求方式;
而在实际项目开发中,“防止接口重复提交”的实现方式有两类,一类是纯粹的针对请求链接URL的,即防止对同一个URL发起多次请求:此种方式明显粒度过大,容易误伤友军;另一类是针对请求链接URL + 请求体 的,这种方式可以说是比较人性化而且也是比较合理的,而我们在后面要介绍的实现方式正是基于此进行实战的;
为了便于小伙伴理解,接下来我们以“用户在前端提交注册信息”为例,介绍“如何防止接口重复提交”的实现思路,如下图所示为整体的实现思路:
从该图中可以得知,如果当前提交的请求URL已经存在于缓存中,且 当前提交的请求体 跟 缓存中该URL对应的请求体一毛一样 且 当前请求URL的时间戳跟上次相同请求URL的时间戳 间隔在8s 内,即代表当前请求属于 “重复提交”;如果这其中有一个条件不成立,则意味着当前请求很有可能是第一次请求,或者已经过了8s时间间隔的 第N次请求了,不属于“重复提交”了。
三、“防止接口重复提交”的实现思路
照着这个思路,接下来我们将采用实际的代码进行实战,其中涉及到的技术:Spring Boot2.0 + 自定义注解 + 拦截器 + 本地缓存(也可以分布式缓存);
(1)首先,需要自定义一个用于加在需要“防止重复提交”的请求方法上 的注解RepeatSubmit,该注解的定义代码很简单,就是一个常规的注解定义,如下代码所示:
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
}
之后,是直接创建一个新的控制器SubmitController,并在其中创建一请求方法,用于处理前端用户提交的注册信息 请求,如下代码所示:
@RestController
@RequestMapping("submit")
public class SubmitController extends BaseController{
//用户注册
@RepeatSubmit
@PostMapping("register")
public BaseResponse register(@RequestBody RegisterDto dto) throws Exception{
BaseResponse response=new BaseResponse(StatusCode.Success);
//log.info("用户注册,提交上来的请求信息为:{}",dto);
//将用户信息插入到db
response.setData(dto);
return response;
}
}
其中,RegisterDto 为自定义的实体类,代码定义如下所示:
@Data
public class RegisterDto implements Serializable{
private String userName;
private String nickName;
private Integer age;
}
(2)将注解加上去之后,接下来需要自定义一个拦截器RepeatSubmitInterceptor,用于拦截并获取 加了上述这个注解的所有请求方法的相关信息,包括其请求URL和请求体数据,其核心代码如下所示:
@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter{
//开始拦截
@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 submitAnnotation=method.getAnnotation(RepeatSubmit.class);
if (submitAnnotation!=null){
//如果是重复提交,则进行拦截,拒绝请求
if (this.isRepeatSubmit(request)){
BaseResponse subResponse=new BaseResponse(StatusCode.CanNotRepeatSubmit);
CommonUtil.renderString(response,new Gson().toJson(subResponse));
return false;
}
}
return true;
}else{
return super.preHandle(request, response, handler);
}
}
//自定义方法逻辑-判定是否重复提交
public abstract boolean isRepeatSubmit(HttpServletRequest request);
}
在这里我们将其定义为抽象类,并自定义一个抽象方法:“判断当前请求是否为重复提交isRepeatSubmit()”,之所以这样做,是因为“判断是否重复提交”可以有多种实现方式,而每种实现方式可以通过继承该抽象类 并 实现该抽象方法 从而将其区分开来,某种程度降低了耦合性(面向接口/抽象类编程);如下代码所示为该抽象类的其中一种实现方式:
/**
* 判断是否重复提交,整体的思路:
* 获取当前请求的URL作为键Key,暂且标记为:A1,其取值为映射Map(Map里面的元素由:请求的链接url 和 请求体的数据组成) 暂且标记为V1;
* 从缓存中(本地缓存或者分布式缓存)查找Key=A1的值V2,如果V2和V1的值一样,即代表当前请求是重复提交的,拒绝执行后续的请求,否则可以继续往后面执行
* 其中,设定重复提交的请求的间隔有效时间为8秒
*
* 注意点:如果在有效时间内,如8秒内,一直发起同个请求url、同个请求体,那么重复提交的有效时间将会自动延长
* @author 修罗debug
* @date 2020/10/21 8:12
* @link 微信:debug0868 QQ:1948831260
* @blog fightjava.com
*/
@Component
public class SameUrlDataRepeatInterceptor extends RepeatSubmitInterceptor{
private static final String REPEAT_PARAMS = "RepeatParams";
private static final String REPEAT_TIME = "RepeatTime";
//防重提交key
public static final String REPEAT_SUBMIT_KEY = "Repeat_Submit:";
private static final int IntervalTime = 8;
//构建本地缓存,有效时间为8秒钟
private final Cache<String,String> cache= CacheBuilder.newBuilder().expireAfterWrite(IntervalTime, TimeUnit.SECONDS).build();
//真正实现“是否重复提交的逻辑”
@Override
public boolean isRepeatSubmit(HttpServletRequest request) {
String currParams=HttpHelper.getBodyString(request);
if (StringUtils.isBlank(currParams)){
currParams=new Gson().toJson(request.getParameterMap());
}
//获取请求地址,充当A1
String url=request.getRequestURI();
//充当B1
RepeatSubmitCacheDto currCacheData=new RepeatSubmitCacheDto(currParams,System.currentTimeMillis(),url);
//充当键A1
String cacheRepeatKey=REPEAT_SUBMIT_KEY+url;
String cacheValue=cache.getIfPresent(cacheRepeatKey);
//从缓存中查找A1对应的值,如果存在,说明当前请求不是第一次了.
if (StringUtils.isNotBlank(cacheValue)){
//充当B2
RepeatSubmitCacheDto preCacheData=new Gson().fromJson(cacheValue,RepeatSubmitCacheDto.class);
if (this.compareParams(currCacheData,preCacheData) && this.compareTime(currCacheData,preCacheData)){
return true;
}
}
//否则,就是第一次请求
Map<String, Object> cacheMap = new HashMap<>();
cacheMap.put(url, currCacheData);
cache.put(cacheRepeatKey,new Gson().toJson(currCacheData));
return false;
}
//比较参数
private boolean compareParams(RepeatSubmitCacheDto currCacheData, RepeatSubmitCacheDto preCacheData){
Boolean res=currCacheData.getRequestData().equals(preCacheData.getRequestData());
return res;
}
//判断两次间隔时间
private boolean compareTime(RepeatSubmitCacheDto currCacheData, RepeatSubmitCacheDto preCacheData){
Boolean res=( (currCacheData.getCurrTime() - preCacheData.getCurrTime()) < (IntervalTime * 1000) );
return res;
}
}
该代码虽然看起来有点多,但是仔细研读,会发现其实这些代码 就是笔者在上文中贴出的实现流程图 的具体实现,可以说是将理论知识进行真正的落地实现;
在这里再重复赘述一下,其整体的实现思路为:获取当前请求的URL作为键Key,暂且标记为:A1,其取值为映射Map(Map里面的元素由:请求的链接url 、 请求体的数据、和 请求时的时间戳 三部分组成) 暂且标记为V1;从缓存中(本地缓存或者分布式缓存)查找Key=A1的值V2,如果V2和V1里的请求体数据一样 且 两次请求是在8s内,即代表当前请求是重复提交的,系统将拒绝执行后续的业务逻辑;否则可以继续往后面执行 “将用户信息插入到数据库中” 的业务逻辑;
(3)最后,需要将上述自定义的拦截器加入中系统全局配置中,如下所示:
@Component
public class CustomWebConfig implements WebMvcConfigurer{
@Autowired
private RepeatSubmitInterceptor submitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(submitInterceptor);
}
}
运行项目,打开Postman,连续多番进行测试,如下几张图所示:
至此,我们已经采用实际的代码实战实现了“如何防止接口重复提交”的功能,值得一提的是,上述代码在实现过程中,其核心在于缓存组件的搭建;在“重复提交”这一业务场景中,它需要满足两个条件方可发挥作用:一个是可以用于缓存信息,即具有Key - Value的特性;另一个是可以对存储的数据设置过期时间;
在这里笔者采用的是google开发工具类中的CacheBuilder构建本地缓存组件的,感兴趣的小伙伴可以自行搜索相关资料;然而这种实现方式在集群多实例部署的情况下是有问题的,因为CacheBuilder只适用于单一架构体系,所以如果是多实例集群部署的情况,最好用Redis。
精致的结尾
(1)文中涉及到的代码已经放在gitee上了,访问链接如下所示,别忘了给个star哦:https://gitee.com/steadyjack/SpringBootTechnologyA
(2) debug亲自录制的课程中心:https://www.roncoo.com/teacher/detail/201808301113060228
(2)期间如何有任何问题都可以加debug的微信进行交流:debug0868
(3) 关注Debug的技术公众号,学习更多的干货实战技术~~~
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
国家发布数字人民币!跟支付宝微信支付区别大了!看完算长知识了
国家发布数字人民币!跟支付宝微信支付区别大了!看完算长知识了 深圳发钱了,1000万元平分给50000个市民,平均每个人200块。但是这200元却不是实实在在的纸币,也不是微信支付宝的转账,而是由银联直接相对应APP之中发放的数字人民币。这个名词听起来非常的直观和炫酷。可能很多人都很好奇,究竟数字人民为何物,它具有什么样的功能呢?小编今天就带你来了解一下。 从字面上,我们就知道数字人民币其实就是为了方便人们使用而做出来的数字版的人民币,这个数字和你的银行的存款是相同的。数字人民币本身就是国家在法律层面直接确定的和我们的纸币具有同等效力的法定货币,说到底数字货币是钱,而移动支付只是支付方式。 面对现在已经成熟且稳定的移动支付领域,为什么银联现在才进行相关工作的开展呢?其实作为国家机构来说,想要推行一款数字型货币的意义和一般的移动支付是不太一样的。从原理上看,不论我们使用的是支付宝还是微信,其实都是利用了他们的便捷功能。作为支付的一方我们花的是自己的钱,但是这笔钱可能存在第三机构的账户之中,也可能存在银行。而数字人民币就有所不同,你使用APP花费出去的每一分钱都是你银行账户之中的钱,没有中...
- 下一篇
Ubuntu 20.10正式发布:首次采用树莓派4 集成GNOME 3.38
Canonical近日正式发布了包含树莓派优化的Ubuntu 20.10桌面版和服务器版系统,以支持研究人员、发明家、教育和企业。 Ubuntu 20.10包含5.8版Linux内核,提供了最新的工具链,包括glibc 2.32,OpenJDK 11,rustc 1.41,GCC 10,LLVM 11,Python 3.8.6,ruby 2.7.0,php 7.4.9,perl 5.30 ,golang 1.13。 值得一提的是,Ubuntu 20.10是首个采用树莓派4(Raspberry Pi 4)桌面图像功能的Ubuntu版本。 Ubuntu 20.10包含了微云(micro cloud),提供VM的小型服务器集群,按需供给的边缘Kuberenetes的LXD 4.6和MicroK8s 1.19,可适用于远程办公室、分公司、仓储和分布的基础设施。 Ubuntu 20.10集成了GNOME 3.38,此版本改进了应用栅格,移除了常用标签和允许根据用户喜好对应用排列和管理。 电源设置中加入了电池百分比开关,私有WiFi热点可通过生成的QR(二维码)进行分享,重启选项已被添加至注销/关...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Hadoop3单机部署,实现最简伪集群
- CentOS7,CentOS8安装Elasticsearch6.8.6