SpringBoot怎么结合Aop+Redis防止接口重复提交
SpringBoot怎么结合Aop+Redis防止接口重复提交
这篇“SpringBoot怎么结合Aop+Redis防止接口重复提交”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“SpringBoot怎么结合Aop+Redis防止接口重复提交”文章吧。
在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:
1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。
2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。
3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
为什么要防止接口重复提交?
对于有些敏感操作接口,比如新增数据接口、付款接口,要是用户操作不当多次点击提交按钮,这些接口就会被多次请求,最后可能导致系统异常。
前端可以如何控制?
前端可以通过js进行控制,当用户点击提交按钮,
1.按钮设置多少秒内不可点击状态
2.按钮点击后弹出loading提示框,避免再次点击,直到接口请求返回后
3.按钮点击后跳转到新的页面
但是,请记住,永远不要相信用户的行为,因为你不知道用户会做哪些奇葩的操作,所以,最重要的还是要在后端处理。
使用aop+redis进行拦截处理
一.创建切面类RepeatSubmitAspect
实现过程:接口请求后,token+请求路径作为key值去redis中读取数据,若能找到这个key,则证明是重复提交的,反之不是。若不是重复提交,则直接放行,并将这个key写入redis中,并设置一定时间过期(我这里是设置的5s过期)
在传统的web项目中,为了防止重复提交,通常做法是:后端生成唯一的提交令牌(uuid),存储在服务端,页面在发起请求时,携带次令牌,后端验证请求后删除令牌,保证请求的唯一性。
但是,上诉的做法是需要前后端都需要进行改动,如果在项目初期,是可以实现的,但是,在项目的后期,很多功能都实现好了,不可能大范围的去改动。
思路
1.自定义注解@NoRepeatSubmit 标记所有Controller中提交的请求
2.通过AOP对所有标记了@NoRepeatSubmit 的方法进行拦截
3.在业务方法执行前,获取当前用户的token或者JSessionId+当前请求地址,作为一个唯一的key,去获取redis分布式锁,如果此时并发获取,只有一个线程能获取到。
4.业务执行后,释放锁
关于Redis分布式锁
使用Redis是为了在负载均衡部署,如果是单机的项目可以使用一个本地线程安全的Cache替代Redis
代码
自定义注解
importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/***@ClassNameNoRepeatSubmit*@Description这里描述*@Authoradmin*@Date2021/3/216:16*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceNoRepeatSubmit{/***设置请求锁定时间**@return*/intlockTime()default10;}
AOP
packagecom.hongkun.aop;/***@ClassNameRepeatSubmitAspect*@Description这里描述*@Authoradmin*@Date2021/3/216:15*/importcom.hongkun.until.ApiResult;importcom.hongkun.until.Result;importcom.hongkun.until.RedisLock;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.util.Assert;importorg.springframework.web.context.request.RequestAttributes;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjavax.servlet.http.HttpServletRequest;importjava.util.UUID;importjava.util.concurrent.TimeUnit;/***@authorliucheng*@since2020/01/15*防止接口重复提交*/@Aspect@ComponentpublicclassRepeatSubmitAspect{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(RepeatSubmitAspect.class);@AutowiredprivateRedisLockredisLock;@Pointcut("@annotation(noRepeatSubmit)")publicvoidpointCut(NoRepeatSubmitnoRepeatSubmit){}@Around("pointCut(noRepeatSubmit)")publicObjectaround(ProceedingJoinPointpjp,NoRepeatSubmitnoRepeatSubmit)throwsThrowable{intlockSeconds=noRepeatSubmit.lockTime();RequestAttributesra=RequestContextHolder.getRequestAttributes();ServletRequestAttributessra=(ServletRequestAttributes)ra;HttpServletRequestrequest=sra.getRequest();Assert.notNull(request,"requestcannotnull");//此处可以用token或者JSessionIdStringtoken=request.getHeader("token");Stringpath=request.getServletPath();Stringkey=getKey(token,path);StringclientId=getClientId();booleanisSuccess=redisLock.lock(key,clientId,lockSeconds,TimeUnit.SECONDS);LOGGER.info("tryLockkey=[{}],clientId=[{}]",key,clientId);if(isSuccess){LOGGER.info("tryLocksuccess,key=[{}],clientId=[{}]",key,clientId);//获取锁成功Objectresult;try{//执行进程result=pjp.proceed();}finally{//解锁redisLock.unlock(key,clientId);LOGGER.info("releaseLocksuccess,key=[{}],clientId=[{}]",key,clientId);}returnresult;}else{//获取锁失败,认为是重复提交的请求LOGGER.info("tryLockfail,key=[{}]",key);returnApiResult.success(200,"重复请求,请稍后再试",null);}}privateStringgetKey(Stringtoken,Stringpath){return"00000"+":"+token+path;}privateStringgetClientId(){returnUUID.randomUUID().toString();}}
以上就是关于“SpringBoot怎么结合Aop+Redis防止接口重复提交”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注恰卡编程网行业资讯频道。
推荐阅读
-
php如何让Swoole/Pool进程池实现Redis持久连接
php如何让Swoole/Pool进程池实现Redis持久连接本篇...
-
php操作redis大全记录
php连接redis测试˂?php$redis=newRedis();$redis-˃conne...
-
PHP经典高级工程师面试题
1.PHP如何实现不用自带的cookie函数为客户端下发cookie。对于分布式系统,如何来保存session值...
-
PHP操作Redis数据库
-
php利用redis防止商品超发来限制抢购,简单又实用
-
php如何实现秒杀功能?php+redis模拟简单抢购场景,快来看看吧
-
PHP高级工程师面试题
-
Laravel结合Redis发送邮箱验证码
-
使用redis缓存实现多服务器PHP sessions共享
-
PHP用redis的有序集合zset实现延迟队列