当前位置: 首页 > 科技观察

防止重复提交数据的6种方法!

时间:2023-03-21 16:37:10 科技观察

本文转载自微信公众号“Java中文社区”,作者:老王。转载本文请联系Java中文社区公众号。某天突然有朋友问雷哥:在Java中,防止重复提交最简单的方案是什么?这句话包含两个关键信息,第一:防止重复提交;第二:最简单。于是雷问他,是单机环境还是分布式环境?他得到的反馈是单机环境,所以很容易,于是雷开始安装*.话不多说,先复现一下这个问题。根据朋友的反馈,大概的场景是这样的,如下图:简化后的模拟代码如下(基于SpringBoot):importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RequestMapping("/user")@RestControllerpublicclassUserController{/***重复请求方法*/@RequestMapping("/add")publicStringaddUser(Stringid){//业务代码...系统.out.println("添加用户ID:"+id);return"执行成功!";}}于是雷哥想到了通过分别拦截前端和后端来解决数据重复提交的问题。前端拦截前端拦截是指通过HTML页面拦截重复的请求。例如,用户点击“提交”按钮后,我们可以将该按钮设置为不可用或隐藏。执行效果如下图所示:前端拦截的实现代码:

但是有一个前端拦截的致命问题,如果懂行的程序员或者不法用户可以直接绕过前端页面,通过模拟请求的方式重复提交请求,比如充值100元,提交10次变成1000元(发家致富的好方法瞬间被发现)。所以,除了在前端拦截一些正常的误操作,后端的拦截也是必不可少的。后端拦截后端拦截的实现思路是在方法执行前判断业务是否已经执行。如果已经执行过,则不执行,否则正常执行。我们将请求的业务ID存储在内存中,并添加互斥锁来保证多线程下程序执行的安全性。大致的实现思路如下图所示:但是,将数据存储在内存中最简单的方法是使用HashMap存储,或者使用GuavaCache也有同样的效果,但是显然HashMap实现功能更快,所以我们先实现一个HashMap防重复(anti-duplication)版。1.基础版-HashMapimportorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.HashMap;importjava.util.Map;/***普通地图版*/@RequestMapping("/user")@RestControllerpublicclassUserController3{//缓存ID集合privateMapreqCache=newHashMap<>();@RequestMapping("/add")publicStringaddUser(Stringid){//非空判断(Ignore)...synchronized(this.getClass()){//重复请求判断if(reqCache.containsKey(id)){//重复请求System.out.println("不要重复提交!!!"+id);return"执行失败";}//存储请求IDreqCache.put(id,1);}//业务代码...System.out.println("添加用户ID:"+id);return"执行成功!";}}实现效果如下图所示:存在问题:这种实现方式有个致命的问题,因为HashMap是无限增长的,所以会占用越来越多的内存,而且随着HashMap个数的增加吨查找的速度也会降低,所以我们需要实现一个可以自动“清除”过期数据的实现。2、优化版——定长数组该版本解决了HashMap无限增长的问题。它通过在数组中添加一个下标计数器(reqCacheCounter)的方式来实现固定数组的循环存储。当数组存储到最后一位时,将数组的存储下标设置为0,然后从头开始存储数据。实现代码如下:importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation。RestController;importjava.util.Arrays;@RequestMapping("/user")@RestControllerpublicclassUserController{privatestaticString[]reqCache=newString[100];//请求ID存储集合privatestaticIntegerreqCacheCounter=0;//请求计数器(表示存储ID的位置)@RequestMapping("/add")publicStringaddUser(Stringid){//非空判断(忽略)...synchronized(this.getClass()){//重复请求判断if(Arrays.asList(reqCache).contains(id)){//重复请求System.out.println("不要重复提交!!!"+id);return"执行失败";}//记录请求IDif(reqCacheCounter>=reqCache.length)reqCacheCounter=0;//重置计数器reqCache[reqCacheCounter]=id;//将ID保存到缓存中reqCacheCounter++;//将下标后移一位}//业务代码...System.out.println("AdduserID:"+编号);return"Executionsucceeded!";}}3.Extendedversion-之前双检测锁(DCL)的实现方式把判断和添加业务放到synchronized中进行加锁操作,显然性能不是很高,所以我们可以使用单例中著名的DCL(DoubleCheckedLocking)优化代码的执行效率,实现代码如下:[]reqCache=newString[100];//请求ID存储集合privatestaticIntegerreqCacheCounter=0;//请求计数器(表示ID存储位置)@RequestMapping("/add")publicStringaddUser(Stringid){//非空判断(ignore)...//重复请求判断if(Arrays.asList(reqCache).contains(id)){//重复请求System.out.println("不要重复提交!!!"+id);返回“执行失败”;}synchronized(this.getClass()){//双重检查锁(DCL,doublecheckedlocking)提高程序执行效率if(Arrays.asList(reqCache).contains(id)){//重复请求System.out.println("请勿重复提交!!!"+id);return"执行失败";}//记录请求IDif(reqCacheCounter>=reqCache.length)reqCacheCounter=0;//重置计数器reqCache[reqCacheCounter]=id;//将ID保存到缓存中reqCacheCounter++;//下标向后移动一位}//业务代码...System.out.println("添加用户ID:"+id);return"执行成功!";}}注意:DCL适合频繁重复提交对于比较高的业务场景,DCL不适用于对面的业务场景。4.完美版——LRUMap上面的代码基本实现了重复数据的拦截,但是显然不够简洁优雅,比如下标计数器的语句和业务Processing等,幸好Apache为我们提供了一个commons-collections框架,它有一个非常有用的数据结构LRUMap,可以保存指定数量的固定数据,它会帮助你根据LRU算法清除最commons-collections。不常用的数据。Tips:LRU是LeastRecentlyUsed的缩写,即最近最少使用。是一种常用的数据淘汰算法,选择最长时间未被使用的数据进行淘汰。首先,让我们添加对Apachecommonscollections的引用:org.apache.commonscommons-collections44.4实现代码如下:importorg.apache.commons.collections4。地图。LRUMap;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RequestMapping("/user")@RestControllerpublicclassUserController{//最大容量为100,剔除数据根据LRU算法Map集合privateLRUMapreqCache=newLRUMap<>(100);@RequestMapping("/add")publicStringaddUser(Stringid){//非空判断(ignore)...synchronized(this.getClass()){//重复请求判断if(reqCache.containsKey(id)){//重复请求System.out.println("不要重复提交!!!"+id);return"执行失败";}//存储请求IDreqCache.put(id,1);}//业务代码...System.out.println("添加用户ID:"+id);return"Executionsucceeded!";}}使用LRUMap后,代码明显简单多了5.最终版——封装以上都是方法级的实现方案。但是在实际业务中,我们可能会有很多方法需要进行防重复。然后我们会为所有类封装一个公共方法。使用:importorg.apache.commons.collections4.map.LRUMap;/***幂等性判断*/publicclassIdempotentUtils{//根据LRU(LeastRecentlyUsed,最近最少使用)算法淘汰Map集合的数据,最大容量为100privatestaticLRUMapreqCache=newLRUMap<>(100);/***幂等判断*@return*/publicstaticbooleanjudge(Stringid,ObjectlockClass){synchronized(lockClass){//重复请求判断if(reqCache.containsKey(id)){//重复请求System.out.println("不要重复提交!!!"+id);returnfalse;}//非重复请求,存储请求IDreqCache.put(id,1);}returntrue;}}调用代码如下:importcom.example.idempote.util.IdempotentUtils;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RequestMapping("/user")@RestControllerpublicclassUserController4{@RequestMapping("/add")publicStringaddUser(Stringid){//非空判断(忽略)...//------------无能调用(开始)-------------if(!IdempotentUtils.judge(id,this.getClass())){rreturn"执行失败";}//------------幂等调用(结束)------------//业务代码..System.out.println("添加用户ID:"+id);return"Executionsucceeded!";}}Tips:一般到这里代码就结束了,但是如果想更简洁也是可以实现的,可以通过自定义注解的方式将业务代码写入注解中。需要调用的方法只需要写一行注解即可,防止重复提交数据。专区留言666)知识扩展——LRUMap实现原理解析既然LRUMap这么强大,那我们就来看看它是如何实现的。LRUMap的本质是持有头节点的环回双链表结构。它的存储结构如下:AbstractLinkedMap.LinkEntryentry;当查询方法被调用时,使用的元素将被放置在双链表表头的前一个位置。源码如下:publicVget(Objectkey,booleanupdateToMRU){LinkEntryentry=this.getEntry(key);if(entry==null){returnnull;}else{if(updateToMRU){this.moveToMRU(进入);}回归。getValue();}}protectedvoidmoveToMRU(LinkEntryentry){if(entry.after!=this.header){++this.modCount;if(entry.before==null){thrownewIllegalStateException("Entry.before为null。如果您的密钥是不可变的,并且您已正确使用同步,则不应发生这种情况。");}entry.before.after=entry.after;entry.after.before=entry.before;entry.after=this.header;entry.before=this.header.before;this.header.before.after=entry;this.header.before=entry;}elseif(entry==this.header){thrownewIllegalStateException("无法将标头移动到MRU这不应该发生,因为您的密钥是不可变的,并且您已经使用正确同步。");}}如果添加元素时容量满了header的最后一个元素会被移除,添加源码如下:protectedvoidaddMapping(inthashIndex,inthashCode,Kkey,Vvalue){//判断容器是否已满if(this.isFull()){LinkEntryreuse=this.header.after;booleanremoveLRUEntry=false;if(!this.scanUntilRemovable){removeLRUEntry=this.removeLRU(reuse);}else{while(reuse!=this.header&&reuse!=null){if(this.removeLRU(reuse)){removeLRUEntry=true;break;}reuse=reuse.after;}if(reuse==null){thrownewIllegalStateException("Entry.after=null,header.after="+this.header.after+"header.before="+this.header.before+"key="+key+"value="+value+"size="+this.size+"maxSize="+this.maxSize+"Thisshouldnotoccurifyourkeysareimmutable,andyouhaveusedsynchronizationproperly.");}}if(removeLRUEntry){if(reuse==null){thrownewIllegalStateException("reuse=null,header.after="+this.header.after+"header.before="+this.header.before+"key="+key+"value="+value+"size="+this.size+"maxSize="+this.最大尺寸+“这不应该发生,因为你的键是不可变的,并且你已经正确使用了同步。”);}this.reuseMapping(reuse,hashIndex,hashCode,key,value);}else{super.addMapping(hashIndex,hashCode,key,value);}}else{super.addMapping(hashIndex,hashCode,key,value);}}容量判断源码:publicbooleanisFull(){returnsize>=maxSize;}如果容量不够直接添加数据full:super.addMapping(hashIndex,hashCode,key,value);如果容量已满,则调用reuseMapping方法,使用LRU算法清除数据。总的来说:LRUMap的本质就是持有头节点的环回双链表结构。当使用一个元素时,该元素被放置在双链表头的前一个位置,添加元素时,如果容量满了,会移除表头的最后一个元素。小结本文讲了6种防止重复提交数据的方法。首先是前端的拦截,可以通过隐藏和设置不可用的按钮来屏蔽正常运行下的重复提交。但是为了避免通过异常渠道重复提交,我们实现了5个版本的后端拦截:HashMap版本、固定数组版本、带双检测锁的数组版本、LRUMap版本和LRUMap封装版本。原文链接:https://mp.weixin.qq.com/s/p1MRZpnxohnX2jIpZDczmQ