首頁 >資料庫 >Redis >SpringBoot怎麼結合Aop+Redis防止介面重複提交

SpringBoot怎麼結合Aop+Redis防止介面重複提交

王林
王林轉載
2023-05-31 10:40:061275瀏覽

在實際的開發項目中,一個對外暴露的接口往往會面臨很多次請求,我們來解釋一下冪等的概念:任意多次執行所產生的影響均與一次執行的影響相同。依照這個意義,最終的意義就是 對資料庫的影響只能是一次性的,不能重複處理。如何保證其冪等性,通常有以下手段:

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;

}

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
 * 防止接口重复提交
 */
@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防止介面重複提交的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除