SpringBoot接口防抖(防重复提交)

news2024/11/23 7:33:36

SpringBoot接口防抖(防重复提交)

概念

Spring Boot接口防抖(Debouncing)的概念是指在处理请求时,通过一定的机制来防止用户频繁触发同一接口请求,以防止重复提交或频繁请求的情况发生。

在Web应用中,用户可能会因为网络延迟、操作失误或者意外多次点击提交按钮,导致相同的请求被发送多次,从而引发数据的重复处理或者系统资源的浪费。接口防抖的目的就是在一定程度上限制这种重复请求的发生,保证系统的稳定性和数据的一致性。

接口防抖通常可以通过以下几种方式实现:

  1. 前端防抖: 在前端页面通过JavaScript等客户端技术实现,对用户的操作进行控制,例如利用定时器或者延迟执行的方式来合并多个相同操作,确保只发送一次请求。
  2. 后端防抖: 在后端服务器端实现,通过拦截器、过滤器等机制对相同请求的执行频率进行控制,拦截并处理重复的请求,防止其继续向下执行。

接口防抖通常需要考虑以下几个方面:

  • 时间间隔设置: 确定两次相同请求之间的时间间隔,即防抖的时间阈值,通常以毫秒为单位。
  • 处理方式: 当检测到重复请求时,需要确定如何处理,可以是直接忽略、返回错误提示或者采取其他适当的措施。
  • 线程安全: 如果应用是多线程的或者是分布式的,需要考虑线程安全和分布式环境下的数据共享和同步问题,确保防抖机制的正确性和可靠性。

如何确定接口是重复

确定接口是否重复,一般可以通过以下几种方式:

  1. 请求参数比较: 比较接口请求的参数是否完全相同。如果接口的请求参数都一致,那么可以认为是相同的请求。
  2. 请求路径和请求方法比较: 比较接口的请求路径(URL)和请求方法(GET、POST等)是否完全相同。如果请求路径和请求方法都一致,那么可以认为是相同的请求。
  3. 请求头比较: 比较接口的请求头信息是否完全相同。请求头包含了很多关于请求的元数据,如用户代理、授权信息等。如果请求头信息完全相同,那么可以认为是相同的请求。
  4. 请求体比较: 对于具有请求体的POST、PUT等请求,可以比较请求体的内容是否完全相同。如果请求体内容一致,那么可以认为是相同的请求。
  5. IP地址和用户标识比较: 可以通过客户端的IP地址和用户标识来判断请求是否来自同一个客户端。如果两个请求具有相同的IP地址和用户标识,那么可以认为是相同的请求。

根据时间戳来防抖

DebounceController.java

package com.sin.controller;// 需要先在pom.xml中添加Spring Web依赖

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @createTime 2024/6/4 11:17
 * @createAuthor SIN
 * @use 时间戳防抖
 */
@Controller
@RequestMapping("/api")
public class DebounceController {

    // 用于存储接口请求的时间戳
    private final ConcurrentHashMap<String, Long> requestTimestamps = new ConcurrentHashMap<>();

    @PostMapping("/submit")
    @ResponseBody
    public String submit() {
        // 接口路径为"/api/submit",模拟防抖处理
        String key = "/api/submit";

        // 获取当前时间戳
        long currentTimestamp = System.currentTimeMillis();

        // 上一次请求的时间戳
        Long lastTimestamp = requestTimestamps.get(key);

        // 如果上一次请求时间不为空,并且与当前时间间隔小于5000毫秒(5秒),则认为是重复请求,直接返回提示
        if (lastTimestamp != null && currentTimestamp - lastTimestamp < 5000) {
            return "重复提交,请稍后再试!";
        }

        // 记录当前请求时间戳
        requestTimestamps.put(key, currentTimestamp);

        // 返回处理结果
        return "提交成功!";
    }
}

  • 第一次提交

在这里插入图片描述

  • 第二次提交

在这里插入图片描述

分布式下如何做防抖

