java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot接口防抖

SpringBoot接口防抖(防重复提交)的实现方法

作者:陆卿之SIN

SpringBoot接口防抖主要通过前端和后端两种方式实现,前端通过JavaScript控制用户操作,后端通过拦截器、过滤器等机制控制请求频率,文中介绍的非常详细,感兴趣的可以了解一下

概念

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

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

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

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

如何确定接口是重复

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

根据时间戳来防抖

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 "提交成功!";
    }
}

在这里插入图片描述

在这里插入图片描述

分布式下如何做防抖

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

分布式缓存

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);
    }
}


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

到此这篇关于SpringBoot接口防抖(防重复提交)的实现方法的文章就介绍到这了,更多相关SpringBoot接口防抖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文