通过spring boot/redis/aspect 防止表单重复提交【防抖】
一、啥是防抖
所谓防抖,一是防用户手抖,二是防网络抖动。在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,进而生成重复的数据记录。要针对用户的误操作,前端通常会实现按钮的loading状态,阻止用户进行多次点击。而对于网络波动造成的请求重发问题,仅靠前端是不行的。为此,后端也应实施相应的防抖逻辑,确保在网络波动的情况下不会接收并处理同一请求多次。
一个理想的防抖组件或机制,我觉得应该具备以下特点:
逻辑正确,也就是不能误判;
响应迅速,不能太慢;
易于集成,逻辑与业务解耦;
良好的用户反馈机制,比如提示“您点击的太快了”
二、思路解析
前面讲了那么多,我们已经知道接口的防抖是很有必要的了,但是在开发之前,我们需要捋清楚几个问题。
2.1.哪一类接口需要防抖?
接口防抖也不是每个接口都需要加,一般需要加防抖的接口有这几类:
用户输入类接口:比如搜索框输入、表单输入等,用户输入往往会频繁触发接口请求,但是每次触发并不一定需要立即发送请求,可以等待用户完成输入一段时间后再发送请求。
按钮点击类接口:比如提交表单、保存设置等,用户可能会频繁点击按钮,但是每次点击并不一定需要立即发送请求,可以等待用户停止点击一段时间后再发送请求。
滚动加载类接口:比如下拉刷新、上拉加载更多等,用户可能在滚动过程中频繁触发接口请求,但是每次触发并不一定需要立即发送请求,可以等待用户停止滚动一段时间后再发送请求。
2.2.如何确定接口是重复的?
防抖也即防重复提交,那么如何确定两次接口就是重复的呢?首先,我们需要给这两次接口的调用加一个时间间隔,大于这个时间间隔的一定不是重复提交;其次,两次请求提交的参数比对,不一定要全部参数,选择标识性强的参数即可;最后,如果想做的更好一点,还可以加一个请求地址的对比。

