SpringBoot接口防抖(防重復(fù)提交)的實(shí)現(xiàn)方法
概念
Spring Boot接口防抖(Debouncing)的概念是指在處理請求時(shí),通過一定的機(jī)制來防止用戶頻繁觸發(fā)同一接口請求,以防止重復(fù)提交或頻繁請求的情況發(fā)生。
在Web應(yīng)用中,用戶可能會(huì)因?yàn)榫W(wǎng)絡(luò)延遲、操作失誤或者意外多次點(diǎn)擊提交按鈕,導(dǎo)致相同的請求被發(fā)送多次,從而引發(fā)數(shù)據(jù)的重復(fù)處理或者系統(tǒng)資源的浪費(fèi)。接口防抖的目的就是在一定程度上限制這種重復(fù)請求的發(fā)生,保證系統(tǒng)的穩(wěn)定性和數(shù)據(jù)的一致性。
接口防抖通??梢酝ㄟ^以下幾種方式實(shí)現(xiàn):
- 前端防抖: 在前端頁面通過JavaScript等客戶端技術(shù)實(shí)現(xiàn),對用戶的操作進(jìn)行控制,例如利用定時(shí)器或者延遲執(zhí)行的方式來合并多個(gè)相同操作,確保只發(fā)送一次請求。
- 后端防抖: 在后端服務(wù)器端實(shí)現(xiàn),通過攔截器、過濾器等機(jī)制對相同請求的執(zhí)行頻率進(jìn)行控制,攔截并處理重復(fù)的請求,防止其繼續(xù)向下執(zhí)行。
接口防抖通常需要考慮以下幾個(gè)方面:
- 時(shí)間間隔設(shè)置: 確定兩次相同請求之間的時(shí)間間隔,即防抖的時(shí)間閾值,通常以毫秒為單位。
- 處理方式: 當(dāng)檢測到重復(fù)請求時(shí),需要確定如何處理,可以是直接忽略、返回錯(cuò)誤提示或者采取其他適當(dāng)?shù)拇胧?/li>
- 線程安全: 如果應(yīng)用是多線程的或者是分布式的,需要考慮線程安全和分布式環(huán)境下的數(shù)據(jù)共享和同步問題,確保防抖機(jī)制的正確性和可靠性。
如何確定接口是重復(fù)
確定接口是否重復(fù),一般可以通過以下幾種方式:
- 請求參數(shù)比較: 比較接口請求的參數(shù)是否完全相同。如果接口的請求參數(shù)都一致,那么可以認(rèn)為是相同的請求。
- 請求路徑和請求方法比較: 比較接口的請求路徑(URL)和請求方法(GET、POST等)是否完全相同。如果請求路徑和請求方法都一致,那么可以認(rèn)為是相同的請求。
- 請求頭比較: 比較接口的請求頭信息是否完全相同。請求頭包含了很多關(guān)于請求的元數(shù)據(jù),如用戶代理、授權(quán)信息等。如果請求頭信息完全相同,那么可以認(rèn)為是相同的請求。
- 請求體比較: 對于具有請求體的POST、PUT等請求,可以比較請求體的內(nèi)容是否完全相同。如果請求體內(nèi)容一致,那么可以認(rèn)為是相同的請求。
- IP地址和用戶標(biāo)識(shí)比較: 可以通過客戶端的IP地址和用戶標(biāo)識(shí)來判斷請求是否來自同一個(gè)客戶端。如果兩個(gè)請求具有相同的IP地址和用戶標(biāo)識(shí),那么可以認(rèn)為是相同的請求。
根據(jù)時(shí)間戳來防抖
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 時(shí)間戳防抖
*/
@Controller
@RequestMapping("/api")
public class DebounceController {
// 用于存儲(chǔ)接口請求的時(shí)間戳
private final ConcurrentHashMap<String, Long> requestTimestamps = new ConcurrentHashMap<>();
@PostMapping("/submit")
@ResponseBody
public String submit() {
// 接口路徑為"/api/submit",模擬防抖處理
String key = "/api/submit";
// 獲取當(dāng)前時(shí)間戳
long currentTimestamp = System.currentTimeMillis();
// 上一次請求的時(shí)間戳
Long lastTimestamp = requestTimestamps.get(key);
// 如果上一次請求時(shí)間不為空,并且與當(dāng)前時(shí)間間隔小于5000毫秒(5秒),則認(rèn)為是重復(fù)請求,直接返回提示
if (lastTimestamp != null && currentTimestamp - lastTimestamp < 5000) {
return "重復(fù)提交,請稍后再試!";
}
// 記錄當(dāng)前請求時(shí)間戳
requestTimestamps.put(key, currentTimestamp);
// 返回處理結(jié)果
return "提交成功!";
}
}
- 第一次提交

