一、核心区别
特性 | 接口防抖(debouncing) | 接口幂等性(idempotency) |
---|---|---|
目的 | 减少资源浪费:防止短时间内多次触发同一操作(如用户频繁点击、网络抖动导致重复请求)。 | 保证结果一致性:确保同一请求无论调用一次还是多次,最终结果相同,避免重复操作导致的数据异常。 |
作用层面 | 前端/后端均可实现:前端优化用户体验,后端过滤重复请求。 | 后端核心逻辑:依赖业务逻辑和数据层设计,确保操作的唯一性。 |
关注点 | 时间窗口内的重复请求:只处理最后一次或首次请求。 | 请求的唯一性标识:通过唯一标识符(如请求id、业务参数)判断是否重复。 |
典型场景 | 用户搜索输入、按钮多次点击、无限滚动加载。 | 支付接口、订单创建、数据修改等需避免重复操作的场景。 |
二、实现方式
接口防抖:
核心思想:在指定时间窗口内,仅允许最后一次(或首次)请求生效。
1.前端画面每次请求添加loading遮罩层(接口响应时间过长就会导致用户体验不好)
2.使用redis每次将请求主要参数和请求人绑定起来,放入指定的缓存时间,第二次再请求看到是同一个接口和同一个人操作则提示:操作频繁,稍后重试!
(推荐,做成自定义注解的方式,实现简单)
3.前端发送请求时,在指定时间窗口内,延迟发送请求
(不推荐,毕竟会延迟发送请求,影响接口速度)
let timeout; function handlesearchinput(event) { cleartimeout(timeout); timeout = settimeout(() => { // 发送请求 fetch('/search', { query: event.target.value }); }, 300); // 300ms防抖间隔 }
接下来聊聊第二种方式,自定义注解:
1.aop (拦截请求,并获取请求具体信息,将url,接口主要参数,用户id存入redis中)
package com.qeoten.sms.edu.config; import com.qeoten.sms.util.api.r; import com.qeoten.sms.util.auth.authutil; import com.qeoten.sms.util.util.digestutil; import com.qeoten.sms.util.util.redisutil; import io.lettuce.core.dynamic.support.reflectionutils; import lombok.extern.slf4j.slf4j; import org.aspectj.lang.proceedingjoinpoint; import org.aspectj.lang.annotation.around; import org.aspectj.lang.annotation.aspect; import org.aspectj.lang.reflect.methodsignature; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; import java.lang.reflect.field; import java.lang.reflect.method; import java.lang.reflect.parameter; import java.util.concurrent.timeunit; /** * 接口防抖aop */ @aspect @component @slf4j public class antishakeaop { @autowired private redisutil redisutil; private static final string prefix = "repeatsubmit"; @around(value = "@annotation(com.qeoten.sms.edu.config.repeatclick)") public object antishake(proceedingjoinpoint pjp) throws throwable { // 获取调用方法的信息和签名信息 methodsignature signature = (methodsignature) pjp.getsignature(); // 获取方法 method method = signature.getmethod(); // 获取注解中的参数 repeatclick annotation = method.getannotation(repeatclick.class); string key = getlockkey(pjp); // 查询redis中是否存在对应关系 if (!redisutil.haskey(key)) { redisutil.setkeyandexpire(key, null, annotation.value(), timeunit.milliseconds); return pjp.proceed(); } else { log.error(annotation.message()); return r.fail(annotation.message()); } } public static string getlockkey(proceedingjoinpoint joinpoint) { //获取连接点的方法签名对象 methodsignature methodsignature = (methodsignature) joinpoint.getsignature(); //method对象 method method = methodsignature.getmethod(); string classname = method.getdeclaringclass().getname(); //获取method对象上的注解对象 //获取方法参数 final object[] args = joinpoint.getargs(); //获取method对象上所有的注解 final parameter[] parameters = method.getparameters(); stringbuilder sb = new stringbuilder(); for (int i = 0; i < parameters.length; i++) { final repeatclick keyparam = parameters[i].getannotation(repeatclick.class); if (keyparam == null) { //如果属性不是repeatsubmit注解,则获取方法的参数名 sb.append(args[i]).append("&"); } else { final object object = args[i]; //获取注解类中所有的属性字段 final field[] fields = object.getclass().getdeclaredfields(); for (field field : fields) { //判断字段上是否有repeatsubmit注解 final repeatclick annotation = field.getannotation(repeatclick.class); //如果没有,跳过 if (annotation == null) { continue; } //如果有,设置accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量) field.setaccessible(true); //如果属性是repeatsubmit注解,则拼接 连接符" & + repeatsubmit" sb.append(reflectionutils.getfield(field, object)).append("&"); } } } //返回指定前缀的key return prefix + ":" + classname + ":" + method.getname() + ":" + authutil.getuserid() + ":" + digestutil.md5hex((sb.tostring())); } }
2.自定义注解模板(配置缓存时间,和指定提示消息)
package com.qeoten.sms.edu.config; import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; /** * @author qt-pc-0021 */ @target({elementtype.method, elementtype.parameter, elementtype.field}) @retention(retentionpolicy.runtime) public @interface repeatclick { /** * 默认的防抖时间ms * * @return */ long value() default 1000; string message() default "操作太频繁,请稍后再试!"; }
3.在需要进行操作表的接口上,添加自定义注解,实现功能
@getmapping("/advancepaper") @apioperationsupport(order = 2) @apioperation(value = "交卷", notes = "传入考试id") @repeatclick public r<myexamvo> advancepaper(@requestparam long examid){ // 接口逻辑,可能频繁操作表 }
接口幂等性:
核心思想:通过唯一标识符(如请求id、业务参数)确保同一请求只处理一次。
1.数据库唯一索引:
数据库设置唯一索引重复提交时,插表就会直接报错重复
(不推荐,毕竟压力直接进入数据库了)
2.数据库乐观锁:(数据修改时间 / 版本号) => 比对
查询列表画面时,将数据的修改时间(毫秒级)记录一下,下次请求增删改接口时,将数据原本的修改时间传入接口,接口第一步判断当前数据的修改时间是否和画面上传入的修改时间一致,一致就代表没有人修改做此数据,否则就提示此数据已被他人修改,请稍后再试!
最后更新记录时,带入版本号或者修改时间进去,
update xxx set name = xxx where id = xxx and updatetime = xxx
(并发量小的时候可以,并发大的时候存在重复修改问题)
3.唯一值+缓存:
其实也就是接口防抖中的第二个实现方案的变化版本
上面提到将接口的主要参数+用户id作为唯一标识存入redis并记录指定的缓存时间,那么这次存入redis不记录时间,并且在接口结束时清除掉此缓存。
(推荐,但是当服务异常挂掉时,或者某些原因接口没有正常执行完成时,redis缓存一直都会在,不好维护,浪费资源)
4.分布式锁(redisson)
业务开始时候去trylock,尝试获取锁(锁的参数可以是本次操作的对象id,假如说本次要给某个商品增加扣减库存,那么参数可以是商品id),保障在接口的最后一步,释放锁即可。
rlock lock = redissonclient.getlock("my-distributed-lock"); // 尝试获取锁:等待最多 10 秒,锁自动续期 30 秒 boolean islocked = lock.trylock(10, 30, timeunit.seconds);
这样每次拿到锁的线程才会继续进行接口逻辑操作。
5.手动实现锁
其实原理和第4点一样,就是需要考虑手动实现锁的复杂性
- 加锁时如何保证加锁和给锁设置有效期的一致性
- 锁的过期时间,锁需要释放
- 锁不能提前释放,防止其他线程获取到此锁
- 怎样给将要过期的锁加过期时间
- 释放锁的时候,如何保证释放的是同一个锁,防止错释放
- 保证释放锁时的原子性
1. 加锁时setnx命令,设置其lock资源名称 + value(一般为threadid / 时间戳) + 过期时间
2. 进行后续业务操作
3. 最后需要用lua脚本来释放锁(先获取锁的value确保是当前的lock,使用脚本释放锁)
总结
- 防抖:重点是减少请求次数,通过时间戳、缓存实现。
- 幂等性:重点是保证结果唯一,通过唯一标识符、数据库约束或锁或业务校验实现。
- 实际应用:通常需要结合两者,例如:
前端防抖:减少无效请求。
后端幂等性:即使防抖失效,也能保证最终结果一致。
根据业务需求选择合适的方案,例如:
- 高频非敏感操作(如普通的修改或者删除接口):使用本地缓存或 redis 防抖。
- 敏感操作(如支付):结合 redis 唯一标识符和数据库唯一索引确保幂等。
到此这篇关于java接口防抖/幂等性解决(redis)的文章就介绍到这了,更多相关java接口防抖幂等性内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论