SpringBoot接口防抖(防重復提交)的實現方法
概念
Spring Boot接口防抖(Debouncing)的概念是指在處理請求時,通過一定的機制來防止用戶頻繁觸發(fā)同一接口請求,以防止重復提交或頻繁請求的情況發(fā)生。
在Web應用中,用戶可能會因為網絡延遲、操作失誤或者意外多次點擊提交按鈕,導致相同的請求被發(fā)送多次,從而引發(fā)數據的重復處理或者系統(tǒng)資源的浪費。接口防抖的目的就是在一定程度上限制這種重復請求的發(fā)生,保證系統(tǒng)的穩(wěn)定性和數據的一致性。
接口防抖通??梢酝ㄟ^以下幾種方式實現:
- 前端防抖: 在前端頁面通過JavaScript等客戶端技術實現,對用戶的操作進行控制,例如利用定時器或者延遲執(zhí)行的方式來合并多個相同操作,確保只發(fā)送一次請求。
- 后端防抖: 在后端服務器端實現,通過攔截器、過濾器等機制對相同請求的執(zhí)行頻率進行控制,攔截并處理重復的請求,防止其繼續(xù)向下執(zhí)行。
接口防抖通常需要考慮以下幾個方面:
- 時間間隔設置: 確定兩次相同請求之間的時間間隔,即防抖的時間閾值,通常以毫秒為單位。
- 處理方式: 當檢測到重復請求時,需要確定如何處理,可以是直接忽略、返回錯誤提示或者采取其他適當的措施。
- 線程安全: 如果應用是多線程的或者是分布式的,需要考慮線程安全和分布式環(huán)境下的數據共享和同步問題,確保防抖機制的正確性和可靠性。
如何確定接口是重復
確定接口是否重復,一般可以通過以下幾種方式:
- 請求參數比較: 比較接口請求的參數是否完全相同。如果接口的請求參數都一致,那么可以認為是相同的請求。
- 請求路徑和請求方法比較: 比較接口的請求路徑(URL)和請求方法(GET、POST等)是否完全相同。如果請求路徑和請求方法都一致,那么可以認為是相同的請求。
- 請求頭比較: 比較接口的請求頭信息是否完全相同。請求頭包含了很多關于請求的元數據,如用戶代理、授權信息等。如果請求頭信息完全相同,那么可以認為是相同的請求。
- 請求體比較: 對于具有請求體的POST、PUT等請求,可以比較請求體的內容是否完全相同。如果請求體內容一致,那么可以認為是相同的請求。
- 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 "提交成功!"; } }
- 第一次提交
- 第二次提交
分布式下如何做防抖
在分布式環(huán)境下,防抖(防重復提交)需要考慮多個節(jié)點之間的數據同步和并發(fā)控制。以下是一種在分布式環(huán)境下實現防抖的方法:
- 使用分布式緩存: 可以使用分布式緩存來存儲接口請求的時間戳信息。常見的分布式緩存系統(tǒng)包括Redis、Memcached等。通過在緩存中存儲請求的時間戳,并設置適當的過期時間,可以實現簡單的防抖功能。
- 使用分布式鎖: 在處理防抖邏輯時,可以使用分布式鎖來確保同一時刻只有一個節(jié)點可以執(zhí)行特定的代碼塊。當某個節(jié)點獲取到鎖時,執(zhí)行防抖邏輯并更新緩存中的時間戳信息,其他節(jié)點在嘗試獲取鎖時可以判斷緩存中的時間戳信息,從而避免重復提交。
分布式緩存
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 提供的用于執(zhí)行 Lua 腳本的接口 * DefaultRedisScript<>(script,Boolean.class):RedisScript 的實例化操作, * script 參數是一個字符串類型的 Lua 腳本,表示要執(zhí)行的 Redis 操作。 * Boolean.class 參數指定了腳本執(zhí)行后的返回類型為布爾值。 * if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then: Redis 的 SETNX 命令,用于在 Redis 中設置一個鍵的值,但只有在該鍵不存在時才設置成功。 * KEYS[1] 表示 Lua 腳本中傳入的鍵的數組,這里取第一個鍵。 * ARGV[1] 表示 Lua 腳本中傳入的參數的數組,這里取第一個參數。 * 如果 SETNX 返回值為 1,表示設置成功,即之前該鍵不存在,執(zhí)行 then 代碼塊中的操作。 * redis.call('PEXPIRE', KEYS[1], ARGV[2]):如果 SETNX 操作成功,接著調用了 Redis 的 PEXPIRE 命令,用于設置鍵的過期時間。 * KEYS[1] 表示要設置過期時間的鍵, * ARGV[2] 表示傳入的第二個參數,即鎖的過期時間。 * return true:如果 SETNX 操作成功,并且設置了過期時間,最終返回 Lua 腳本執(zhí)行結果為 true,表示獲取鎖成功。 * end:結束 if 條件語句塊。 * return false:如果 SETNX 操作失敗,即之前該鍵已存在,或者設置過程中出現異常,最終返回 Lua 腳本執(zhí)行結果為 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)建一個包含元素的列表,該元素時LockKey即為鎖的鍵名 List<String> keys = Collections.singletonList(lockKey); /** * 執(zhí)行redis的操作 * script:之前創(chuàng)建的RedisScript的對象,用于執(zhí)行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 "獲取鎖成功!"; //捕獲可能發(fā)生的線程中斷異常, } catch (InterruptedException e) { // 將當前線程重新標記為中斷狀態(tài) Thread.currentThread().interrupt(); return "獲取鎖時發(fā)生異常:" + 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),則認為發(fā)生了重復請求,返回 true。 if (lastRequestTime != null && currentTime - Long.parseLong(lastRequestTime) < DEBOUNCE_TIME) { // 如果防抖時間內有重復請求,則返回 true return true; } else { // 如果沒有發(fā)生重復請求,則將當前時間戳保存到 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); // 創(chuàng)建了一個包含鎖鍵的列表,作為 Lua 腳本的 KEYS 參數。 List<String> keys = Collections.singletonList(lockKey); // 執(zhí)行 Lua 腳本,傳入了腳本對象、鍵列表和請求標識作為參數,從而釋放了鎖 redisTemplate.execute(script, keys, requestId); } }
- 第一次訪問
- 第二次訪問
到此這篇關于SpringBoot接口防抖(防重復提交)的實現方法的文章就介紹到這了,更多相關SpringBoot接口防抖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
使用LambdaQueryWrapper動態(tài)加過濾條件?動態(tài)Lambda
這篇文章主要介紹了使用LambdaQueryWrapper動態(tài)加過濾條件?動態(tài)Lambda,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教。2022-01-01詳解Java如何在CompletableFuture中實現日志記錄
這篇文章主要為大家詳細介紹了一種slf4j自帶的MDC類,來記錄完整的請求日志,和在CompletableFuture異步線程中如何保留鏈路id,需要的可以參考一下2023-04-04詳解SpringCloud Gateway之過濾器GatewayFilter
這篇文章主要介紹了詳解SpringCloud Gateway之過濾器GatewayFilter,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10史上最全最強SpringMVC詳細示例實戰(zhàn)教程(圖文)
這篇文章主要介紹了史上最全最強SpringMVC詳細示例實戰(zhàn)教程(圖文),需要的朋友可以參考下2016-12-12簡單說說Java SE、Java EE、Java ME三者之間的區(qū)別
本篇文章小編就為大家簡單說說Java SE、Java EE、Java ME三者之間的區(qū)別。需要的朋友可以過來參考下,希望對大家有所幫助2013-10-10