- 第二次提交

分布式下如何做防抖
在分布式環(huán)境下,防抖(防重復(fù)提交)需要考慮多個(gè)節(jié)點(diǎn)之間的數(shù)據(jù)同步和并發(fā)控制。以下是一種在分布式環(huán)境下實(shí)現(xiàn)防抖的方法:
- 使用分布式緩存: 可以使用分布式緩存來存儲(chǔ)接口請求的時(shí)間戳信息。常見的分布式緩存系統(tǒng)包括Redis、Memcached等。通過在緩存中存儲(chǔ)請求的時(shí)間戳,并設(shè)置適當(dāng)?shù)倪^期時(shí)間,可以實(shí)現(xiàn)簡單的防抖功能。
- 使用分布式鎖: 在處理防抖邏輯時(shí),可以使用分布式鎖來確保同一時(shí)刻只有一個(gè)節(jié)點(diǎn)可以執(zhí)行特定的代碼塊。當(dāng)某個(gè)節(jié)點(diǎn)獲取到鎖時(shí),執(zhí)行防抖邏輯并更新緩存中的時(shí)間戳信息,其他節(jié)點(diǎn)在嘗試獲取鎖時(shí)可以判斷緩存中的時(shí)間戳信息,從而避免重復(fù)提交。
分布式緩存
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中是否存在請求標(biāo)記
if (redisTemplate.hasKey(REQUEST_KEY)) {
return "重復(fù)提交,請稍后再試!";
}
// 將請求標(biāo)記寫入Redis,并設(shè)置過期時(shí)間
redisTemplate.opsForValue().set(REQUEST_KEY, "1", 5, TimeUnit.SECONDS);
// 返回處理結(jié)果
return "提交成功!";
}
}
- 第一次提交

- 第二次提交


