Java接口防抖/冪等性解決方案(redis)
一、核心區(qū)別
特性 | 接口防抖(Debouncing) | 接口冪等性(Idempotency) |
---|---|---|
目的 | 減少資源浪費:防止短時間內(nèi)多次觸發(fā)同一操作(如用戶頻繁點擊、網(wǎng)絡抖動導致重復請求)。 | 保證結(jié)果一致性:確保同一請求無論調(diào)用一次還是多次,最終結(jié)果相同,避免重復操作導致的數(shù)據(jù)異常。 |
作用層面 | 前端/后端均可實現(xiàn):前端優(yōu)化用戶體驗,后端過濾重復請求。 | 后端核心邏輯:依賴業(yè)務邏輯和數(shù)據(jù)層設(shè)計,確保操作的唯一性。 |
關(guān)注點 | 時間窗口內(nèi)的重復請求:只處理最后一次或首次請求。 | 請求的唯一性標識:通過唯一標識符(如請求ID、業(yè)務參數(shù))判斷是否重復。 |
典型場景 | 用戶搜索輸入、按鈕多次點擊、無限滾動加載。 | 支付接口、訂單創(chuàng)建、數(shù)據(jù)修改等需避免重復操作的場景。 |
二、實現(xiàn)方式
接口防抖:
核心思想:在指定時間窗口內(nèi),僅允許最后一次(或首次)請求生效。
1.前端畫面每次請求添加loading遮罩層(接口響應時間過長就會導致用戶體驗不好)
2.使用redis每次將請求主要參數(shù)和請求人綁定起來,放入指定的緩存時間,第二次再請求看到是同一個接口和同一個人操作則提示:操作頻繁,稍后重試!
(推薦,做成自定義注解的方式,實現(xiàn)簡單)
3.前端發(fā)送請求時,在指定時間窗口內(nèi),延遲發(fā)送請求
(不推薦,畢竟會延遲發(fā)送請求,影響接口速度)
let timeout; function handleSearchInput(event) { clearTimeout(timeout); timeout = setTimeout(() => { // 發(fā)送請求 fetch('/search', { query: event.target.value }); }, 300); // 300ms防抖間隔 }
接下來聊聊第二種方式,自定義注解:
1.AOP (攔截請求,并獲取請求具體信息,將url,接口主要參數(shù),用戶id存入Redis中)
package com.qeoten.sms.edu.config; import com.qeoten.sms.util.api.R; import com.qeoten.sms.util.auth.AuthUtil; import com.qeoten.sms.util.util.DigestUtil; import com.qeoten.sms.util.util.RedisUtil; import io.lettuce.core.dynamic.support.ReflectionUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.concurrent.TimeUnit; /** * 接口防抖aop */ @Aspect @Component @Slf4j public class AntiShakeAOP { @Autowired private RedisUtil redisUtil; private static final String prefix = "RepeatSubmit"; @Around(value = "@annotation(com.qeoten.sms.edu.config.RepeatClick)") public Object antiShake(ProceedingJoinPoint pjp) throws Throwable { // 獲取調(diào)用方法的信息和簽名信息 MethodSignature signature = (MethodSignature) pjp.getSignature(); // 獲取方法 Method method = signature.getMethod(); // 獲取注解中的參數(shù) RepeatClick annotation = method.getAnnotation(RepeatClick.class); String key = getLockKey(pjp); // 查詢redis中是否存在對應關(guān)系 if (!redisUtil.hasKey(key)) { redisUtil.setKeyAndExpire(key, null, annotation.value(), TimeUnit.MILLISECONDS); return pjp.proceed(); } else { log.error(annotation.message()); return R.fail(annotation.message()); } } public static String getLockKey(ProceedingJoinPoint joinPoint) { //獲取連接點的方法簽名對象 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); //Method對象 Method method = methodSignature.getMethod(); String className = method.getDeclaringClass().getName(); //獲取Method對象上的注解對象 //獲取方法參數(shù) final Object[] args = joinPoint.getArgs(); //獲取Method對象上所有的注解 final Parameter[] parameters = method.getParameters(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < parameters.length; i++) { final RepeatClick keyParam = parameters[i].getAnnotation(RepeatClick.class); if (keyParam == null) { //如果屬性不是RepeatSubmit注解,則獲取方法的參數(shù)名 sb.append(args[i]).append("&"); } else { final Object object = args[i]; //獲取注解類中所有的屬性字段 final Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { //判斷字段上是否有RepeatSubmit注解 final RepeatClick annotation = field.getAnnotation(RepeatClick.class); //如果沒有,跳過 if (annotation == null) { continue; } //如果有,設(shè)置Accessible為true(為true時可以使用反射訪問私有變量,否則不能訪問私有變量) field.setAccessible(true); //如果屬性是RepeatSubmit注解,則拼接 連接符" & + RepeatSubmit" sb.append(ReflectionUtils.getField(field, object)).append("&"); } } } //返回指定前綴的key return prefix + ":" + className + ":" + method.getName() + ":" + AuthUtil.getUserId() + ":" + DigestUtil.md5Hex((sb.toString())); } }
2.自定義注解模板(配置緩存時間,和指定提示消息)
package com.qeoten.sms.edu.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author QT-PC-0021 */ @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatClick { /** * 默認的防抖時間ms * * @return */ long value() default 1000; String message() default "操作太頻繁,請稍后再試!"; }
3.在需要進行操作表的接口上,添加自定義注解,實現(xiàn)功能
@GetMapping("/advancePaper") @ApiOperationSupport(order = 2) @ApiOperation(value = "交卷", notes = "傳入考試id") @RepeatClick public R<MyExamVo> advancePaper(@RequestParam Long examId){ // 接口邏輯,可能頻繁操作表 }
接口冪等性:
核心思想:通過唯一標識符(如請求ID、業(yè)務參數(shù))確保同一請求只處理一次。
1.數(shù)據(jù)庫唯一索引:
數(shù)據(jù)庫設(shè)置唯一索引重復提交時,插表就會直接報錯重復
(不推薦,畢竟壓力直接進入數(shù)據(jù)庫了)
2.數(shù)據(jù)庫樂觀鎖:(數(shù)據(jù)修改時間 / 版本號) => 比對
查詢列表畫面時,將數(shù)據(jù)的修改時間(毫秒級)記錄一下,下次請求增刪改接口時,將數(shù)據(jù)原本的修改時間傳入接口,接口第一步判斷當前數(shù)據(jù)的修改時間是否和畫面上傳入的修改時間一致,一致就代表沒有人修改做此數(shù)據(jù),否則就提示此數(shù)據(jù)已被他人修改,請稍后再試!
最后更新記錄時,帶入版本號或者修改時間進去,
update xxx set name = xxx where id = xxx and updateTime = xxx
(并發(fā)量小的時候可以,并發(fā)大的時候存在重復修改問題)
3.唯一值+緩存:
其實也就是接口防抖中的第二個實現(xiàn)方案的變化版本
上面提到將接口的主要參數(shù)+用戶id作為唯一標識存入redis并記錄指定的緩存時間,那么這次存入redis不記錄時間,并且在接口結(jié)束時清除掉此緩存。
(推薦,但是當服務異常掛掉時,或者某些原因接口沒有正常執(zhí)行完成時,redis緩存一直都會在,不好維護,浪費資源)
4.分布式鎖(redisson)
業(yè)務開始時候去tryLock,嘗試獲取鎖(鎖的參數(shù)可以是本次操作的對象id,假如說本次要給某個商品增加扣減庫存,那么參數(shù)可以是商品id),保障在接口的最后一步,釋放鎖即可。
RLock lock = redissonClient.getLock("my-distributed-lock"); // 嘗試獲取鎖:等待最多 10 秒,鎖自動續(xù)期 30 秒 boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
這樣每次拿到鎖的線程才會繼續(xù)進行接口邏輯操作。
5.手動實現(xiàn)鎖
其實原理和第4點一樣,就是需要考慮手動實現(xiàn)鎖的復雜性
- 加鎖時如何保證加鎖和給鎖設(shè)置有效期的一致性
- 鎖的過期時間,鎖需要釋放
- 鎖不能提前釋放,防止其他線程獲取到此鎖
- 怎樣給將要過期的鎖加過期時間
- 釋放鎖的時候,如何保證釋放的是同一個鎖,防止錯釋放
- 保證釋放鎖時的原子性
1. 加鎖時setnx命令,設(shè)置其lock資源名稱 + value(一般為threadId / 時間戳) + 過期時間
2. 進行后續(xù)業(yè)務操作
3. 最后需要用lua腳本來釋放鎖(先獲取鎖的value確保是當前的lock,使用腳本釋放鎖)
總結(jié)
- 防抖:重點是減少請求次數(shù),通過時間戳、緩存實現(xiàn)。
- 冪等性:重點是保證結(jié)果唯一,通過唯一標識符、數(shù)據(jù)庫約束或鎖或業(yè)務校驗實現(xiàn)。
- 實際應用:通常需要結(jié)合兩者,例如:
前端防抖:減少無效請求。
后端冪等性:即使防抖失效,也能保證最終結(jié)果一致。
根據(jù)業(yè)務需求選擇合適的方案,例如:
- 高頻非敏感操作(如普通的修改或者刪除接口):使用本地緩存或 Redis 防抖。
- 敏感操作(如支付):結(jié)合 Redis 唯一標識符和數(shù)據(jù)庫唯一索引確保冪等。
到此這篇關(guān)于Java接口防抖/冪等性解決(redis)的文章就介紹到這了,更多相關(guān)Java接口防抖冪等性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解spring boot實現(xiàn)多數(shù)據(jù)源代碼實戰(zhàn)
本篇文章主要介紹了詳解spring boot實現(xiàn)多數(shù)據(jù)源代碼實戰(zhàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07使用jekins自動構(gòu)建部署java maven項目的方法步驟
這篇文章主要介紹了使用jekins自動構(gòu)建部署java maven項目的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01Java中樹的存儲結(jié)構(gòu)實現(xiàn)示例代碼
本篇文章主要介紹了Java中樹的存儲結(jié)構(gòu)實現(xiàn)示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09Java中Stream實現(xiàn)List排序的六個核心技巧總結(jié)
這篇文章主要介紹了Java中Stream實現(xiàn)List排序的六個核心技巧,分別是自然序排序、反向排序、空值安全處理、多字段組合排序、并行流加速、原地排序等,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2025-04-04