在分布式环境下,防抖(防重复提交)需要考虑多个节点之间的数据同步和并发控制。以下是一种在分布式环境下实现防抖的方法:

  1. 使用分布式缓存: 可以使用分布式缓存来存储接口请求的时间戳信息。常见的分布式缓存系统包括Redis、Memcached等。通过在缓存中存储请求的时间戳,并设置适当的过期时间,可以实现简单的防抖功能。
  2. 使用分布式锁: 在处理防抖逻辑时,可以使用分布式锁来确保同一时刻只有一个节点可以执行特定的代码块。当某个节点获取到锁时,执行防抖逻辑并更新缓存中的时间戳信息,其他节点在尝试获取锁时可以判断缓存中的时间戳信息,从而避免重复提交。

分布式缓存

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yml
spring:
  data:
    redis:
      host: 192.168.226.134
      password: 123456
RedisDebounceController.java
package com.sin.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @createTime 2024/6/4 11:17
 * @createAuthor SIN
 * @use 分布式缓存(Redis)防抖
 */
@RestController
@RequestMapping("/api")
public class RedisDebounceController {

    private static final String REQUEST_KEY = "submit:request";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostMapping("/redisSubmit")
    public String submit() {
        // 检查Redis中是否存在请求标记
        if (redisTemplate.hasKey(REQUEST_KEY)) {
            return "重复提交,请稍后再试!";
        }

        // 将请求标记写入Redis,并设置过期时间
        redisTemplate.opsForValue().set(REQUEST_KEY, "1", 5, TimeUnit.SECONDS);

        // 返回处理结果
        return "提交成功!";
    }
}
  • 第一次提交

在这里插入图片描述

  • 第二次提交

在这里插入图片描述

在这里插入图片描述

使用了固定的键名"submit:request"来存储接口请求的标记,Redis中是否存在请求标记,如果存在则认为是重复提交,直接返回提示信息。如果不存在请求标记,则将请求标记写入Redis,并设置过期时间为5秒,以确保在此时间内同一个接口不能重复提交

分布式锁

RedisLockDebounceController.java
package com.sin.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @createTime 2024/6/4 11:29
 * @createAuthor SIN
 * @use 使用分布式锁防抖
 */
@RestController
@RequestMapping("/api")
public class RedisLockDebounceController {
    private static final long LOCK_EXPIRE_TIME = 10000L; // 锁的过期时间,单位毫秒
    private static final long DEBOUNCE_TIME = 10000L; // 防抖时间,单位毫秒

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostMapping("/redis/lock")
    public String acquireLock(String key) {
        String lockKey = key; // 锁的键名为传入的 key 参数
        String requestId = String.valueOf(System.currentTimeMillis()); // 请求 ID 为当前时间戳的字符串形式

        /**
         * Lua 脚本的作用是尝试获取分布式锁。它通过 SETNX 命令尝试在 Redis 中设置一个键的值,如果设置成功,则进一步设置该键的过期时间,并返回 true 表示获取锁成功;如果设置失败,则表示锁已被其他客户端获取,返回 false 表示获取锁失败。
         * RedisScript<Boolean>: Spring Data Redis 提供的用于执行 Lua 脚本的接口
         * DefaultRedisScript<>(script,Boolean.class):RedisScript 的实例化操作,
         *          script 参数是一个字符串类型的 Lua 脚本,表示要执行的 Redis 操作。
         *          Boolean.class 参数指定了脚本执行后的返回类型为布尔值。
         * if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then: Redis 的 SETNX 命令,用于在 Redis 中设置一个键的值,但只有在该键不存在时才设置成功。
         *          KEYS[1] 表示 Lua 脚本中传入的键的数组,这里取第一个键。
         *          ARGV[1] 表示 Lua 脚本中传入的参数的数组,这里取第一个参数。
         *          如果 SETNX 返回值为 1,表示设置成功,即之前该键不存在,执行 then 代码块中的操作。
         * redis.call('PEXPIRE', KEYS[1], ARGV[2]):如果 SETNX 操作成功,接着调用了 Redis 的 PEXPIRE 命令,用于设置键的过期时间。
         *          KEYS[1] 表示要设置过期时间的键,
         *          ARGV[2] 表示传入的第二个参数,即锁的过期时间。
         * return true:如果 SETNX 操作成功,并且设置了过期时间,最终返回 Lua 脚本执行结果为 true,表示获取锁成功。
         * end:结束 if 条件语句块。
         * return false:如果 SETNX 操作失败,即之前该键已存在,或者设置过程中出现异常,最终返回 Lua 脚本执行结果为 false,表示获取锁失败。
         */
        RedisScript<Boolean> script = new DefaultRedisScript<>(
                "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then " +
                        "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                        "return true " +
                        "end " +
                        "return false", Boolean.class);

        // 创建一个包含元素的列表,该元素时LockKey即为锁的键名
        List<String> keys = Collections.singletonList(lockKey);
        /**
         * 执行redis的操作
         * script:之前创建的RedisScript的对象,用于执行Lua脚本
         * keys:Lua脚本中的Keys参数,即为键的数组,只有一个键,即锁的键名
         * requestId:Lua 脚本中的 ARGV 参数,即参数的数组,传入了请求 ID,用于标识这次获取锁的请求
         * String.valueOf(LOCK_EXPIRE_TIME):Lua 脚本中的 ARGV 参数,即参数的数组。传入了锁的过期时间,以毫秒为单位
         */
        Boolean result = redisTemplate.execute(script, keys, requestId, String.valueOf(LOCK_EXPIRE_TIME));

        // 如果 result 不为 null,并且为真(即成功获取了锁)
        if (result != null && result) {
            try {
                // 模拟处理逻辑
                Thread.sleep(1000);

                // 检查是否在防抖时间内有重复请求
                if (isDuplicateRequest(key)) {
                    return "重复提交,请稍后再试!";
                }

                // 返回处理结果
                return "获取锁成功!";
            //捕获可能发生的线程中断异常,
            } catch (InterruptedException e) {
                // 将当前线程重新标记为中断状态
                Thread.currentThread().interrupt();
                return "获取锁时发生异常:" + e.getMessage();
            } finally {
                // 释放锁
                releaseLock(lockKey, requestId);
            }
        } else {
            return "获取锁失败,请稍后再试!";
        }
    }

