SpringCloud微服務(wù)實(shí)戰(zhàn)——搭建企業(yè)級(jí)開(kāi)發(fā)框架(三十九):使用Redis分布式鎖(Redisson)+自定義注解+AOP實(shí)現(xiàn)微服務(wù)重復(fù)請(qǐng)求控制

??通常我們可以在前端通過(guò)防抖和節(jié)流來(lái)解決短時(shí)間內(nèi)請(qǐng)求重復(fù)提交的問(wèn)題筹吐,如果因網(wǎng)絡(luò)問(wèn)題秘遏、Nginx重試機(jī)制、微服務(wù)Feign重試機(jī)制或者用戶故意繞過(guò)前端防抖和節(jié)流設(shè)置邦危,直接頻繁發(fā)起請(qǐng)求周拐,都會(huì)導(dǎo)致系統(tǒng)防重請(qǐng)求失敗凰兑,甚至導(dǎo)致后臺(tái)產(chǎn)生多條重復(fù)記錄,此時(shí)我們需要考慮在后臺(tái)增加防重設(shè)置勾给。
??考慮到微服務(wù)分布式的場(chǎng)景锅知,這里通過(guò)使用Redisson分布式鎖+自定義注解+AOP的方式來(lái)實(shí)現(xiàn)后臺(tái)防止重復(fù)請(qǐng)求的功能,基本實(shí)現(xiàn)思路:通過(guò)在需要防重的接口添加自定義防重注解售睹,設(shè)置防重參數(shù),通過(guò)AOP攔截請(qǐng)求參數(shù)昌妹,根據(jù)注解配置,生成分布式鎖的Key烂叔,并設(shè)置有效時(shí)間固歪。每次請(qǐng)求訪問(wèn)時(shí),都會(huì)嘗試獲取鎖牢裳,如果獲取到,則執(zhí)行胞四,如果獲取不到,那么說(shuō)明請(qǐng)求在設(shè)置的重復(fù)請(qǐng)求間隔內(nèi),返回請(qǐng)勿頻繁請(qǐng)求提示信息氓侧。

1、自定義防止重復(fù)請(qǐng)求注解偎痛,根據(jù)業(yè)務(wù)場(chǎng)景設(shè)置了以下參數(shù):
  • interval: 防止重復(fù)提交的時(shí)間間隔独郎。
  • timeUnit: 防止重復(fù)提交的時(shí)間間隔的單位枚赡。
  • currentSession: 是否將sessionId作為防重參數(shù)(微服務(wù)及跨域前后端分離時(shí)谓谦,無(wú)法使用,Chrome等瀏覽器跨域時(shí)禁止攜帶cookie反粥,每次sessionId都是新的)。
  • currentUser: 是否將用戶id作為防重參數(shù)才顿。
  • keys: 可以作為防重參數(shù)的字段(通過(guò)Spring Expression表達(dá)式,可以做到多參數(shù)時(shí)幅垮,具體取哪個(gè)參數(shù)的值)尾组。
  • ignoreKeys: 需要忽略的防重參數(shù)字段,例如有些參數(shù)中的時(shí)間戳演怎,此和keys互斥,當(dāng)keys配置了之后爷耀,ignoreKeys失效。
  • conditions:當(dāng)參數(shù)中的某個(gè)字段達(dá)到條件時(shí)跑杭,執(zhí)行防重配置咆耿,默認(rèn)不需要配置。
  • argsIndex: 當(dāng)沒(méi)有配置keys參數(shù)時(shí)萨螺,防重?cái)r截后會(huì)對(duì)所有參數(shù)取值作為分布式鎖的key,這里時(shí)椭盏,當(dāng)多參數(shù)時(shí)吻商,配置取哪一個(gè)參數(shù)作為key掏颊,可以多個(gè)。此和keys互斥盆偿,當(dāng)keys配置了之后准浴,argsIndex配置失效。
