??通常我們可以在前端通過(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支持一下。