您好,登錄后才能下訂單哦!
這篇“SpringBoot怎么結合Aop+Redis防止接口重復提交”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“SpringBoot怎么結合Aop+Redis防止接口重復提交”文章吧。
在實際的開發項目中,一個對外暴露的接口往往會面臨很多次請求,我們來解釋一下冪等的概念:任意多次執行所產生的影響均與一次執行的影響相同。按照這個含義,最終的含義就是 對數據庫的影響只能是一次性的,不能重復處理。如何保證其冪等性,通常有以下手段:
1、數據庫建立唯一性索引,可以保證最終插入數據庫的只有一條數據。
2、token機制,每次接口請求前先獲取一個token,然后再下次請求的時候在請求的header體中加上這個token,后臺進行驗證,如果驗證通過刪除token,下次請求再次判斷token。
3、悲觀鎖或者樂觀鎖,悲觀鎖可以保證每次for update的時候其他sql無法update數據(在數據庫引擎是innodb的時候,select的條件必須是唯一索引,防止鎖全表)
4、先查詢后判斷,首先通過查詢數據庫是否存在數據,如果存在證明已經請求過了,直接拒絕該請求,如果沒有存在,就證明是第一次進來,直接放行。
為什么要防止接口重復提交?
對于有些敏感操作接口,比如新增數據接口、付款接口,要是用戶操作不當多次點擊提交按鈕,這些接口就會被多次請求,最后可能導致系統異常。
前端可以如何控制?
前端可以通過js進行控制,當用戶點擊提交按鈕,
1.按鈕設置多少秒內不可點擊狀態
2.按鈕點擊后彈出loading提示框,避免再次點擊,直到接口請求返回后
3.按鈕點擊后跳轉到新的頁面
但是,請記住,永遠不要相信用戶的行為,因為你不知道用戶會做哪些奇葩的操作,所以,最重要的還是要在后端處理。
使用aop+redis進行攔截處理
一.創建切面類RepeatSubmitAspect
實現過程:接口請求后,token+請求路徑作為key值去redis中讀取數據,若能找到這個key,則證明是重復提交的,反之不是。若不是重復提交,則直接放行,并將這個key寫入redis中,并設置一定時間過期(我這里是設置的5s過期)
在傳統的web項目中,為了防止重復提交,通常做法是:后端生成唯一的提交令牌(uuid),存儲在服務端,頁面在發起請求時,攜帶次令牌,后端驗證請求后刪除令牌,保證請求的唯一性。
但是,上訴的做法是需要前后端都需要進行改動,如果在項目初期,是可以實現的,但是,在項目的后期,很多功能都實現好了,不可能大范圍的去改動。
思路
1.自定義注解@NoRepeatSubmit 標記所有Controller中提交的請求
2.通過AOP對所有標記了@NoRepeatSubmit 的方法進行攔截
3.在業務方法執行前,獲取當前用戶的token或者JSessionId+當前請求地址,作為一個唯一的key,去獲取redis分布式鎖,如果此時并發獲取,只有一個線程能獲取到。
4.業務執行后,釋放鎖
關于Redis分布式鎖
使用Redis是為了在負載均衡部署,如果是單機的項目可以使用一個本地線程安全的Cache替代Redis
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @ClassName NoRepeatSubmit * @Description 這里描述 * @Author admin * @Date 2021/3/2 16:16 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatSubmit { /** * 設置請求鎖定時間 * * @return */ int lockTime() default 10; }
package com.hongkun.aop; /** * @ClassName RepeatSubmitAspect * @Description 這里描述 * @Author admin * @Date 2021/3/2 16:15 */ import com.hongkun.until.ApiResult; import com.hongkun.until.Result; import com.hongkun.until.RedisLock; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author liucheng * @since 2020/01/15 * 防止接口重復提交 */ @Aspect @Component public class RepeatSubmitAspect { private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class); @Autowired private RedisLock redisLock; @Pointcut("@annotation(noRepeatSubmit)") public void pointCut(NoRepeatSubmit noRepeatSubmit) { } @Around("pointCut(noRepeatSubmit)") public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable { int lockSeconds = noRepeatSubmit.lockTime(); RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); Assert.notNull(request, "request can not null"); // 此處可以用token或者JSessionId String token = request.getHeader("token"); String path = request.getServletPath(); String key = getKey(token, path); String clientId = getClientId(); boolean isSuccess = redisLock.lock(key, clientId, lockSeconds,TimeUnit.SECONDS); LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId); if (isSuccess) { LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId); // 獲取鎖成功 Object result; try { // 執行進程 result = pjp.proceed(); } finally { // 解鎖 redisLock.unlock(key, clientId); LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); } return result; } else { // 獲取鎖失敗,認為是重復提交的請求 LOGGER.info("tryLock fail, key = [{}]", key); return ApiResult.success(200, "重復請求,請稍后再試", null); } } private String getKey(String token, String path) { return "00000"+":"+token + path; } private String getClientId() { return UUID.randomUUID().toString(); } }
以上就是關于“SpringBoot怎么結合Aop+Redis防止接口重復提交”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。