package com.gitegg.platform.base.annotation.resubmit;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 防止重復(fù)提交注解
 * 1兄裂、當(dāng)設(shè)置了keys時(shí),通過(guò)表達(dá)式確定取哪幾個(gè)參數(shù)作為防重key
 * 2谈撒、當(dāng)未設(shè)置keys時(shí)匾南,可以設(shè)置argsIndex設(shè)置取哪幾個(gè)參數(shù)作為防重key
 * 3、argsIndex和ignoreKeys是未設(shè)置keys時(shí)生效蛆楞,排除不需要防重的參數(shù)
 * 4、因部分瀏覽器在跨域請(qǐng)求時(shí)豹爹,不允許request請(qǐng)求攜帶cookie,導(dǎo)致每次sessionId都是新的光稼,所以這里默認(rèn)使用用戶id作為key的一部分孩等,不使用sessionId
 * @author GitEgg
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResubmitLock {

    /**
     * 防重復(fù)提交校驗(yàn)的時(shí)間間隔
     */
    long interval() default 5;

    /**
     * 防重復(fù)提交校驗(yàn)的時(shí)間間隔的單位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 是否僅在當(dāng)前session內(nèi)進(jìn)行防重復(fù)提交校驗(yàn)
     */
    boolean currentSession() default false;

    /**
     * 是否選用當(dāng)前操作用戶的信息作為防重復(fù)提交校驗(yàn)key的一部分
     */
    boolean currentUser() default true;

    /**
     * keys和ignoreKeys不能同時(shí)使用
     * 參數(shù)Spring EL表達(dá)式例如 #{param.name},表達(dá)式的值作為防重復(fù)校驗(yàn)key的一部分
     */
    String[] keys() default {};

    /**
     * keys和ignoreKeys不能同時(shí)使用
     * ignoreKeys不區(qū)分入?yún)ⅲ腥雲(yún)碛邢嗤淖侄螘r(shí)冰垄,都將過(guò)濾掉
     */
    String[] ignoreKeys() default {};

    /**
     * Spring EL表達(dá)式,決定是否進(jìn)行重復(fù)提交校驗(yàn),多個(gè)條件之間為且的關(guān)系,默認(rèn)是進(jìn)行校驗(yàn)
     */
    String[] conditions() default {"true"};

    /**
     * 當(dāng)未配置key時(shí)权她,設(shè)置哪幾個(gè)參數(shù)作為防重對(duì)象,默認(rèn)取所有參數(shù)
     *
     * @return
     */
    int[] argsIndex() default {};

}
2隅要、自定義AOP攔截防重請(qǐng)求的業(yè)務(wù)邏輯處理,詳細(xì)邏輯處理請(qǐng)看代碼注釋「芯郑可以在Nacos中增加配置resubmit-lock: enable: false 使防重配置失效暂衡,默認(rèn)不配置為生效狀態(tài)崖瞭。因?yàn)槭荝esubmitLockAspect是否初始化的ConditionalOnProperty配置,此配置修改需要重啟服務(wù)生效书聚。
package com.gitegg.platform.boot.aspect;

import com.gitegg.platform.base.annotation.resubmit.ResubmitLock;
import com.gitegg.platform.base.enums.ResultCodeEnum;
import com.gitegg.platform.base.exception.SystemException;
import com.gitegg.platform.base.util.JsonUtils;
import com.gitegg.platform.boot.util.ExpressionUtils;
import com.gitegg.platform.boot.util.GitEggAuthUtils;
import com.gitegg.platform.boot.util.GitEggWebUtils;
import com.gitegg.platform.redis.lock.IDistributedLockService;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;


/**
 * @author GitEgg
 * @date 2022-4-10
 */
@Log4j2
@Component
@Aspect
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@ConditionalOnProperty(name = "enabled", prefix = "resubmit-lock", havingValue = "true", matchIfMissing = true)
public class ResubmitLockAspect {

    private static final String REDIS_SEPARATOR = ":";

    private static final String RESUBMIT_CHECK_KEY_PREFIX = "resubmit_lock" + REDIS_SEPARATOR;

    private final IDistributedLockService distributedLockService;

    /**
     * 前置通知 防止重復(fù)提交
     *
     * @param joinPoint 切點(diǎn)
     * @param resubmitLock 注解配置
     */
    @Before("@annotation(resubmitLock)")
    public void resubmitCheck(JoinPoint joinPoint, ResubmitLock resubmitLock) throws Throwable {

        final Object[] args = joinPoint.getArgs();
        final String[] conditions = resubmitLock.conditions();

        //根據(jù)條件判斷是否需要進(jìn)行防重復(fù)提交檢查
        if (ExpressionUtils.getConditionValue(args, conditions) && !ArrayUtils.isEmpty(args)) {
            doCheck(resubmitLock, args);
        }
    }