- 定义一个RequestLock,配置超时时间、异常消息、分组标识(用户标识)
/*** 请求锁,防止重复提交** @author xt*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLock {/*** 过期时间** @return*/long expire() default 3;/*** 异常提示** @return*/String message() default "您的操作太快了,请稍后重试";/*** 参数分隔符** @return*/String delimiter() default "|";/*** 时间单位** @return*/TimeUnit timeUnit() default TimeUnit.SECONDS;/*** 前缀(从请求header key)** @return*/String group() default "loginuserid";
}
- 定义一个aspect 实现对注解RequestLock的endpoint进行拦截
@EnableAspectJAutoProxy
@Aspect
@Configuration
@Order
public class RequestLockAspect {@Resourceprivate RedisTemplate redisTemplate;@Pointcut("execution(public * * (..)) && @annotation(org.xt.shisui.redis.duplicate.RequestLock)")public void endpointPointcut() {}@Around("endpointPointcut()")public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method method = methodSignature.getMethod();RequestLock requestLock = method.getAnnotation(RequestLock.class);if (redisTemplate != null) {String key = RequestLockKeyGenerator.getLockKey(joinPoint);Boolean success = redisTemplate.opsForValue().setIfAbsent(key, new byte[0], requestLock.expire(), requestLock.timeUnit());if (Boolean.FALSE.equals(success)) {return Response.no(requestLock.message());}}return joinPoint.proceed();}
}
- 根据请求参数构建RequestLock锁的key,即Redis存储的key
/*** 根据请求参数构建锁的key** @author xt* @date 2022-07-15 14:21*/
public class RequestLockKeyGenerator {public static String getLockKey(ProceedingJoinPoint joinPoint) {String ipAddress = null, group = null;ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();//方法名称String methodName = signature.getName();//类路径String declaringTypeName = signature.getDeclaringTypeName();RequestLock requestLock = method.getAnnotation(RequestLock.class);if (attributes != null) {//加上请求中的ip和分组标识,防止错误拦截HttpServletRequest request = attributes.getRequest();ipAddress = request.getRemoteAddr();group = request.getHeader(requestLock.group());}final Object[] args = joinPoint.getArgs();final Parameter[] parameters = method.getParameters();StringBuilder params = new StringBuilder();String delimiter = requestLock.delimiter();for (int i = 0; i < parameters.length; i++) {//忽略特殊参数,如图片、大文本等,如果是存hashcode 可以不需要这个注解final RequestLockKeyIgnore keyIgnore = parameters[i].getAnnotation(RequestLockKeyIgnore.class);if (keyIgnore != null) {continue;}Object arg = args[i];if (arg != null) {params.append(delimiter).append(arg);}}StringBuilder result = new StringBuilder();result.append(declaringTypeName).append(delimiter).append(methodName).append(delimiter).append(ipAddress).append(delimiter).append(delimiter).append(group).append(params.hashCode());return result.toString();}
}
- 如果Redis存储请求参数字符串,可以增加特殊参数忽略注解,如图片等属性,建议用hashcode
/*** 忽略该参数,防止一些base64字符串被当做主键** @author xt* @date 2022-01-05 14:37*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLockKeyIgnore {
}
- 具体使用demo
@RequestLock(expire = 5)@ApiOperation("新增")@RequestMapping(value = "/create", method = RequestMethod.POST)public Response<ChatSpeechcraftCategoryCreateResp> create(@RequestBody @Validated ChatSpeechcraftCategoryCreateReq req, final HttpServletRequest request) throws SimpleException {return chatSpeechcraftCategoryApiService.create(req);}
相关文章:
通过spring boot/redis/aspect 防止表单重复提交【防抖】
一、啥是防抖 所谓防抖,一是防用户手抖,二是防网络抖动。在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,进而生成重复的数据记录。要针…...
C++ 作业 24/3/14
1、成员函数版本实现算术运算符的重载;全局函数版本实现算术运算符的重载 #include <iostream>using namespace std;class Test {friend const Test operator-(const Test &L,const Test &R); private:int c;int n; public:Test(){}Test(int c,int n…...
新品牌推广怎么做?百度百科创建是第一站
创业企业的宣传推广怎么做?对于初创的企业、或者品牌来说,推广方式都有一个循序渐进的过程,但多数领导者都会做出同一选择,第一步就是给自己的企业创建一个百度百科词条。在百度百科建立自己的企业、或产品词条,不仅可以树立相关信…...
k8s系列-kubectl 命令快速参考
这些指令适用于 Kubernetes v1.29。要检查版本,请使用 kubectl version 命令。 我们经常用到 --all-namespaces 参数,你应该要知道它的简写: kubectl -AKubectl 上下文和配置 kubectl config view # 显示合并的 kubeconfig 配置# 同时使用多…...
微信小程序--开启下拉刷新页面
1、下拉刷新获取数据enablePullDownRefresh 开启下拉刷新: enablePullDownRefreshbooleanfalse是否开启当前页面下拉刷新 案例: 下拉刷新,获取新的列表数据,其实就是进行一次新的网络请求: 第一步:在.json文件中开…...
【研发日记】Matlab/Simulink技能解锁(五)——Simulink布线技巧
前言 见《【研发日记】Matlab/Simulink技能解锁(一)——在Simulink编辑窗口Debug》 见《【研发日记】Matlab/Simulink技能解锁(二)——在Function编辑窗口Debug》 见《【研发日记】Matlab/Simulink技能解锁(三)——在Stateflow编辑窗口Debug》 见《【研发日记】Matlab/Simulink…...
FPGA高端项目:FPGA基于GS2971+GS2972架构的SDI视频收发+OSD动态字符叠加,提供1套工程源码和技术支持
目录 1、前言免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本方案的SDI接收发送本方案的SDI接收图像缩放应用本方案的SDI接收纯verilog图像缩放纯verilog多路视频拼接应用本方案的SDI接收HLS图像缩放HLS多路视频拼接应用本方案的SDI接收HLS多路视频融合叠加应用本方案的S…...
面向对象编程第二式:继承 (Java篇)
本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. 🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人…...
2024最新小狐狸AI 免授权源码
后台安装步骤: 1、在宝塔新建个站点,php版本使用7.2 、 7.3 或 7.4,把压缩包上传到站点根目录,运行目录设置为/public 2、导入数据库文件,数据库文件是 /db.sql 3、修改数据库连接配置,配置文件是/.env 4、…...
5.69 BCC工具之runqlen.py解读
一,工具简介 runqlen工具用于分析和报告运行队列(run queue)的长度,并以直方图的形式展示。它通过在所有CPU上以99赫兹的频率对运行队列长度进行采样来工作。 在操作系统中,运行队列是指内核用来管理待执行(runnable)进程的队列。当一个进程准备好执行,但由于某些原因…...
什么软件可以改变ip地址
什么软件可以修改ip地址,想必很多朋友都在寻找类似的软件,也想知道其中的答案,也能提高自己工作的效率。 经过小编在互联网摸爬滚打这些年,测试认证和整理后,发现一款名叫深度IP转换器的软件,这个确确实实能…...
C语言-strncmp strncat strncpy长度受限制的字符串函数
strncmp strncat strncpy长度受限制的字符串函数 首先 我们需要知道 这几个的语法格式差不多 这里传递的size_t的长度是传递的字节长度 不是个数 也就这里int*是四个字节 char*是一个字节 如果是整数进行交换 。此时也就需要20个字节,这样可以交换五个整数 这里差…...
ROS Kinetic通信编程:话题、服务、动作编程
文章目录 一、话题编程二、服务编程三、动作编程 接上篇,继续学习ROS通信编程基础 一、话题编程 步骤: 创建发布者 初始化ROS节点向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型按照一定频率循环发布消息 创建订阅者 初始化…...
还原wps纯粹的编辑功能
1.关闭稻壳模板: 1.1. 启动wps(注意不要乱击稻壳模板,点了就找不到右键菜单了) 1.2. 在稻壳模板选项卡右击:选不再默认展示 2.关闭托盘中wps云盘图标:右击云盘图标/同步与设置: 2.1.关闭云文档同步 2.2.窗口选桌面应用…...
【烹饪】清炒菠菜的学习笔记
1 焯水:15s左右即可 Kimi教授 菠菜含有草酸,与含钙丰富的食物共煮时可能会形成草酸钙,影响钙的吸收,因此在烹饪时通常建议先用开水烫一下菠菜以减少草酸含量。 2 可选调料:鸡精...
AcWing 4964.子矩阵
首先就是运用了暴力的思路,能够过个70%的数据,剩下的直接时间超时了,没办法优化了。 讲一下暴力的思路: 其实就是模拟而已,也就是看作想要找的矩阵为一个小窗口,然后不断移动的事而已。 #include<ios…...
代码随想录算法训练营第day20|530.二叉搜索树的最小绝对差 、 501.二叉搜索树中的众数 、236. 二叉树的最近公共祖先
530.二叉搜索树的最小绝对差 力扣题目链接 (opens new window) 给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。 示例: 提示:树中至少有 2 个节点。 二叉搜索树是一颗有序的树,可以通过中…...
Hystrix的原理及应用:构建微服务容错体系的利器(二)
本系列文章简介: 本系列文章旨在深入剖析Hystrix的原理及应用,帮助大家理解其如何在微服务容错体系中发挥关键作用。我们将从Hystrix的核心原理出发,探讨其隔离、熔断、降级等机制的实现原理;接着,我们将结合实际应用场…...
【nuget】如何移动 nuget 缓存文件夹
如何移动 nuget 缓存文件夹 一、了解NuGet包的默认存放路径二、为什么需要修改NuGet包的默认存放路径?使用下面的命令查看本地包位置三、更改下载的NuGet包存储位置四、修改VS离线包引用地址五、验证修改的新路径是否成功默认情况下,NuGet下载的包存放在系统盘(C盘中),这样一…...
H266开源视频编码器VVENC现状
VVenC 是由 Fraunhofer HHI 研究团队开发的,主要是视频编码系统组。HHI 是欧洲最大的研究组织 Fraunhofer 协会的成员,该协会是德国的一个大型非营利性组织。源代码在: https://github.com/fraunhoferhhi/vvenc VVenC几乎与H.266视频标准同时…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
关于uniapp展示PDF的解决方案
在 UniApp 的 H5 环境中使用 pdf-vue3 组件可以实现完整的 PDF 预览功能。以下是详细实现步骤和注意事项: 一、安装依赖 安装 pdf-vue3 和 PDF.js 核心库: npm install pdf-vue3 pdfjs-dist二、基本使用示例 <template><view class"con…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...
在Zenodo下载文件 用到googlecolab googledrive
方法:Figshare/Zenodo上的数据/文件下载不下来?尝试利用Google Colab :https://zhuanlan.zhihu.com/p/1898503078782674027 参考: 通过Colab&谷歌云下载Figshare数据,超级实用!!࿰…...