    /**
     * 防止重复请求
     * @param key 键,即锁的键名
     * @return
     */
    private boolean isDuplicateRequest(String key) {
        // 检查是否在防抖时间内有重复请求
        String lastRequestTime = redisTemplate.opsForValue().get("lastRequestTime:" + key); // 获取上次请求时间
        long currentTime = System.currentTimeMillis(); // 当前时间戳
        // 如果上次请求时间不为 null(即 Redis 中存在上次请求时间),且当前时间距离上次请求时间小于防抖时间 DEBOUNCE_TIME(10000L),则认为发生了重复请求,返回 true。
        if (lastRequestTime != null && currentTime - Long.parseLong(lastRequestTime) < DEBOUNCE_TIME) { // 如果防抖时间内有重复请求,则返回 true
            return true;
        } else {
            // 如果没有发生重复请求,则将当前时间戳保存到 Redis 中,作为上次请求时间。同时设置了过期时间 DEBOUNCE_TIME(10000L),以毫秒为单位。
            redisTemplate.opsForValue().set("lastRequestTime:" + key, String.valueOf(currentTime), DEBOUNCE_TIME, TimeUnit.MILLISECONDS); // 否则将当前时间作为上次请求时间并设置过期时间,返回 false
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey 接受锁的键
     * @param requestId 请求标识作为参数
     */
    private void releaseLock(String lockKey, String requestId) {
        // 释放锁。脚本中的 KEYS[1] 和 ARGV[1] 会分别被传入 keys 和 requestId 参数替换
        String releaseLockScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
                "return redis.call('DEL', KEYS[1]) " +
                "else " +
                "return 0 " +
                "end";
        // 将 Lua 脚本字符串转换为 RedisScript 对象,指定了返回类型为 Long
        RedisScript<Long> script = new DefaultRedisScript<>(releaseLockScript, Long.class);
        // 创建了一个包含锁键的列表,作为 Lua 脚本的 KEYS 参数。
        List<String> keys = Collections.singletonList(lockKey);
        // 执行 Lua 脚本,传入了脚本对象、键列表和请求标识作为参数,从而释放了锁
        redisTemplate.execute(script, keys, requestId);
    }
}


  • 第一次访问

在这里插入图片描述

  • 第二次访问

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1789234.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jupyter Notebook 切换虚拟环境

具体流程&#xff1a; 1. 查看当前jupyter notebook的环境&#xff1a; 从上面可看出当前jupyter只有一个python 3的环境。 2、 在终端中切换至想要添加的环境&#xff1a; 2.1 激活你要添加的环境 conda activate sf 2.2 在当前环境中安装ipykernel conda install ipyke…

手把手教你【如何使用Vue3+Spring Boot实现一个视频点播功能】

一、简介 本项目是一个实际的视频点播应用&#xff0c;采用了Vue3和Spring Boot作为主要技术栈。旨在帮助开发者通过学习和参考实现思路来掌握相关知识。它主要解决了阿里云视频点播服务的接入、视频基础信息管理以及上传视频后获取视频ID等关键流程&#xff0c;涉及前后端交互…

【最新支持】OpenCV实验大师C++ SDK支持YOLOv10了推理了

学习《OpenCV应用开发&#xff1a;入门、进阶与工程化实践》一书 做真正的OpenCV开发者&#xff0c;从入门到入职&#xff0c;一步到位&#xff01; OpenCV实验大师C SDK YOLO系列模型推理SDK 支持 YOLOv5、YOLOv8、YOLOv10系列网络从推理与部署 基于OpenCV DNN 与OpenVINO实…

Echarts 在指定部分做文字标记

文章目录 需求分析1. demo12. demo22. demo3 定位解决需求 实现在Echarts的折线图中,相同Y值的两点之间显示’abc’ 分析 1. demo1 使用 ECharts 的 markLine 功能来在相邻两个点之间添加标记。其中,我们通过设置标记的 yAxis 和 label 来控制标记的位置和显示内容。最后…

前端 CSS 经典:3D Hover Effect 效果

前言&#xff1a;有趣的 3D Hover Effect 效果&#xff0c;通过 js 监听鼠标移动&#xff0c;动态赋值 rotateX&#xff0c;rotateY 的旋转度来实现。 效果图&#xff1a; 代码实现&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta …

【NoSQL数据库】Redis简介

Redis Redis简介 Redis关系型数据库和非关系型数据库Redis 简介redis速度快的原因 Redis 配置Linux 源码安装redis命令工具 关系型数据库和非关系型数据库 关系型数据库&#xff08;Relational Database&#xff09;和非关系型数据库&#xff08;Non-Relational Database&…

在仓库新建分支之后,Vscode里面看不到

问题描述 在仓库新建了分支 但是在Vscode里面看不到这个新建的分支 解决 参考文章&#xff1a;http://t.csdnimg.cn/V92a3 在终端输入&#xff1a;git remote update origin --prune 命令解释 git remote update origin --prune 是一个 Git 命令&#xff0c;用于更新远程…

小猪APP分发:让APP封装变得如此简单

你是否曾经在开发完一款APP后&#xff0c;为了封装、分发而头疼不已&#xff1f;别担心&#xff0c;小猪APP分发来拯救你了&#xff01;这款神器不仅能让你的工作变得更加高效&#xff0c;还能让你的APP在各大平台上顺利分发。 小猪APP封装www.ppzhu.net APP封装的挑战 开发一…

Python3.9及以上Pyinstaller 反编译教程(exe转py)

文章目录 前言1.使用pyinstxtractor.py将exe文件转换成pyc文件2.给pyc文件添加文件头3.使用pycdc工具反编译pyc文件&#xff0c;获得源码 前言 经常使用pyinstaller将一些写的python程序打包成了各种exe&#xff0c;时间一长&#xff0c;源码丢失&#xff0c;为了恢复一部分源…

学习Python我能做些什么了?你真的了解了嘛?

工欲善其事&#xff0c;必先利其器。学习不是盲目的&#xff0c;是有目标性的。所以&#xff0c;在学习之前充分了解自己所学技能的前景&#xff0c;学完能做什么&#xff0c;大概地薪资待遇是很有必要的。 Python作为人工智能的重要编程语言&#xff0c;无论发展前景还是就业…

【文末附gpt升级秘笈】Suno全新功能在音乐创作领域的应用与影响

Suno全新功能在音乐创作领域的应用与影响 摘要&#xff1a; 随着科技的飞速发展&#xff0c;人工智能与音乐创作的结合日益紧密。本文旨在探讨Suno全新功能——即兴哼唱创作与声音模仿——在音乐创作领域的应用与影响。通过深入分析这一技术的原理、特点及其在音乐创作中的实际…

Linux 系统怎么快速「批量重命名」文件

如果需要对文件批量重命名&#xff0c;怎么办&#xff0c;是不是要找个工具&#xff0c;下载看这么使用。其实在 Linux、macOS 系统上使用脚本可以轻松搞定。 如&#xff0c;这里有一批图片文件&#xff0c;后缀名可能是jpg、jpeg、png 等&#xff0c;名称如 “我是待重命名的…

uniPush2.0消息推行(云对象)

1.创建uniCloud云开发环境 关联云服务空间&#xff08;没有云空间到官网上创建&#xff09;步骤如下 2. index.obj.js代码 &#xff0c;然后上传部署 // 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj // jsdoc语法提示教程&#xff1a;https://ask.dc…

6.6高算力开发套件深度解读 | 2024高通边缘智能创新应用大赛公开课

2024高通边缘智能创新应用大赛系列公开课即将迎来激动人心的收官之作&#xff01; 美格智能新技术研究院李书杰将在本次直播中带来一场关于边缘智能模组与开发技术的专业分享盛宴&#xff0c;深入剖析高算力开发套件的独特魅力和核心亮点。 锁定6月6日晚上8点&#xff0c;直播…

通配符https数字证书260

随着越来越多的人开始使用互联网&#xff0c;互联网上的信息变得繁杂&#xff0c;用户很难识别网站信息的真实性&#xff0c;为了维护互联网的环境&#xff0c;开发者开始使用https证书对网站传输数据进行加密和身份认证&#xff0c;以此来保护用户的隐私以及标示网站的真实性。…

带池化注意力 Strip Pooling | Rethinking Spatial Pooling for Scene Parsing

论文地址:https://arxiv.org/abs/2003.13328 代码地址:https://github.com/houqb/SPNet 空间池化已被证明在捕获像素级预测任务的长距离上下文信息方面非常有效,如场景解析。在本文中,我们超越了通常具有N N规则形状的常规空间池化,重新思考空间池化的构成,引入了一种…

智能分析设备助力废固运输车辆信息采集

进出车辆信息采集&#xff0c;这一环节可谓是整个废固生产及处理企业监管体系中的基石。前端摄像机以其敏锐的感知能力&#xff0c;精准捕捉废固运输车辆的车牌、车头、车尾以及车厢的细致画面&#xff0c;同时记录下对应的视频流信息。这些信息的采集不仅为后续的监管提供了详…

掘金AI 商战宝典-高阶班:如何用AI制作视频(11节视频课)

课程目录&#xff1a; 1-第一讲用AI自动做视频&#xff08;上&#xff09;_1.mp4 2-第二讲用AI自动做视频&#xff08;中&#xff09;_1.mp4 3-第四讲A1做视频实战&#xff1a;店铺宣传_1.mp4 4-第五讲Al做视频实战&#xff1a;商品带贷1.mp4 5-第六讲Al做视频实战&#x…

在618集中上新,蕉下、VVC们为何押注拼多多?

编辑&#xff5c;Ray 自前两年崛起的防晒产品&#xff0c;今年依旧热度不减。 头部品牌蕉下&#xff0c;2020年入驻拼多多&#xff0c;如今年销售额已过亿元。而自去年起重点押注拼多多的时尚防晒品牌VVC&#xff0c;很快销量翻番。这两家公司&#xff0c;不约而同在618之前上…

Windows下使用Airsim+QGC进行PX4硬件在环HITL(一)

Windows下使用AirsimQGC进行PX4硬件在环HITL This tutorial will guide you through the installation of Airsim and QGC on Windows, so that the hardware-in-the-loop experiment can be conducted. Hardware-in-the-Loop (HITL or HIL) is a simulation mode in which nor…