    /**
     * key的組成為: resubmit_lock:userId:sessionId:uri:method:(根據(jù)spring EL表達(dá)式對(duì)參數(shù)進(jìn)行拼接)
     *
     * @param resubmitLock 注解
     * @param args       方法入?yún)?     */
    private void doCheck(@NonNull ResubmitLock resubmitLock, Object[] args) {

        final String[] keys = resubmitLock.keys();
        final boolean currentUser = resubmitLock.currentUser();
        final boolean currentSession = resubmitLock.currentSession();

        String method = GitEggWebUtils.getRequest().getMethod();
        String uri = GitEggWebUtils.getRequest().getRequestURI();

        StringBuffer lockKeyBuffer = new StringBuffer(RESUBMIT_CHECK_KEY_PREFIX);

        if (null != GitEggAuthUtils.getTenantId())
        {
            lockKeyBuffer.append( GitEggAuthUtils.getTenantId()).append(REDIS_SEPARATOR);
        }

        // 此判斷暫時(shí)預(yù)留斩个,適配后續(xù)無(wú)用戶登錄場(chǎng)景,因部分瀏覽器在跨域請(qǐng)求時(shí)受啥,不允許request請(qǐng)求攜帶cookie鸽心,導(dǎo)致每次sessionId都是新的,所以這里默認(rèn)使用用戶id作為key的一部分顽频,不使用sessionId
        if (currentSession)
        {
            lockKeyBuffer.append( GitEggWebUtils.getSessionId()).append(REDIS_SEPARATOR);
        }

        // 默認(rèn)沒(méi)有將user數(shù)據(jù)作為防重key
        if (currentUser && null != GitEggAuthUtils.getCurrentUser())
        {
            lockKeyBuffer.append( GitEggAuthUtils.getCurrentUser().getId() ).append(REDIS_SEPARATOR);
        }

        lockKeyBuffer.append(uri).append(REDIS_SEPARATOR).append(method);


        StringBuffer parametersBuffer = new StringBuffer();
        // 優(yōu)先判斷是否設(shè)置防重字段,因keys試數(shù)組嘁圈,取值時(shí)是按照順序排列的莺奸,這里不需要重新排序
        if (ArrayUtils.isNotEmpty(keys))
        {
            Object[] argsForKey = ExpressionUtils.getExpressionValue(args, keys);
            for (Object obj : argsForKey) {
                parametersBuffer.append(REDIS_SEPARATOR).append(String.valueOf(obj));
            }
        }
        // 如果沒(méi)有設(shè)置防重的字段,那么需要把所有的字段和值作為key灭贷,因通過(guò)反射獲取字段時(shí),順序時(shí)不確定的仗岖,這里取出來(lái)之后需要進(jìn)行排序
        else{
            // 只有當(dāng)keys為空時(shí)览妖,ignoreKeys和argsIndex生效
            final String[] ignoreKeys = resubmitLock.ignoreKeys();
            final int[] argsIndex = resubmitLock.argsIndex();
            if (ArrayUtils.isNotEmpty(argsIndex))
            {
                for(int index : argsIndex){
                    parametersBuffer.append(REDIS_SEPARATOR).append( getKeyAndValueJsonStr(args[index], ignoreKeys));
                }
            }
            else
            {
                for(Object obj : args){
                    parametersBuffer.append(REDIS_SEPARATOR).append( getKeyAndValueJsonStr(obj, ignoreKeys) );
                }
            }
        }

        // 將請(qǐng)求參數(shù)取md5值作為key的一部分,MD5理論上會(huì)重復(fù)讽膏,但是key中還包含session或者用戶id,所以同用戶在極端時(shí)間內(nèi)請(qǐng)參數(shù)不同生成的相同md5值的概率極低
        String parametersKey = DigestUtils.md5DigestAsHex(parametersBuffer.toString().getBytes());
        lockKeyBuffer.append(parametersKey);

        try {
            boolean isLock = distributedLockService.tryLock(lockKeyBuffer.toString(), 0, resubmitLock.interval(), resubmitLock.timeUnit());
            if (!isLock)
            {
                throw new SystemException(ResultCodeEnum.RESUBMIT_LOCK.code, ResultCodeEnum.RESUBMIT_LOCK.msg);
            }
        } catch (InterruptedException e) {
            throw new SystemException(ResultCodeEnum.RESUBMIT_LOCK.code, ResultCodeEnum.RESUBMIT_LOCK.msg);
        }
    }

