溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

SpringBoot怎么結(jié)合Aop+Redis防止接口重復(fù)提交

發(fā)布時間:2022-03-29 13:45:25 來源:億速云 閱讀:400 作者:iii 欄目:大數(shù)據(jù)

這篇“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;

}

AOP

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è)資訊頻道。

向AI問一下細節(jié)

免責(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)容。

AI