您好,登錄后才能下訂單哦!
這篇“SpringBoot怎么結(jié)合Aop+Redis防止接口重復(fù)提交”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“SpringBoot怎么結(jié)合Aop+Redis防止接口重復(fù)提交”文章吧。
在實際的開發(fā)項目中,一個對外暴露的接口往往會面臨很多次請求,我們來解釋一下冪等的概念:任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。按照這個含義,最終的含義就是 對數(shù)據(jù)庫的影響只能是一次性的,不能重復(fù)處理。如何保證其冪等性,通常有以下手段:
1、數(shù)據(jù)庫建立唯一性索引,可以保證最終插入數(shù)據(jù)庫的只有一條數(shù)據(jù)。
2、token機制,每次接口請求前先獲取一個token,然后再下次請求的時候在請求的header體中加上這個token,后臺進行驗證,如果驗證通過刪除token,下次請求再次判斷token。
3、悲觀鎖或者樂觀鎖,悲觀鎖可以保證每次for update的時候其他sql無法update數(shù)據(jù)(在數(shù)據(jù)庫引擎是innodb的時候,select的條件必須是唯一索引,防止鎖全表)
4、先查詢后判斷,首先通過查詢數(shù)據(jù)庫是否存在數(shù)據(jù),如果存在證明已經(jīng)請求過了,直接拒絕該請求,如果沒有存在,就證明是第一次進來,直接放行。
為什么要防止接口重復(fù)提交?
對于有些敏感操作接口,比如新增數(shù)據(jù)接口、付款接口,要是用戶操作不當(dāng)多次點擊提交按鈕,這些接口就會被多次請求,最后可能導(dǎo)致系統(tǒng)異常。
前端可以如何控制?
前端可以通過js進行控制,當(dāng)用戶點擊提交按鈕,
1.按鈕設(shè)置多少秒內(nèi)不可點擊狀態(tài)
2.按鈕點擊后彈出loading提示框,避免再次點擊,直到接口請求返回后
3.按鈕點擊后跳轉(zhuǎn)到新的頁面
但是,請記住,永遠不要相信用戶的行為,因為你不知道用戶會做哪些奇葩的操作,所以,最重要的還是要在后端處理。
使用aop+redis進行攔截處理
一.創(chuàng)建切面類RepeatSubmitAspect
實現(xiàn)過程:接口請求后,token+請求路徑作為key值去redis中讀取數(shù)據(jù),若能找到這個key,則證明是重復(fù)提交的,反之不是。若不是重復(fù)提交,則直接放行,并將這個key寫入redis中,并設(shè)置一定時間過期(我這里是設(shè)置的5s過期)
在傳統(tǒng)的web項目中,為了防止重復(fù)提交,通常做法是:后端生成唯一的提交令牌(uuid),存儲在服務(wù)端,頁面在發(fā)起請求時,攜帶次令牌,后端驗證請求后刪除令牌,保證請求的唯一性。
但是,上訴的做法是需要前后端都需要進行改動,如果在項目初期,是可以實現(xiàn)的,但是,在項目的后期,很多功能都實現(xiàn)好了,不可能大范圍的去改動。
思路
1.自定義注解@NoRepeatSubmit 標(biāo)記所有Controller中提交的請求
2.通過AOP對所有標(biāo)記了@NoRepeatSubmit 的方法進行攔截
3.在業(yè)務(wù)方法執(zhí)行前,獲取當(dāng)前用戶的token或者JSessionId+當(dāng)前請求地址,作為一個唯一的key,去獲取redis分布式鎖,如果此時并發(fā)獲取,只有一個線程能獲取到。
4.業(yè)務(wù)執(zhí)行后,釋放鎖
關(guān)于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 { /** * 設(shè)置請求鎖定時間 * * @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 * 防止接口重復(fù)提交 */ @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 { // 執(zhí)行進程 result = pjp.proceed(); } finally { // 解鎖 redisLock.unlock(key, clientId); LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); } return result; } else { // 獲取鎖失敗,認為是重復(fù)提交的請求 LOGGER.info("tryLock fail, key = [{}]", key); return ApiResult.success(200, "重復(fù)請求,請稍后再試", null); } } private String getKey(String token, String path) { return "00000"+":"+token + path; } private String getClientId() { return UUID.randomUUID().toString(); } }
以上就是關(guān)于“SpringBoot怎么結(jié)合Aop+Redis防止接口重復(fù)提交”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。