    /**
     * 將字段轉(zhuǎn)換為json字符串
     * @param obj
     * @return
     */
    public static String getKeyAndValueJsonStr(Object obj, String[] ignoreKeys) {
        Map<String, Object> map = Maps.newHashMap();
        // 得到類(lèi)對(duì)象
        Class objCla = (Class) obj.getClass();
        /* 得到類(lèi)中的所有屬性集合 */
        Field[] fs = objCla.getDeclaredFields();
        for (int i = 0; i < fs.length; i++) {
            Field f = fs[i];
            // 設(shè)置些屬性是可以訪問(wèn)的
            f.setAccessible(true);
            Object val = new Object();
            try {
                String filedName = f.getName();
                // 如果字段在排除列表俐末,那么不將字段放入map
                if (null != ignoreKeys && Arrays.asList(ignoreKeys).contains(filedName))
                {
                    continue;
                }
                val = f.get(obj);
                // 得到此屬性的值
                // 設(shè)置鍵值
                map.put(filedName, val);
            } catch (IllegalArgumentException e) {
                log.error("getKeyAndValue IllegalArgumentException", e);
                throw new RuntimeException("您的操作太頻繁,請(qǐng)稍后再試");
            } catch (IllegalAccessException e) {
                log.error("getKeyAndValue IllegalAccessException", e);
                throw new RuntimeException("您的操作太頻繁载矿,請(qǐng)稍后再試");
            }
        }
        Map<String, Object> sortMap = sortMapByKey(map);
        String mapStr = JsonUtils.mapToJson(sortMap);
        return mapStr;
    }

    private static Map<String, Object> sortMapByKey(Map<String, Object> map) {
        if (map == null || map.isEmpty()) {
            return null;
        }
        Map<String, Object> sortMap = new TreeMap<String, Object>(new Comparator<String>() {
            @Override
            public int compare(String o1,String o2) {
                return ((String)o1).compareTo((String) o2);
            }
        });
        sortMap.putAll(map);
        return sortMap;
    }

}

3烹卒、Redisson分布式鎖自定義接口
package com.gitegg.platform.redis.lock;

import java.util.concurrent.TimeUnit;

/**
 * 分布式鎖接口
 * @author GitEgg
 * @date 2022-4-10
 */
public interface IDistributedLockService {

    /**
     * 加鎖
     * @param lockKey key
     */
    void lock(String lockKey);

    /**
     * 釋放鎖
     *
     * @param lockKey key
     */
    void unlock(String lockKey);

    /**
     * 加鎖并設(shè)置有效期
     *
     * @param lockKey key
     * @param timeout 有效時(shí)間,默認(rèn)時(shí)間單位在實(shí)現(xiàn)類(lèi)傳入
     */
    void lock(String lockKey, int timeout);

    /**
     * 加鎖并設(shè)置有效期指定時(shí)間單位
     * @param lockKey key
     * @param timeout 有效時(shí)間
     * @param unit    時(shí)間單位
     */
    void lock(String lockKey, int timeout, TimeUnit unit);

    /**
     * 嘗試獲取鎖逢勾,獲取到則持有該鎖返回true,未獲取到立即返回false
     * @param lockKey
     * @return true-獲取鎖成功 false-獲取鎖失敗
     */
    boolean tryLock(String lockKey);

    /**
     * 嘗試獲取鎖,獲取到則持有該鎖leaseTime時(shí)間.
     * 若未獲取到敏沉,在waitTime時(shí)間內(nèi)一直嘗試獲取炎码,超過(guò)watiTime還未獲取到則返回false
     * @param lockKey   key
     * @param waitTime  嘗試獲取時(shí)間
     * @param leaseTime 鎖持有時(shí)間
     * @param unit      時(shí)間單位
     * @return true-獲取鎖成功 false-獲取鎖失敗
     * @throws InterruptedException
     */
    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
            throws InterruptedException;

    /**
     * 鎖是否被任意一個(gè)線程鎖持有
     * @param lockKey
     * @return true-被鎖 false-未被鎖
     */
    boolean isLocked(String lockKey);
}

4、Redisson分布式鎖自定義接口實(shí)現(xiàn)類(lèi)
package com.gitegg.platform.redis.lock.impl;

import com.gitegg.platform.redis.lock.IDistributedLockService;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * 分布式鎖的 Redisson 接口實(shí)現(xiàn)
 * @author GitEgg
 * @date 2022-4-10
 */
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class DistributedLockServiceImpl implements IDistributedLockService {

    private final RedissonClient redissonClient;


    @Override
    public void lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
    }

    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void lock(String lockKey, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, TimeUnit.MILLISECONDS);
    }

    @Override
    public void lock(String lockKey, int timeout, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
    }

    @Override
    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }

    @Override
    public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock(waitTime, leaseTime, unit);
    }

    @Override
    public boolean isLocked(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isLocked();
    }
}

