SpringBoot 3.3 接口防抖的一些實現方案,超贊!
在現代 Web 應用中,前端與后端的交互頻繁而復雜,用戶的操作如按鈕點擊、表單提交等,都會引發向后端發送請求。雖然這些請求中的多數是有意義的,但一些場景下,用戶的誤操作或網絡波動可能導致同一請求在短時間內被重復觸發。這種重復請求不僅會增加服務器的負擔,消耗寶貴的資源,還可能引發數據不一致性的問題。因此,如何有效地防止接口被頻繁調用,成為了開發者必須解決的問題。
接口防抖是一種常見的優化手段,通過延遲請求的處理或限制請求的頻率,來確保在一定時間內只執行一次操作,從而減少服務器負擔。本文將深入探討幾種常見的接口防抖策略及其在 Spring Boot 3.3 項目中的實現,并展示如何通過配置來靈活選擇不同的防抖方案。此外,還將介紹如何在前端使用 Jquery 實現按鈕的防抖點擊,從而進一步優化用戶體驗。
什么是接口防抖
接口防抖(Debounce)是一種前后端結合的技術手段,主要用于防止在短時間內多次觸發同一操作。通常情況下,用戶可能會因為網絡延遲、誤點擊等原因在短時間內多次發送相同的請求,如果不加以控制,這些請求會同時傳遞到服務器,導致服務器處理多次同一業務邏輯,從而造成資源浪費甚至系統崩潰。
防抖技術可以通過延遲處理或限制頻率,確保在指定時間內同一操作只被執行一次。例如,用戶在點擊按鈕時,如果短時間內多次點擊,只有第一個請求會被處理,后續請求將被忽略。這種機制不僅可以優化服務器性能,還能提升用戶體驗。
運行效果:
圖片
圖片
若想獲取項目完整代碼以及其他文章的項目源碼,且在代碼編寫時遇到問題需要咨詢交流,歡迎加入下方的知識星球。
項目基礎配置
pom.xml 配置
首先,在 pom.xml 中配置 Spring Boot、Thymeleaf、Bootstrap,以及其他相關依賴:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>debounce</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>debounce</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml 配置
在 application.yml 中配置接口防抖策略的選擇:
server:
port: 8080
spring:
debounce:
strategy: time-window # 可選值:time-window, token-bucket, sliding-window
time-window:
duration: 1000 # 時間窗口長度(毫秒)
token-bucket:
capacity: 10 # 令牌桶容量
refill-rate: 1 # 令牌補充速率(每秒)
sliding-window:
size: 5 # 滑動窗口大小
interval: 1000 # 時間間隔(毫秒)
接口防抖策略實現
定義 DebounceStrategy 接口
package com.icoderoad.debounce.strategy;
public interface DebounceStrategy {
/**
* 判斷當前請求是否應該被處理
*
* @param key 唯一標識(如用戶ID、IP等)
* @return 如果應該處理請求,返回true;否則返回false
*/
boolean shouldProceed(String key);
}
時間窗口防抖(time-window)
時間窗口防抖策略(Time Window Debounce)是最簡單的一種防抖機制,它允許在一個固定的時間窗口內只執行一次操作。例如,在設置了 1 秒的時間窗口后,無論用戶在這一秒內點擊多少次按鈕,系統只會響應第一次點擊,其余的點擊將被忽略。時間窗口策略適用于那些短時間內不希望重復操作的場景。
時間窗口策略實現
首先,實現時間窗口策略:
package com.icoderoad.debounce.strategy;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("timeWindowStrategy")
public class TimeWindowStrategy implements DebounceStrategy {
private final long durationMillis;
private final ConcurrentHashMap<String, Long> requestTimes = new ConcurrentHashMap<>();
public TimeWindowStrategy(@Value("${spring.debounce.time-window.duration}") long durationMillis) {
this.durationMillis = durationMillis;
}
@Override
public boolean shouldProceed(String key) {
long currentTime = System.currentTimeMillis();
Long lastRequestTime = requestTimes.put(key, currentTime);
if (lastRequestTime == null || currentTime - lastRequestTime >= durationMillis) {
return true;
} else {
return false;
}
}
}
令牌桶防抖(token-bucket)
令牌桶防抖策略(Token Bucket Debounce)通過維護一個令牌桶,每次請求需要消耗一個令牌。當令牌桶為空時,請求將被拒絕或延遲處理。令牌會以固定的速率被重新生成,確保在長時間內的請求可以被平穩處理。令牌桶策略適用于那些需要控制請求速率的場景,如 API 限流。
令牌桶策略實現
package com.icoderoad.debounce.strategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
@Component("tokenBucketStrategy")
public class TokenBucketStrategy implements DebounceStrategy {
private final int capacity;
private final int refillRate;
private final ConcurrentHashMap<String, Semaphore> tokenBuckets = new ConcurrentHashMap<>();
public TokenBucketStrategy(
@Value("${spring.debounce.token-bucket.capacity}") int capacity,
@Value("${spring.debounce.token-bucket.refill-rate}") int refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
startRefillTask();
}
@Override
public boolean shouldProceed(String key) {
Semaphore semaphore = tokenBuckets.computeIfAbsent(key, k -> new Semaphore(capacity));
return semaphore.tryAcquire();
}
private void startRefillTask() {
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000 / refillRate);
tokenBuckets.forEach((key, semaphore) -> semaphore.release(1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}
滑動窗口防抖(sliding-window)
滑動窗口防抖策略(Sliding Window Debounce)通過在一個固定的時間窗口內統計請求次數,并將其滑動以覆蓋整個時間區間。只允許在這個窗口內的一定次數的請求通過?;瑒哟翱诓呗赃m用于需要在一段時間內精確控制請求次數的場景。
滑動窗口策略實現
package com.icoderoad.debounce.strategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
@Component("slidingWindowStrategy")
public class SlidingWindowStrategy implements DebounceStrategy {
private final int maxSize;
private final long intervalMillis;
private final ConcurrentHashMap<String, LinkedBlockingQueue<Long>> requestTimestamps = new ConcurrentHashMap<>();
public SlidingWindowStrategy(
@Value("${spring.debounce.sliding-window.size}") int maxSize,
@Value("${spring.debounce.sliding-window.interval}") long intervalMillis) {
this.maxSize = maxSize;
this.intervalMillis = intervalMillis;
}
@Override
public boolean shouldProceed(String key) {
long currentTime = System.currentTimeMillis();
LinkedBlockingQueue<Long> timestamps = requestTimestamps.computeIfAbsent(key, k -> new LinkedBlockingQueue<>());
synchronized (timestamps) {
while (!timestamps.isEmpty() && currentTime - timestamps.peek() > intervalMillis) {
timestamps.poll();
}
if (timestamps.size() < maxSize) {
timestamps.offer(currentTime);
return true;
} else {
return false;
}
}
}
}
策略選擇器
package com.icoderoad.debounce.strategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class DebounceStrategySelector {
@Value("${spring.debounce.strategy}")
private String strategy;
private final TimeWindowStrategy timeWindowStrategy;
private final TokenBucketStrategy tokenBucketStrategy;
private final SlidingWindowStrategy slidingWindowStrategy;
@Autowired
public DebounceStrategySelector(
TimeWindowStrategy timeWindowStrategy,
TokenBucketStrategy tokenBucketStrategy,
SlidingWindowStrategy slidingWindowStrategy) {
this.timeWindowStrategy = timeWindowStrategy;
this.tokenBucketStrategy = tokenBucketStrategy;
this.slidingWindowStrategy = slidingWindowStrategy;
}
public DebounceStrategy select() {
switch (strategy) {
case "token-bucket":
return tokenBucketStrategy;
case "sliding-window":
return slidingWindowStrategy;
case "time-window":
default:
return timeWindowStrategy;
}
}
}
自定義注解
package com.icoderoad.debounce.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Debounce {
String key() default "";
long duration() default 1000;
}
AOP 實現
將防抖策略的選擇集成到 AOP 中:
package com.icoderoad.debounce.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.icoderoad.debounce.strategy.DebounceStrategy;
import com.icoderoad.debounce.strategy.DebounceStrategySelector;
import com.icoderoad.debounce.strategy.SlidingWindowStrategy;
import com.icoderoad.debounce.strategy.TimeWindowStrategy;
import com.icoderoad.debounce.strategy.TokenBucketStrategy;
@Aspect
@Component
public class DebounceAspect {
private final DebounceStrategySelector strategySelector;
@Autowired
public DebounceAspect(DebounceStrategySelector strategySelector) {
this.strategySelector = strategySelector;
}
@Around("@annotation(debounce)")
public Object around(ProceedingJoinPoint joinPoint, Debounce debounce) throws Throwable {
DebounceStrategy strategy = strategySelector.select();
String key = debounce.key();
if (strategy instanceof TimeWindowStrategy) {
if (((TimeWindowStrategy) strategy).shouldProceed(key)) {
return joinPoint.proceed();
}
} else if (strategy instanceof TokenBucketStrategy) {
if (((TokenBucketStrategy) strategy).shouldProceed(key)) {
return joinPoint.proceed();
}
} else if (strategy instanceof SlidingWindowStrategy) {
if (((SlidingWindowStrategy) strategy).shouldProceed(key)) {
return joinPoint.proceed();
}
}
throw new RuntimeException("請求頻率過高,請稍后再試。");
}
}
自定義異常類
首先,可以定義一個自定義異常類 TooManyRequestsException:
package com.icoderoad.debounce.exception;
public class TooManyRequestsException extends RuntimeException {
public TooManyRequestsException(String message) {
super(message);
}
}
創建錯誤響應體
定義一個簡單的 ErrorResponse 類,用于返回錯誤信息:
package com.icoderoad.debounce.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
private String message;
}
創建自定義異常處理
使用 @ControllerAdvice 和 @ExceptionHandler 處理 TooManyRequestsException 異常,并返回自定義的響應體和狀態碼:
package com.icoderoad.debounce.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import com.icoderoad.debounce.exception.ErrorResponse;
import com.icoderoad.debounce.exception.TooManyRequestsException;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TooManyRequestsException.class)
public ResponseEntity<Object> handleTooManyRequestsException(TooManyRequestsException ex) {
// 返回 429 狀態碼和自定義錯誤信息
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.body(new ErrorResponse("請求頻率過高,請稍后再試!"));
}
}
接口控制器實現 DebounceController
package com.icoderoad.debounce.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.icoderoad.debounce.aspect.Debounce;
@RestController
public class DebounceController {
@Debounce(key = "debounceTest")
@GetMapping("/api/debounce-test")
public ResponseEntity<String> debounceTest() {
return ResponseEntity.ok("請求成功!");
}
}
視圖控制器
package com.icoderoad.debounce.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
前端實現
前端使用 Bootstrap 實現一個簡單的按鈕觸發接口調用的頁面,并展示防抖效果。
在src/main/resources/templates目錄下創建文件 index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>接口防抖測試</title>
<!-- 使用HTTP協議引入Bootstrap和jQuery -->
<link rel="stylesheet" >
<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4">接口防抖測試</h1>
<!-- 按鈕,點擊后觸發請求 -->
<button id="debounceButton" class="btn btn-primary btn-lg">觸發請求</button>
<!-- 用于顯示響應結果的div -->
<div id="responseMessage" class="mt-3 alert" style="display:none;"></div>
</div>
<!-- 引入Bootstrap的JS文件 -->
<script src="http://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
<script>
/**
* 防抖函數
* @param func 需要防抖的函數
* @param delay 延遲時間,單位毫秒
* @returns {Function}
*/
function debounce(func, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
};
}
$(document).ready(function () {
const $button = $('#debounceButton');
const $responseDiv = $('#responseMessage');
/**
* 發送請求的函數
*/
function sendRequest() {
$.ajax({
url: '/api/test',
method: 'GET',
success: function(data) {
$responseDiv
.removeClass('alert-danger')
.addClass('alert-success')
.text(data)
.show();
},
error: function(xhr) {
$responseDiv
.removeClass('alert-success')
.addClass('alert-danger')
.text('請求過于頻繁,請稍后再試!')
.show();
}
});
}
// 使用防抖函數包裝sendRequest,防止頻繁點擊
const debouncedSendRequest = debounce(sendRequest, 500); // 500毫秒內只執行一次
$button.on('click', debouncedSendRequest);
});
</script>
</body>
</html>
總結
在本文中,我們深入探討了幾種常見的接口防抖策略——時間窗口、令牌桶、滑動窗口,并展示了如何在 Spring Boot 3.3 項目中實現這些策略。通過配置文件的靈活選擇,可以在不同場景下使用不同的防抖策略,從而優化系統的性能和穩定性。此外,我們還介紹了如何在前端頁面中使用 Jquery 實現按鈕的防抖點擊,進一步防止用戶重復操作。通過這些防抖手段,可以有效地降低系統的負載,提高用戶體驗,避免潛在的系統風險。