SpringBoot接口防抖(防重復(fù)提交)的實(shí)現(xiàn)方法
概念
Spring Boot接口防抖(Debouncing)的概念是指在處理請求時,通過一定的機(jī)制來防止用戶頻繁觸發(fā)同一接口請求,以防止重復(fù)提交或頻繁請求的情況發(fā)生。
在Web應(yīng)用中,用戶可能會因?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)行控制,例如利用定時器或者延遲執(zhí)行的方式來合并多個相同操作,確保只發(fā)送一次請求。
- 后端防抖: 在后端服務(wù)器端實(shí)現(xiàn),通過攔截器、過濾器等機(jī)制對相同請求的執(zhí)行頻率進(jìn)行控制,攔截并處理重復(fù)的請求,防止其繼續(xù)向下執(zhí)行。
接口防抖通常需要考慮以下幾個方面:
- 時間間隔設(shè)置: 確定兩次相同請求之間的時間間隔,即防抖的時間閾值,通常以毫秒為單位。
- 處理方式: 當(dāng)檢測到重復(fù)請求時,需要確定如何處理,可以是直接忽略、返回錯誤提示或者采取其他適當(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)識比較: 可以通過客戶端的IP地址和用戶標(biāo)識來判斷請求是否來自同一個客戶端。如果兩個請求具有相同的IP地址和用戶標(biāo)識,那么可以認(rèn)為是相同的請求。
根據(jù)時間戳來防抖
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"; // 獲取當(dāng)前時間戳 long currentTimestamp = System.currentTimeMillis(); // 上一次請求的時間戳 Long lastTimestamp = requestTimestamps.get(key); // 如果上一次請求時間不為空,并且與當(dāng)前時間間隔小于5000毫秒(5秒),則認(rèn)為是重復(fù)請求,直接返回提示 if (lastTimestamp != null && currentTimestamp - lastTimestamp < 5000) { return "重復(fù)提交,請稍后再試!"; } // 記錄當(dāng)前請求時間戳 requestTimestamps.put(key, currentTimestamp); // 返回處理結(jié)果 return "提交成功!"; } }
- 第一次提交
- 第二次提交
分布式下如何做防抖
在分布式環(huán)境下,防抖(防重復(fù)提交)需要考慮多個節(jié)點(diǎn)之間的數(shù)據(jù)同步和并發(fā)控制。以下是一種在分布式環(huán)境下實(shí)現(xiàn)防抖的方法:
- 使用分布式緩存: 可以使用分布式緩存來存儲接口請求的時間戳信息。常見的分布式緩存系統(tǒng)包括Redis、Memcached等。通過在緩存中存儲請求的時間戳,并設(shè)置適當(dāng)?shù)倪^期時間,可以實(shí)現(xiàn)簡單的防抖功能。
- 使用分布式鎖: 在處理防抖邏輯時,可以使用分布式鎖來確保同一時刻只有一個節(jié)點(diǎn)可以執(zhí)行特定的代碼塊。當(dāng)某個節(jié)點(diǎn)獲取到鎖時,執(zhí)行防抖邏輯并更新緩存中的時間戳信息,其他節(jié)點(diǎn)在嘗試獲取鎖時可以判斷緩存中的時間戳信息,從而避免重復(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è)置過期時間 redisTemplate.opsForValue().set(REQUEST_KEY, "1", 5, TimeUnit.SECONDS); // 返回處理結(jié)果 return "提交成功!"; } }
- 第一次提交
- 第二次提交
使用了固定的鍵名"submit:request"來存儲接口請求的標(biāo)記,Redis中是否存在請求標(biāo)記,如果存在則認(rèn)為是重復(fù)提交,直接返回提示信息。如果不存在請求標(biāo)記,則將請求標(biāo)記寫入Redis,并設(shè)置過期時間為5秒,以確保在此時間內(nèi)同一個接口不能重復(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; // 鎖的過期時間,單位毫秒 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 參數(shù) String requestId = String.valueOf(System.currentTimeMillis()); // 請求 ID 為當(dāng)前時間戳的字符串形式 /** * Lua 腳本的作用是嘗試獲取分布式鎖。它通過 SETNX 命令嘗試在 Redis 中設(shè)置一個鍵的值,如果設(shè)置成功,則進(jìn)一步設(shè)置該鍵的過期時間,并返回 true 表示獲取鎖成功;如果設(shè)置失敗,則表示鎖已被其他客戶端獲取,返回 false 表示獲取鎖失敗。 * RedisScript<Boolean>: Spring Data Redis 提供的用于執(zhí)行 Lua 腳本的接口 * DefaultRedisScript<>(script,Boolean.class):RedisScript 的實(shí)例化操作, * script 參數(shù)是一個字符串類型的 Lua 腳本,表示要執(zhí)行的 Redis 操作。 * Boolean.class 參數(shù)指定了腳本執(zhí)行后的返回類型為布爾值。 * if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then: Redis 的 SETNX 命令,用于在 Redis 中設(shè)置一個鍵的值,但只有在該鍵不存在時才設(shè)置成功。 * KEYS[1] 表示 Lua 腳本中傳入的鍵的數(shù)組,這里取第一個鍵。 * ARGV[1] 表示 Lua 腳本中傳入的參數(shù)的數(shù)組,這里取第一個參數(shù)。 * 如果 SETNX 返回值為 1,表示設(shè)置成功,即之前該鍵不存在,執(zhí)行 then 代碼塊中的操作。 * redis.call('PEXPIRE', KEYS[1], ARGV[2]):如果 SETNX 操作成功,接著調(diào)用了 Redis 的 PEXPIRE 命令,用于設(shè)置鍵的過期時間。 * KEYS[1] 表示要設(shè)置過期時間的鍵, * ARGV[2] 表示傳入的第二個參數(shù),即鎖的過期時間。 * return true:如果 SETNX 操作成功,并且設(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)建一個包含元素的列表,該元素時LockKey即為鎖的鍵名 List<String> keys = Collections.singletonList(lockKey); /** * 執(zhí)行redis的操作 * script:之前創(chuàng)建的RedisScript的對象,用于執(zhí)行Lua腳本 * keys:Lua腳本中的Keys參數(shù),即為鍵的數(shù)組,只有一個鍵,即鎖的鍵名 * requestId:Lua 腳本中的 ARGV 參數(shù),即參數(shù)的數(shù)組,傳入了請求 ID,用于標(biāo)識這次獲取鎖的請求 * String.valueOf(LOCK_EXPIRE_TIME):Lua 腳本中的 ARGV 參數(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); // 檢查是否在防抖時間內(nèi)有重復(fù)請求 if (isDuplicateRequest(key)) { return "重復(fù)提交,請稍后再試!"; } // 返回處理結(jié)果 return "獲取鎖成功!"; //捕獲可能發(fā)生的線程中斷異常, } catch (InterruptedException e) { // 將當(dāng)前線程重新標(biāo)記為中斷狀態(tài) Thread.currentThread().interrupt(); return "獲取鎖時發(fā)生異常:" + e.getMessage(); } finally { // 釋放鎖 releaseLock(lockKey, requestId); } } else { return "獲取鎖失敗,請稍后再試!"; } } /** * 防止重復(fù)請求 * @param key 鍵,即鎖的鍵名 * @return */ private boolean isDuplicateRequest(String key) { // 檢查是否在防抖時間內(nèi)有重復(fù)請求 String lastRequestTime = redisTemplate.opsForValue().get("lastRequestTime:" + key); // 獲取上次請求時間 long currentTime = System.currentTimeMillis(); // 當(dāng)前時間戳 // 如果上次請求時間不為 null(即 Redis 中存在上次請求時間),且當(dāng)前時間距離上次請求時間小于防抖時間 DEBOUNCE_TIME(10000L),則認(rèn)為發(fā)生了重復(fù)請求,返回 true。 if (lastRequestTime != null && currentTime - Long.parseLong(lastRequestTime) < DEBOUNCE_TIME) { // 如果防抖時間內(nèi)有重復(fù)請求,則返回 true return true; } else { // 如果沒有發(fā)生重復(fù)請求,則將當(dāng)前時間戳保存到 Redis 中,作為上次請求時間。同時設(shè)置了過期時間 DEBOUNCE_TIME(10000L),以毫秒為單位。 redisTemplate.opsForValue().set("lastRequestTime:" + key, String.valueOf(currentTime), DEBOUNCE_TIME, TimeUnit.MILLISECONDS); // 否則將當(dāng)前時間作為上次請求時間并設(shè)置過期時間,返回 false return false; } } /** * 釋放鎖 * @param lockKey 接受鎖的鍵 * @param requestId 請求標(biāo)識作為參數(shù) */ private void releaseLock(String lockKey, String requestId) { // 釋放鎖。腳本中的 KEYS[1] 和 ARGV[1] 會分別被傳入 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)建了一個包含鎖鍵的列表,作為 Lua 腳本的 KEYS 參數(shù)。 List<String> keys = Collections.singletonList(lockKey); // 執(zhí)行 Lua 腳本,傳入了腳本對象、鍵列表和請求標(biāo)識作為參數(shù),從而釋放了鎖 redisTemplate.execute(script, keys, requestId); } }
- 第一次訪問
- 第二次訪問
到此這篇關(guān)于SpringBoot接口防抖(防重復(fù)提交)的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)SpringBoot接口防抖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用LambdaQueryWrapper動態(tài)加過濾條件?動態(tài)Lambda
這篇文章主要介紹了使用LambdaQueryWrapper動態(tài)加過濾條件?動態(tài)Lambda,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教。2022-01-01詳解Java如何在CompletableFuture中實(shí)現(xiàn)日志記錄
這篇文章主要為大家詳細(xì)介紹了一種slf4j自帶的MDC類,來記錄完整的請求日志,和在CompletableFuture異步線程中如何保留鏈路id,需要的可以參考一下2023-04-04詳解SpringCloud Gateway之過濾器GatewayFilter
這篇文章主要介紹了詳解SpringCloud Gateway之過濾器GatewayFilter,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10Mybatis Plus 代碼生成器的實(shí)現(xiàn)
這篇文章主要介紹了Mybatis Plus 代碼生成器的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03JAVA通過Filter實(shí)現(xiàn)允許服務(wù)跨域請求的方法
這里的域指的是這樣的一個概念:我們認(rèn)為若協(xié)議 + 域名 + 端口號均相同,那么就是同域即我們常說的瀏覽器請求的同源策略。這篇文章主要介紹了JAVA通過Filter實(shí)現(xiàn)允許服務(wù)跨域請求,需要的朋友可以參考下2018-11-11spring中定時任務(wù)taskScheduler的詳細(xì)介紹
這篇文章主要介紹了spring中定時任務(wù)taskScheduler的相關(guān)資料,文中通過示例代碼介紹的很詳細(xì),相信對大家具有一定的參考價值,有需要的朋友們下面來一起看看吧。2017-02-02史上最全最強(qiáng)SpringMVC詳細(xì)示例實(shí)戰(zhàn)教程(圖文)
這篇文章主要介紹了史上最全最強(qiáng)SpringMVC詳細(xì)示例實(shí)戰(zhàn)教程(圖文),需要的朋友可以參考下2016-12-12簡單說說Java SE、Java EE、Java ME三者之間的區(qū)別
本篇文章小編就為大家簡單說說Java SE、Java EE、Java ME三者之間的區(qū)別。需要的朋友可以過來參考下,希望對大家有所幫助2013-10-10java用重定向方法從文件中讀入或?qū)懭霐?shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了用重定向方法從文件中讀入或?qū)懭霐?shù)據(jù),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03