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