5攒菠、Spring Expression自定義工具類(lèi)歉闰,通過(guò)此工具類(lèi)獲取注解上的Expression表達(dá)式,以獲取相應(yīng)請(qǐng)求對(duì)象的值和敬,如果請(qǐng)求對(duì)象有多個(gè),可以通過(guò)Expression表達(dá)式精準(zhǔn)獲取昼弟。
package com.gitegg.platform.boot.util;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Spring Expression 工具類(lèi)
 * @author GitEgg
 * @date 2022-4-11
 */
public class ExpressionUtils {

    private static final Map<String, Expression> EXPRESSION_CACHE = new ConcurrentHashMap<>(64);

    /**
     * 獲取Expression對(duì)象
     *
     * @param expressionString Spring EL 表達(dá)式字符串 例如 #{param.id}
     * @return Expression
     */
    @Nullable
    public static Expression getExpression(@Nullable String expressionString) {

        if (StringUtils.isBlank(expressionString)) {
            return null;
        }

        if (EXPRESSION_CACHE.containsKey(expressionString)) {
            return EXPRESSION_CACHE.get(expressionString);
        }

        Expression expression = new SpelExpressionParser().parseExpression(expressionString);
        EXPRESSION_CACHE.put(expressionString, expression);
        return expression;
    }

    /**
     * 根據(jù)Spring EL表達(dá)式字符串從根對(duì)象中求值
     *
     * @param root             根對(duì)象
     * @param expressionString Spring EL表達(dá)式
     * @param clazz            值得類(lèi)型
     * @param <T>              泛型
     * @return 值
     */
    @Nullable
    public static <T> T getExpressionValue(@Nullable Object root, @Nullable String expressionString, @NonNull Class<? extends T> clazz) {
        if (root == null) {
            return null;
        }
        Expression expression = getExpression(expressionString);
        if (expression == null) {
            return null;
        }

        return expression.getValue(root, clazz);
    }

    @Nullable
    public static <T> T getExpressionValue(@Nullable Object root, @Nullable String expressionString) {
        if (root == null) {
            return null;
        }
        Expression expression = getExpression(expressionString);
        if (expression == null) {
            return null;
        }
        //noinspection unchecked
        return (T) expression.getValue(root);
    }

    /**
     * 求值
     *
     * @param root              根對(duì)象
     * @param expressionStrings Spring EL表達(dá)式
     * @param <T>               泛型 這里的泛型要慎用,大多數(shù)情況下要使用Object接收避免出現(xiàn)轉(zhuǎn)換異常
     * @return 結(jié)果集
     */
    public static <T> T[] getExpressionValue(@Nullable Object root, @Nullable String... expressionStrings) {
        if (root == null) {
            return null;
        }
        if (ArrayUtils.isEmpty(expressionStrings)) {
            return null;
        }
        //noinspection ConstantConditions
        Object[] values = new Object[expressionStrings.length];
        for (int i = 0; i < expressionStrings.length; i++) {
            //noinspection unchecked
            values[i] = (T) getExpressionValue(root, expressionStrings[i]);
        }
        //noinspection unchecked
        return (T[]) values;
    }

    /**
     * 表達(dá)式條件求值
     * 如果為值為null則返回false,
     * 如果為布爾類(lèi)型直接返回,
     * 如果為數(shù)字類(lèi)型則判斷是否大于0
     *
     * @param root             根對(duì)象
     * @param expressionString Spring EL表達(dá)式
     * @return 值
     */
    @Nullable
    public static boolean getConditionValue(@Nullable Object root, @Nullable String expressionString) {
        Object value = getExpressionValue(root, expressionString);
        if (value == null) {
            return false;
        }
        if (value instanceof Boolean) {
            return (boolean) value;
        }
        if (value instanceof Number) {
            return ((Number) value).longValue() > 0;
        }
        return true;
    }

