在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:
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
代码
自定义注解
import java.lang.annotation.elementtype;import java.lang.annotation.retention;import java.lang.annotation.retentionpolicy;import java.lang.annotation.target;/** * @classname norepeatsubmit * @description 这里描述 * @author admin * @date 2021/3/2 16:16 */@target(elementtype.method)@retention(retentionpolicy.runtime)public @interface norepeatsubmit { /** * 设置请求锁定时间 * * @return */ int locktime() default 10;}
aoppackage com.hongkun.aop;/** * @classname repeatsubmitaspect * @description 这里描述 * @author admin * @date 2021/3/2 16:15 */import com.hongkun.until.apiresult;import com.hongkun.until.result;import com.hongkun.until.redislock;import org.aspectj.lang.proceedingjoinpoint;import org.aspectj.lang.annotation.around;import org.aspectj.lang.annotation.aspect;import org.aspectj.lang.annotation.pointcut;import org.slf4j.logger;import org.slf4j.loggerfactory;import org.springframework.beans.factory.annotation.autowired;import org.springframework.stereotype.component;import org.springframework.util.assert;import org.springframework.web.context.request.requestattributes;import org.springframework.web.context.request.requestcontextholder;import org.springframework.web.context.request.servletrequestattributes;import javax.servlet.http.httpservletrequest;import java.util.uuid;import java.util.concurrent.timeunit;/** * @author liucheng * @since 2020/01/15 * 防止接口重复提交 */@aspect@componentpublic class repeatsubmitaspect { private static final logger logger = loggerfactory.getlogger(repeatsubmitaspect.class); @autowired private redislock redislock; @pointcut("@annotation(norepeatsubmit)") public void pointcut(norepeatsubmit norepeatsubmit) { } @around("pointcut(norepeatsubmit)") public object around(proceedingjoinpoint pjp, norepeatsubmit norepeatsubmit) throws throwable { int lockseconds = norepeatsubmit.locktime(); requestattributes ra = requestcontextholder.getrequestattributes(); servletrequestattributes sra = (servletrequestattributes) ra; httpservletrequest request = sra.getrequest(); assert.notnull(request, "request can not null"); // 此处可以用token或者jsessionid string token = request.getheader("token"); string path = request.getservletpath(); string key = getkey(token, path); string clientid = getclientid(); boolean issuccess = redislock.lock(key, clientid, lockseconds,timeunit.seconds); logger.info("trylock key = [{}], clientid = [{}]", key, clientid); if (issuccess) { logger.info("trylock success, key = [{}], clientid = [{}]", key, clientid); // 获取锁成功 object result; try { // 执行进程 result = pjp.proceed(); } finally { // 解锁 redislock.unlock(key, clientid); logger.info("releaselock success, key = [{}], clientid = [{}]", key, clientid); } return result; } else { // 获取锁失败,认为是重复提交的请求 logger.info("trylock fail, key = [{}]", key); return apiresult.success(200, "重复请求,请稍后再试", null); } } private string getkey(string token, string path) { return "00000"+":"+token + path; } private string getclientid() { return uuid.randomuuid().tostring(); }}
以上就是springboot怎么结合aop+redis防止接口重复提交的详细内容。