防重放(接口防抖)

1、概念

所谓防抖,一是防用户手抖,二是防网络抖动。在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,进而生成重复的数据记录。要针对用户的误操作,前端通常会实现按钮的loading状态,阻止用户进行多次点击。而对于网络波动造成的请求重发问题,仅靠前端是不行的。为此,后端也应实施相应的防抖逻辑,确保在网络波动的情况下不会接收并处理同一请求多次。
一个理想的防抖组件或机制,我觉得应该具备以下特点:

逻辑正确,也就是不能误判;
响应迅速,不能太慢;
易于集成,逻辑与业务解耦;
良好的用户反馈机制,比如提示“您点击的太快了”

2、思路解析

哪一类接口需要防抖?
接口防抖也不是每个接口都需要加,一般需要加防抖的接口有这几类:

用户输入类接口:比如搜索框输入、表单输入等,用户输入往往会频繁触发接口请求,但是每次触发并不一定需要立即发送请求,可以等待用户完成输入一段时间后再发送请求。
按钮点击类接口:比如提交表单、保存设置等,用户可能会频繁点击按钮,但是每次点击并不一定需要立即发送请求,可以等待用户停止点击一段时间后再发送请求。
滚动加载类接口:比如下拉刷新、上拉加载更多等,用户可能在滚动过程中频繁触发接口请求,但是每次触发并不一定需要立即发送请求,可以等待用户停止滚动一段时间后再发送请求。
如何确定接口是重复的?
防抖也即防重复提交,那么如何确定两次接口就是重复的呢?首先,我们需要给这两次接口的调用加一个时间间隔,大于这个时间间隔的一定不是重复提交;其次,两次请求提交的参数比对,不一定要全部参数,选择标识性强的参数即可;最后,如果想做的更好一点,还可以加一个请求地址的对比。

3、分布式环境下如何防抖动

3.1、使用共享缓存

在这里插入图片描述

3.2、使用分布式锁

在这里插入图片描述
常见的分布式组件有Redis、Zookeeper等,但结合实际业务来看,一般都会选择Redis,因为Redis一般都是Web系统必备的组件,不需要额外搭建

3.3、具体实现

现在有一个保存用户的接口

@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity<String> add(@RequestBody AddReq addReq) {
        return userService.add(addReq);
}

AddReq.java

package com.summo.demo.model.request;
import java.util.List;
import lombok.Data;
@Datapublic class AddReq {
    /**     * 用户名称     */    private String userName;
    /**     * 用户手机号     */    private String userPhone;
    /**     * 角色ID列表     */    private List<Long> roleIdList;}

目前数据库表中没有对userPhone字段做UK索引,这就会导致每调用一次add就会创建一个用户,即使userPhone相同。

3.3.1、请求锁

根据上面的要求,我定了一个注解@RequestLock,使用方式很简单,把这个注解打在接口方法上即可。RequestLock.java

package com.summo.demo.model.request;

import java.util.List;

import lombok.Data;

@Data
public class AddReq {

    /**
     * 用户名称
     */
    private String userName;

    /**
     * 用户手机号
     */
    private String userPhone;

    /**
     * 角色ID列表
     */
    private List<Long> roleIdList;
}

@Req

### Java 实现接口防抖的技术方案 在 Java 开发中,尤其是基于 Spring Boot 的项目中,可以通过多种技术手段实现接口防抖功能。以下是常见的几种实现方式及其原理: #### 1. **基于 Redis 的分布式锁** 通过 Redis 存储键值对的方式实现防抖效果。当用户发起请求时,生成唯一的 key 并存储到 Redis 中,在指定的时间范围内阻止重复请求。 ```java import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; public boolean debounce(String uniqueKey, long expireTimeInSeconds) { StringRedisTemplate redisTemplate = getRedisTemplate(); // 获取 Redis 模板对象 Boolean isPresent = redisTemplate.opsForValue().setIfAbsent(uniqueKey, "true", expireTimeInSeconds, TimeUnit.SECONDS); if (isPresent != null && !isPresent) { // 如果返回 false 表示已经存在该 key,则认为是重复请求 throw new RuntimeException("请勿重复提交"); } return true; // 请求成功处理逻辑继续往下走 } ``` 上述代码利用 `SETNX` 命令(即 set-if-not-exist),只有当 key 不存在时才会设置成功[^1]。这种方式适用于高并发场景下的接口防抖需求。 #### 2. **本地线程安全变量** 对于单机部署的应用程序来说,也可以采用简单的 ThreadLocal 或者 ConcurrentHashMap 来保存当前用户的操作状态。 ```java private final Map<String, Long> requestMap = new ConcurrentHashMap<>(); public void handleRequest(String userId){ synchronized(this.requestMap){ Long lastAccessedTime = this.requestMap.get(userId); if(lastAccessedTime!=null && System.currentTimeMillis()-lastAccessedTime<5000L){ //假设间隔时间为5秒 throw new IllegalStateException("Too frequent requests"); }else{ this.requestMap.put(userId,System.currentTimeMillis()); // 正常业务逻辑... } } } ``` 此方法简单易懂,适合小型应用或者不需要考虑跨服务器同步的情况[^2]。 #### 3. **AOP 切面拦截器** 借助 AOP 面向切面编程的思想,在进入具体服务层之前就完成对重复请求的过滤工作。 定义一个自定义注解 @Debounce: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Debounce { int interval() default 5000;//默认超时时长为5s } ``` 编写对应的 Aspect 类: ```java @Aspect @Component public class DebounceInterceptor { private static final Logger logger = LoggerFactory.getLogger(DebounceInterceptor.class); @Around("@annotation(debounce)") public Object aroundAdvice(ProceedingJoinPoint joinPoint, Debounce debounce)throws Throwable{ MethodSignature signature = (MethodSignature)joinPoint.getSignature(); Class<?> targetClass = signature.getMethod().getDeclaringClass(); StringBuilder sb=new StringBuilder(targetClass.getName()).append(".").append(signature.getName()); List<Object> args= Arrays.asList(joinPoint.getArgs()); for(Object arg :args ){ if(arg instanceof Serializable){ sb.append("_").append(((Serializable)arg).toString()); } } String cacheKey=sb.toString(); CacheManager cm=getCacheManager();//从容器里取出cache manager实例 Element element=cm.get(cacheKey); if(element==null){ try{ cm.put(new Element(cacheKey,new Date())); return joinPoint.proceed(); }finally{ Timer timer=new Timer(true); timer.schedule(new TimerTask(){ @Override public void run(){ cm.remove(cacheKey); } },debounce.interval()); } }else{ throw new RepeatSubmitException("Do not submit repeatedly within "+debounce.interval()+" ms."); } } } ``` 这种方法的好处在于无需修改原有代码结构即可达到目的,并且易于扩展维护[^3]^。 #### 4. **数据库层面保障** 除了以上提到的技术外,还可以从业务角度出发,在数据库设计阶段加入唯一约束条件来避免数据冗余问题的发生。例如针对某些特定字段组合创建联合索引来强制保证其唯一性[^4]。 --- ### 注意事项 无论采取哪种措施都需要综合考量实际应用场景以及性能开销等因素做出合理选择;另外值得注意的是单纯依靠前端校验无法完全杜绝恶意行为带来的风险因此后端验证必不可少。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值