    /**
     * 表達(dá)式條件求值
     *
     * @param root              根對(duì)象
     * @param expressionStrings Spring EL表達(dá)式數(shù)組
     * @return 值
     */
    @Nullable
    public static boolean getConditionValue(@Nullable Object root, @Nullable String... expressionStrings) {
        if (root == null) {
            return false;
        }
        if (ArrayUtils.isEmpty(expressionStrings)) {
            return false;
        }
        //noinspection ConstantConditions
        for (String expressionString : expressionStrings) {
            if (!getConditionValue(root, expressionString)) {
                return false;
            }
        }
        return true;
    }
}
5变骡、防重測(cè)試芭逝,我們?cè)谙到y(tǒng)的用戶接口(GitEgg-Cloud工程的UserController類(lèi))上進(jìn)行測(cè)試,通過(guò)多參數(shù)接口以及配置keys旬盯,不配置keys等各種場(chǎng)景進(jìn)行測(cè)試翎猛,在測(cè)試時(shí)為了達(dá)到效果接剩,可以將interval 時(shí)間設(shè)置為30秒。
  • 設(shè)置user參數(shù)的realName搂漠,mobile和page參數(shù)的size為key進(jìn)行防重測(cè)試
    @ResubmitLock(interval = 30, keys = {"[0].realName","[0].mobile","[1].size"})
    public PageResult<UserInfo> list(@ApiIgnore QueryUserDTO user, @ApiIgnore Page<UserInfo> page) {
        Page<UserInfo> pageUser = userService.selectUserList(page, user);
        PageResult<UserInfo> pageResult = new PageResult<>(pageUser.getTotal(), pageUser.getRecords());
        return pageResult;
    }
  • 不設(shè)置防重參數(shù)的key某弦,只取第一個(gè)參數(shù)user,配置排除的參數(shù)怔毛,不參與放重key的生成
    @ResubmitLock(interval = 30, argsIndex = {0}, ignoreKeys = {"email","status"})
    public PageResult<UserInfo> list(@ApiIgnore QueryUserDTO user, @ApiIgnore Page<UserInfo> page) {
        Page<UserInfo> pageUser = userService.selectUserList(page, user);
        PageResult<UserInfo> pageResult = new PageResult<>(pageUser.getTotal(), pageUser.getRecords());
        return pageResult;
    }
  • 測(cè)試結(jié)果


    測(cè)試結(jié)果

相關(guān)引用:
1腾降、防重配置項(xiàng)及通過(guò)SpringExpression獲取相應(yīng)參數(shù):http://www.reibang.com/p/77895a822237
2、Redisson分布式鎖及相關(guān)工具類(lèi):https://blog.csdn.net/wsh_ningjing/article/details/115326052

GitEgg-Cloud是一款基于SpringCloud整合搭建的企業(yè)級(jí)微服務(wù)應(yīng)用開(kāi)發(fā)框架螃壤,開(kāi)源項(xiàng)目地址:

Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg

歡迎感興趣的小伙伴Star支持一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冤馏,一起剝皮案震驚了整個(gè)濱河市寄啼,隨后出現(xiàn)的幾起案子逮光,更是在濱河造成了極大的恐慌墩划,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杜漠,死亡現(xiàn)場(chǎng)離奇詭異蚣旱,居然都是意外死亡碑幅,警方通過(guò)查閱死者的電腦和手機(jī)塞绿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裹赴,“玉大人,你說(shuō)我怎么就攤上這事棋返。” “怎么了睛竣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)殊者。 經(jīng)常有香客問(wèn)我验夯,道長(zhǎng)猖吴,這世上最難降的妖魔是什么挥转? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮绑谣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刑然。我一直安慰自己,他們只是感情好泼掠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布垦细。 她就那樣靜靜地躺著,像睡著了一般括改。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嘱能,一...
    開(kāi)封第一講書(shū)人閱讀 51,287評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音惹骂,去河邊找鬼。 笑死对粪,一個(gè)胖子當(dāng)著我的面吹牛右冻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纱扭,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼暗赶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起肃叶,我...
    開(kāi)封第一講書(shū)人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤忆首,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后被环,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡详幽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年筛欢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唇聘。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡版姑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迟郎,到底是詐尸還是另有隱情剥险,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布宪肖,位于F島的核電站表制,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏么介。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一蜕衡、第九天 我趴在偏房一處隱蔽的房頂上張望壤短。 院中可真熱鬧,春花似錦慨仿、人聲如沸久脯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)淡喜。三九已至问裕,卻和暖如春大溜,著一層夾襖步出監(jiān)牢的瞬間织中,已是汗流浹背苗缩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工崭参, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人慰于。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓钮科,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親婆赠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绵脯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容