使用了固定的鍵名"submit:request"來存儲(chǔ)接口請求的標(biāo)記,Redis中是否存在請求標(biāo)記,如果存在則認(rèn)為是重復(fù)提交,直接返回提示信息。如果不存在請求標(biāo)記,則將請求標(biāo)記寫入Redis,并設(shè)置過期時(shí)間為5秒,以確保在此時(shí)間內(nèi)同一個(gè)接口不能重復(fù)提交
分布式鎖
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; // 鎖的過期時(shí)間,單位毫秒
private static final long DEBOUNCE_TIME = 10000L; // 防抖時(shí)間,單位毫秒
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PostMapping("/redis/lock")
public String acquireLock(String key) {
String lockKey = key; // 鎖的鍵名為傳入的 key 參數(shù)
String requestId = String.valueOf(System.currentTimeMillis()); // 請求 ID 為當(dāng)前時(shí)間戳的字符串形式
/**
* Lua 腳本的作用是嘗試獲取分布式鎖。它通過 SETNX 命令嘗試在 Redis 中設(shè)置一個(gè)鍵的值,如果設(shè)置成功,則進(jìn)一步設(shè)置該鍵的過期時(shí)間,并返回 true 表示獲取鎖成功;如果設(shè)置失敗,則表示鎖已被其他客戶端獲取,返回 false 表示獲取鎖失敗。
* RedisScript<Boolean>: Spring Data Redis 提供的用于執(zhí)行 Lua 腳本的接口
* DefaultRedisScript<>(script,Boolean.class):RedisScript 的實(shí)例化操作,
* script 參數(shù)是一個(gè)字符串類型的 Lua 腳本,表示要執(zhí)行的 Redis 操作。
* Boolean.class 參數(shù)指定了腳本執(zhí)行后的返回類型為布爾值。
* if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then: Redis 的 SETNX 命令,用于在 Redis 中設(shè)置一個(gè)鍵的值,但只有在該鍵不存在時(shí)才設(shè)置成功。
* KEYS[1] 表示 Lua 腳本中傳入的鍵的數(shù)組,這里取第一個(gè)鍵。
* ARGV[1] 表示 Lua 腳本中傳入的參數(shù)的數(shù)組,這里取第一個(gè)參數(shù)。
* 如果 SETNX 返回值為 1,表示設(shè)置成功,即之前該鍵不存在,執(zhí)行 then 代碼塊中的操作。
* redis.call('PEXPIRE', KEYS[1], ARGV[2]):如果 SETNX 操作成功,接著調(diào)用了 Redis 的 PEXPIRE 命令,用于設(shè)置鍵的過期時(shí)間。
* KEYS[1] 表示要設(shè)置過期時(shí)間的鍵,
* ARGV[2] 表示傳入的第二個(gè)參數(shù),即鎖的過期時(shí)間。
* return true:如果 SETNX 操作成功,并且設(shè)置了過期時(shí)間,最終返回 Lua 腳本執(zhí)行結(jié)果為 true,表示獲取鎖成功。
* end:結(jié)束 if 條件語句塊。
* return false:如果 SETNX 操作失敗,即之前該鍵已存在,或者設(shè)置過程中出現(xiàn)異常,最終返回 Lua 腳本執(zhí)行結(jié)果為 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);
// 創(chuàng)建一個(gè)包含元素的列表,該元素時(shí)LockKey即為鎖的鍵名
List<String> keys = Collections.singletonList(lockKey);
/**
* 執(zhí)行redis的操作
* script:之前創(chuàng)建的RedisScript的對象,用于執(zhí)行Lua腳本
* keys:Lua腳本中的Keys參數(shù),即為鍵的數(shù)組,只有一個(gè)鍵,即鎖的鍵名
* requestId:Lua 腳本中的 ARGV 參數(shù),即參數(shù)的數(shù)組,傳入了請求 ID,用于標(biāo)識(shí)這次獲取鎖的請求
* String.valueOf(LOCK_EXPIRE_TIME):Lua 腳本中的 ARGV 參數(shù),即參數(shù)的數(shù)組。傳入了鎖的過期時(shí)間,以毫秒為單位
*/
Boolean result = redisTemplate.execute(script, keys, requestId, String.valueOf(LOCK_EXPIRE_TIME));
// 如果 result 不為 null,并且為真(即成功獲取了鎖)
if (result != null && result) {
try {
// 模擬處理邏輯
Thread.sleep(1000);
// 檢查是否在防抖時(shí)間內(nèi)有重復(fù)請求
if (isDuplicateRequest(key)) {
return "重復(fù)提交,請稍后再試!";
}
// 返回處理結(jié)果
return "獲取鎖成功!";
//捕獲可能發(fā)生的線程中斷異常,
} catch (InterruptedException e) {
// 將當(dāng)前線程重新標(biāo)記為中斷狀態(tài)
Thread.currentThread().interrupt();
return "獲取鎖時(shí)發(fā)生異常:" + e.getMessage();
} finally {
// 釋放鎖
releaseLock(lockKey, requestId);
}
} else {
return "獲取鎖失敗,請稍后再試!";
}
}
/**
* 防止重復(fù)請求
* @param key 鍵,即鎖的鍵名
* @return
*/
private boolean isDuplicateRequest(String key) {
// 檢查是否在防抖時(shí)間內(nèi)有重復(fù)請求
String lastRequestTime = redisTemplate.opsForValue().get("lastRequestTime:" + key); // 獲取上次請求時(shí)間
long currentTime = System.currentTimeMillis(); // 當(dāng)前時(shí)間戳
// 如果上次請求時(shí)間不為 null(即 Redis 中存在上次請求時(shí)間),且當(dāng)前時(shí)間距離上次請求時(shí)間小于防抖時(shí)間 DEBOUNCE_TIME(10000L),則認(rèn)為發(fā)生了重復(fù)請求,返回 true。
if (lastRequestTime != null && currentTime - Long.parseLong(lastRequestTime) < DEBOUNCE_TIME) { // 如果防抖時(shí)間內(nèi)有重復(fù)請求,則返回 true
return true;
} else {
// 如果沒有發(fā)生重復(fù)請求,則將當(dāng)前時(shí)間戳保存到 Redis 中,作為上次請求時(shí)間。同時(shí)設(shè)置了過期時(shí)間 DEBOUNCE_TIME(10000L),以毫秒為單位。
redisTemplate.opsForValue().set("lastRequestTime:" + key, String.valueOf(currentTime), DEBOUNCE_TIME, TimeUnit.MILLISECONDS); // 否則將當(dāng)前時(shí)間作為上次請求時(shí)間并設(shè)置過期時(shí)間,返回 false
return false;
}
}
/**
* 釋放鎖
* @param lockKey 接受鎖的鍵
* @param requestId 請求標(biāo)識(shí)作為參數(shù)
*/
private void releaseLock(String lockKey, String requestId) {
// 釋放鎖。腳本中的 KEYS[1] 和 ARGV[1] 會(huì)分別被傳入 keys 和 requestId 參數(shù)替換
String releaseLockScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
"return redis.call('DEL', KEYS[1]) " +
"else " +
"return 0 " +
"end";
// 將 Lua 腳本字符串轉(zhuǎn)換為 RedisScript 對象,指定了返回類型為 Long
RedisScript<Long> script = new DefaultRedisScript<>(releaseLockScript, Long.class);
// 創(chuàng)建了一個(gè)包含鎖鍵的列表,作為 Lua 腳本的 KEYS 參數(shù)。
List<String> keys = Collections.singletonList(lockKey);
// 執(zhí)行 Lua 腳本,傳入了腳本對象、鍵列表和請求標(biāo)識(shí)作為參數(shù),從而釋放了鎖
redisTemplate.execute(script, keys, requestId);
}
}
- 第一次訪問

- 第二次訪問


到此這篇關(guān)于SpringBoot接口防抖(防重復(fù)提交)的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)SpringBoot接口防抖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
eclipse實(shí)現(xiàn)DSA數(shù)字簽名
這篇文章主要為大家詳細(xì)介紹了eclipse實(shí)現(xiàn)DSA數(shù)字簽名算法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06
詳解如何使用java實(shí)現(xiàn)Open Addressing
這篇文章主要介紹了詳解如何使用java實(shí)現(xiàn)Open Addressing,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
Netty分布式ByteBuf使用subPage級別內(nèi)存分配剖析
這篇文章主要為大家介紹了Netty分布式ByteBuf使用subPage級別內(nèi)存分配剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
springboot表單提交之validator校驗(yàn)
在前臺(tái)表單驗(yàn)證的時(shí)候,通常會(huì)校驗(yàn)一些數(shù)據(jù)的可行性,比如是否為空,長度,身份證,郵箱等等,這篇文章主要給大家介紹了關(guān)于springboot表單提交之validator校驗(yàn)的相關(guān)資料,需要的朋友可以參考下2021-05-05

