基于AOP+Redis的簡(jiǎn)易滑動(dòng)窗口限流
在分布式系統(tǒng)設(shè)計(jì)中,限流是保障服務(wù)穩(wěn)定性的核心技術(shù)之一?;瑒?dòng)窗口限流算法以其精確性和平滑性優(yōu)勢(shì),成為解決傳統(tǒng)固定窗口限流臨界突變問(wèn)題的理想方案。本文將深入解析滑動(dòng)窗口算法原理,并通過(guò)AOP+Redis滑動(dòng)窗口限流。
固定窗口與滑動(dòng)窗口對(duì)比
固定窗口限流及其缺陷
固定窗口限流將時(shí)間劃分為固定區(qū)間(如每分鐘),統(tǒng)計(jì)每個(gè)區(qū)間內(nèi)的請(qǐng)求數(shù)量。這種方法雖然簡(jiǎn)單,但存在嚴(yán)重缺陷:

當(dāng)大量請(qǐng)求集中在兩個(gè)窗口的交界處時(shí)(如00:59:59和01:00:00),系統(tǒng)會(huì)在極短時(shí)間內(nèi)接收雙倍于閾值的請(qǐng)求,導(dǎo)致服務(wù)過(guò)載?;瑒?dòng)窗口限流通過(guò)動(dòng)態(tài)時(shí)間區(qū)間解決了這個(gè)問(wèn)題。
核心原理:為每個(gè)請(qǐng)求動(dòng)態(tài)定義一個(gè)以當(dāng)前時(shí)間為終點(diǎn)、向前回溯固定時(shí)長(zhǎng)T的時(shí)間區(qū)間(滑動(dòng)窗口),統(tǒng)計(jì)該區(qū)間內(nèi)的請(qǐng)求數(shù):
- 動(dòng)態(tài)窗口:每個(gè)請(qǐng)求到達(dá)時(shí)計(jì)算
[當(dāng)前時(shí)間 - T, 當(dāng)前時(shí)間]區(qū)間 - 實(shí)時(shí)統(tǒng)計(jì):計(jì)算該區(qū)間內(nèi)的請(qǐng)求數(shù)量
- 決策執(zhí)行:請(qǐng)求數(shù) < 閾值 → 允許;否則拒絕
- 窗口滑動(dòng):過(guò)期請(qǐng)求自動(dòng)移出統(tǒng)計(jì)范圍
Redis實(shí)現(xiàn)方案
Redis的有序集合(ZSET)是實(shí)現(xiàn)滑動(dòng)窗口限流的理想數(shù)據(jù)結(jié)構(gòu):
ZSET結(jié)合了集合(Set)和哈希(Hash)的特性:
- 唯一成員:每個(gè)成員(member)在集合中唯一
- 分?jǐn)?shù)排序:每個(gè)成員關(guān)聯(lián)一個(gè)分?jǐn)?shù)(score),用于排序
- 自動(dòng)排序:成員按分?jǐn)?shù)值從小到大排序
之所以是滑動(dòng)窗口限流的理想選擇,關(guān)鍵在于它完美解決了滑動(dòng)窗口算法的三個(gè)核心需求:
- 時(shí)間序列的天然支持:ZSET的分?jǐn)?shù)(score)機(jī)制為時(shí)間戳提供了原生支持。當(dāng)我們將請(qǐng)求時(shí)間戳作為score存儲(chǔ)時(shí),所有請(qǐng)求按時(shí)間順序自動(dòng)排序,形成精確的時(shí)間序列。這使得界定時(shí)間窗口邊界變得簡(jiǎn)單——只需計(jì)算
當(dāng)前時(shí)間 - 窗口大小就能得到窗口起始點(diǎn),無(wú)需額外維護(hù)時(shí)間索引。這種設(shè)計(jì)讓滑動(dòng)窗口的"滑動(dòng)"機(jī)制得以自然實(shí)現(xiàn)。 - 高效的范圍操作能力:滑動(dòng)窗口的核心操作是清理過(guò)期請(qǐng)求,ZSET的
ZREMRANGEBYSCORE命令正是為此而生。它能以O(log(N)+M)的復(fù)雜度高效刪除指定時(shí)間范圍外的歷史請(qǐng)求。這種高效的范圍刪除能力確保了窗口滑動(dòng)時(shí)的實(shí)時(shí)性能。 - 精確的實(shí)時(shí)統(tǒng)計(jì)特性:通過(guò)
ZCARD和ZCOUNT命令,ZSET提供原子級(jí)的精確計(jì)數(shù)能力。在滑動(dòng)窗口算法中,我們需要實(shí)時(shí)統(tǒng)計(jì)當(dāng)前窗口內(nèi)的請(qǐng)求數(shù)并與閾值比較,這些命令能以O(shè)(1)和O(log(N))復(fù)雜度瞬間完成統(tǒng)計(jì)。這種即時(shí)反饋機(jī)制對(duì)高并發(fā)場(chǎng)景至關(guān)重要,確保限流決策的及時(shí)性和準(zhǔn)確性。
| 命令 | 統(tǒng)計(jì)范圍 | 時(shí)間復(fù)雜度 | 典型使用場(chǎng)景 |
|---|---|---|---|
| ZCARD key | 整個(gè) ZSET 的總成員數(shù) | O(1) | 清理過(guò)期數(shù)據(jù)后快速獲取當(dāng)前窗口請(qǐng)求總數(shù) |
| ZCOUNT key min max | 指定 score 范圍內(nèi)的成員數(shù) | O(log(N)) | 動(dòng)態(tài)統(tǒng)計(jì)子窗口/特定時(shí)間段的請(qǐng)求量 |
代碼
自定義注解
首先自定義注解,定義限流維度、窗口大小、時(shí)間單位、窗口內(nèi)最大請(qǐng)求數(shù)量
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SlidingWindowLimit {
/**
* 限流維度的 SpEL 表達(dá)式
* 示例:
* - 按郵箱: "#email"
* - 按 IP: "#request.remoteAddr"
* - 按用戶(hù) ID + 郵箱: "#user.id + ':' + #user.email"
*/
String keySpEL() default "#email";
/**
* 窗口大小
*/
int windowSize() default 60;
/**
* 時(shí)間單位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 窗口內(nèi)最大請(qǐng)求數(shù)
*/
int maxRequests() default 10;
}

切面類(lèi)
1. 切面配置與基礎(chǔ)結(jié)構(gòu)
首先創(chuàng)建切面類(lèi)
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class SlidingWindowLimitAspect {
@Resource
private RedissonClient redissonClient;
private static final Logger log = LoggerFactory.getLogger(SlidingWindowLimitAspect.class);
private static final String RATE_LIMIT_PREFIX = "rate_limit";
}
@Aspect:聲明該類(lèi)為AOP切面@Order:設(shè)置切面執(zhí)行優(yōu)先級(jí)(數(shù)字越小優(yōu)先級(jí)越高)redissonClient:Redis客戶(hù)端操作接口RATE_LIMIT_PREFIX:Redis鍵名前綴,用于區(qū)分限流數(shù)據(jù)
2. 切面入口方法 - around()
@Around("@annotation(slidingWindowLimit)")
public Object around(ProceedingJoinPoint joinPoint, SlidingWindowLimit slidingWindowLimit) throws Throwable {
//獲取方法簽名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
//判斷是否為Http請(qǐng)求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes == null){
log.warn("方法 {} 不在 Web 請(qǐng)求上下文中,跳過(guò)限流檢查。", methodName);
return joinPoint.proceed();
}
try {
Object parseSpEL = parseSpEL(joinPoint, signature, slidingWindowLimit.keySpEL());
String rateLimitKey = buildRateLimitKey(parseSpEL, slidingWindowLimit);
if(isRateLimited(rateLimitKey, slidingWindowLimit.windowSize(), slidingWindowLimit.timeUnit(), slidingWindowLimit.maxRequests())){
throw new RateLimitExceededException("Rate limit exceeded");
}
return joinPoint.proceed();
}catch (RateLimitExceededException e){
log.warn("方法 {} 觸發(fā)了限流,已拒絕訪問(wèn)。", methodName);
throw e;
}catch (Exception e){
log.error("方法 {} 觸發(fā)了異常,已拒絕訪問(wèn)。", methodName, e);
throw e;
}
}
執(zhí)行過(guò)程:

3. SpEL解析方法 - parseSpEL()
private Object parseSpEL(ProceedingJoinPoint joinPoint,MethodSignature signature, String keySpEL){
StandardEvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
String[] parameterNames = signature.getParameterNames();
for (int i = 0; i < args.length; i++) {
if(parameterNames!=null && i< parameterNames.length){
context.setVariable(parameterNames[i], args[i]);
}else {
context.setVariable("arg" + i, args[i]);
}
}
ExpressionParser parser = new SpelExpressionParser();
return parser.parseExpression(keySpEL).getValue(context);
}
功能說(shuō)明:
- 動(dòng)態(tài)解析注解中的SpEL表達(dá)式(如
#email) - 將方法參數(shù)注入表達(dá)式上下文
- 支持靈活的限流鍵生成策略
4.限流鍵構(gòu)建方法 - buildRateLimitKey()
private String buildRateLimitKey(Object keyValue, SlidingWindowLimit slidingWindowLimit){
if(keyValue == null){
throw new IllegalArgumentException("限流參數(shù)不能為空");
}
return String.format("%s:%s:%s",RATE_LIMIT_PREFIX,slidingWindowLimit.keySpEL(), keyValue);
}
鍵格式說(shuō)明:
rate_limit:SpEL表達(dá)式:參數(shù)值 ↓ ↓ ↓ rate_limit:#email:user@example.com
5. 限流核心邏輯 - isRateLimited()
private boolean isRateLimited(String key, int windowSize, TimeUnit timeUnit, int maxRequests){
//獲取當(dāng)前時(shí)間數(shù)
long currentTime = System.currentTimeMillis();
long windowStartTime = currentTime - convertToMillis(windowSize, timeUnit);
// 獲取 Redisson 的 ZSet 操作對(duì)象
RScoredSortedSet<Long> scoredSortedSet = redissonClient.getScoredSortedSet(key);
// 1. 刪除窗口外的過(guò)期請(qǐng)求
scoredSortedSet.removeRangeByScore(0, true, windowStartTime, true); // [0, windowStartTime]
// 2. 添加當(dāng)前請(qǐng)求的時(shí)間戳到 ZSet
scoredSortedSet.add(currentTime, currentTime); // score 和 value 均為時(shí)間戳
// 3. 統(tǒng)計(jì)窗口內(nèi)請(qǐng)求數(shù)量
int count = scoredSortedSet.size();
return count > maxRequests;
}
Redis操作序列:
ZREMRANGEBYSCORE key 0 windowStart:刪除過(guò)期請(qǐng)求ZADD key currentTime currentTime:添加當(dāng)前請(qǐng)求ZCARD key:獲取當(dāng)前請(qǐng)求數(shù)
6. 時(shí)間單位轉(zhuǎn)換 - convertToMillis()
private long convertToMillis(int windowSize, TimeUnit timeUnit) {
return switch (timeUnit) {
case SECONDS -> timeUnit.toMillis(windowSize);
case MINUTES -> timeUnit.toMillis(windowSize);
case HOURS -> timeUnit.toMillis(windowSize);
case DAYS -> timeUnit.toMillis(windowSize);
case MILLISECONDS -> windowSize;
default -> throw new IllegalArgumentException("不支持的時(shí)間單位: " + timeUnit);
};
}
完整代碼
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class SlidingWindowLimitAspect {
@Resource
private RedissonClient redissonClient;
private static final Logger log = LoggerFactory.getLogger(SlidingWindowLimitAspect.class);
private static final String RATE_LIMIT_PREFIX = "rate_limit";
@Around("@annotation(slidingWindowLimit)")
public Object around(ProceedingJoinPoint joinPoint, SlidingWindowLimit slidingWindowLimit) throws Throwable {
//獲取方法簽名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
//判斷是否為Http請(qǐng)求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes == null){
log.warn("方法 {} 不在 Web 請(qǐng)求上下文中,跳過(guò)限流檢查。", methodName);
return joinPoint.proceed();
}
try {
Object parseSpEL = parseSpEL(joinPoint, signature, slidingWindowLimit.keySpEL());
String rateLimitKey = buildRateLimitKey(parseSpEL, slidingWindowLimit);
if(isRateLimited(rateLimitKey, slidingWindowLimit.windowSize(), slidingWindowLimit.timeUnit(), slidingWindowLimit.maxRequests())){
throw new RateLimitExceededException("Rate limit exceeded");
}
return joinPoint.proceed();
}catch (RateLimitExceededException e){
log.warn("方法 {} 觸發(fā)了限流,已拒絕訪問(wèn)。", methodName);
throw e;
}catch (Exception e){
log.error("方法 {} 觸發(fā)了異常,已拒絕訪問(wèn)。", methodName, e);
throw e;
}
}
private Object parseSpEL(ProceedingJoinPoint joinPoint,MethodSignature signature, String keySpEL){
StandardEvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
String[] parameterNames = signature.getParameterNames();
for (int i = 0; i < args.length; i++) {
if(parameterNames!=null && i< parameterNames.length){
context.setVariable(parameterNames[i], args[i]);
}else {
context.setVariable("arg" + i, args[i]);
}
}
ExpressionParser parser = new SpelExpressionParser();
return parser.parseExpression(keySpEL).getValue(context);
}
private String buildRateLimitKey(Object keyValue, SlidingWindowLimit slidingWindowLimit){
if(keyValue == null){
throw new IllegalArgumentException("限流參數(shù)不能為空");
}
return String.format("%s:%s:%s",RATE_LIMIT_PREFIX,slidingWindowLimit.keySpEL(), keyValue);
}
private boolean isRateLimited(String key, int windowSize, TimeUnit timeUnit, int maxRequests){
//獲取當(dāng)前時(shí)間數(shù)
long currentTime = System.currentTimeMillis();
long windowStartTime = currentTime - convertToMillis(windowSize, timeUnit);
// 獲取 Redisson 的 ZSet 操作對(duì)象
RScoredSortedSet<Long> scoredSortedSet = redissonClient.getScoredSortedSet(key);
// 1. 刪除窗口外的過(guò)期請(qǐng)求
scoredSortedSet.removeRangeByScore(0, true, windowStartTime, true); // [0, windowStartTime]
// 2. 添加當(dāng)前請(qǐng)求的時(shí)間戳到 ZSet
scoredSortedSet.add(currentTime, currentTime); // score 和 value 均為時(shí)間戳
// 3. 統(tǒng)計(jì)窗口內(nèi)請(qǐng)求數(shù)量
int count = scoredSortedSet.size();
return count > maxRequests;
}
/**
* 時(shí)間單位轉(zhuǎn)換,將時(shí)間單位轉(zhuǎn)換為毫秒數(shù)
* @param windowSize 窗口大小
* @param timeUnit 時(shí)間單位
* @return
*/
private long convertToMillis(int windowSize, TimeUnit timeUnit){
return switch (timeUnit){
case NANOSECONDS, SECONDS, MICROSECONDS, MINUTES, HOURS, DAYS -> timeUnit.toMillis(windowSize);
case MILLISECONDS -> windowSize;
default -> throw new IllegalArgumentException("不支持的時(shí)間單位: " + timeUnit);
};
}
}
注解使用
比如說(shuō)我們現(xiàn)在定義發(fā)送驗(yàn)證碼的方法60秒內(nèi)只能發(fā)送三次
@PostMapping("/send/code")
@SlidingWindowLimit(keySpEL = "#email.email",windowSize = 60, maxRequests = 3)
public Result sendVerificationCode(@RequestBody EmailSendDTO email) {
userService.sendVerificationCode(email.getEmail());
return Result.success();
}
每一次訪問(wèn)時(shí),Redis都會(huì)記錄下時(shí)間戳,如果第四次訪問(wèn)時(shí)的時(shí)間戳與第一次訪問(wèn)的時(shí)間戳之間少于60秒,則返回
{
"code": 429,
"message": "請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試",
"data": null
}
優(yōu)化
private boolean isRateLimited(String key, int windowSize, TimeUnit timeUnit, int maxRequests){
//獲取當(dāng)前時(shí)間數(shù)
long currentTime = System.currentTimeMillis();
long windowStartTime = currentTime - convertToMillis(windowSize, timeUnit);
// 獲取 Redisson 的 ZSet 操作對(duì)象
RScoredSortedSet<Long> scoredSortedSet = redissonClient.getScoredSortedSet(key);
// 1. 刪除窗口外的過(guò)期請(qǐng)求
scoredSortedSet.removeRangeByScore(0, true, windowStartTime, true); // [0, windowStartTime]
// 2. 添加當(dāng)前請(qǐng)求的時(shí)間戳到 ZSet
scoredSortedSet.add(currentTime, currentTime); // score 和 value 均為時(shí)間戳
// 3. 統(tǒng)計(jì)窗口內(nèi)請(qǐng)求數(shù)量
int count = scoredSortedSet.size();
return count > maxRequests;
}
在這個(gè)方法中,存在幾個(gè)問(wèn)題:
代碼中直接將時(shí)間戳作為ZSET的成員(member)和分?jǐn)?shù)(score),當(dāng)同一毫秒內(nèi)有多個(gè)請(qǐng)求時(shí),后寫(xiě)入的請(qǐng)求會(huì)覆蓋先前的請(qǐng)求(ZSET成員唯一),導(dǎo)致計(jì)數(shù)不準(zhǔn)確。
當(dāng)前操作序列:
在并發(fā)場(chǎng)景下,多個(gè)請(qǐng)求可能同時(shí)通過(guò)計(jì)數(shù)檢查,導(dǎo)致實(shí)際請(qǐng)求量超過(guò)閾值
- 刪除過(guò)期請(qǐng)求
- 添加當(dāng)前請(qǐng)求
- 獲取當(dāng)前計(jì)數(shù)
當(dāng)某個(gè)限流鍵長(zhǎng)時(shí)間無(wú)請(qǐng)求時(shí),對(duì)應(yīng)的空Z(yǔ)SET會(huì)永久占用內(nèi)存
那么優(yōu)化時(shí),可以利用UUID作為member 這樣不會(huì)出現(xiàn)覆蓋的情況,使用Lua腳本進(jìn)行執(zhí)行避免多個(gè)請(qǐng)求同時(shí)通過(guò)計(jì)數(shù)檢查的情況,針對(duì)問(wèn)題三可以通過(guò)設(shè)置過(guò)期時(shí)間來(lái)解決,優(yōu)化后的代碼如下:
private boolean isRateLimited(String key, int windowSize, TimeUnit timeUnit, int maxRequests) {
// 1. 計(jì)算窗口大?。ê撩耄?
long windowMillis = convertToMillis(windowSize, timeUnit);
// 2. 獲取當(dāng)前時(shí)間和窗口起始時(shí)間
long currentTime = System.currentTimeMillis();
long windowStartTime = currentTime - windowMillis;
// 3. 生成唯一請(qǐng)求ID
String requestId = UUID.randomUUID().toString();
// 4. 計(jì)算過(guò)期時(shí)間(秒)
long expireSeconds = calculateExpireSeconds(windowMillis);
// 5. Lua腳本(使用分?jǐn)?shù)范圍精確統(tǒng)計(jì))
String luaScript =
"redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[2])\n" + // 清理過(guò)期數(shù)據(jù)
"local count = redis.call('ZCOUNT', KEYS[1], ARGV[2], ARGV[1])\n" + // 精確統(tǒng)計(jì)窗口內(nèi)請(qǐng)求
"if count >= tonumber(ARGV[4]) then\n" +
" return 1\n" + // 觸發(fā)限流
"end\n" +
"redis.call('ZADD', KEYS[1], ARGV[1], ARGV[3])\n" + // 添加當(dāng)前請(qǐng)求
"redis.call('EXPIRE', KEYS[1], ARGV[5])\n" + // 設(shè)置過(guò)期時(shí)間
"return 0"; // 允許通過(guò)
try {
RScript script = redissonClient.getScript();
Long result = script.eval(
RScript.Mode.READ_WRITE,
luaScript,
RScript.ReturnType.INTEGER,
Collections.singletonList(key),
currentTime, windowStartTime, requestId, maxRequests, expireSeconds
);
return result != null && result == 1;
} catch (Exception e) {
log.error("限流服務(wù)異常,降級(jí)放行", e);
return false; // Redis故障時(shí)允許請(qǐng)求
}
}
private long calculateExpireSeconds(long windowMillis) {
// 過(guò)期時(shí)間 = 2 * 窗口大小(秒),向上取整
double expireSec = (windowMillis * 2.0) / 1000;
long result = (long) Math.ceil(expireSec);
return Math.max(1, result); // 至少1秒
}
Lua 腳本執(zhí)行邏輯:
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[2]):清理過(guò)期數(shù)據(jù),將 ZSET 中分?jǐn)?shù)小于等于窗口起始時(shí)間的成員刪除。local count = redis.call('ZCOUNT', KEYS[1], ARGV[2], ARGV[1]):精確統(tǒng)計(jì)窗口內(nèi)請(qǐng)求數(shù)量,即分?jǐn)?shù)在窗口起始時(shí)間和當(dāng)前時(shí)間之間的成員數(shù)量。if count >= tonumber(ARGV[4]) then return 1 end:如果統(tǒng)計(jì)的請(qǐng)求數(shù)量大于等于閾值,則返回 1,表示觸發(fā)限流。redis.call('ZADD', KEYS[1], ARGV[1], ARGV[3]):添加當(dāng)前請(qǐng)求,將當(dāng)前請(qǐng)求的唯一 ID 作為成員,當(dāng)前時(shí)間作為分?jǐn)?shù)添加到 ZSET 中。redis.call('EXPIRE', KEYS[1], ARGV[5]):設(shè)置 ZSET 的過(guò)期時(shí)間,避免長(zhǎng)時(shí)間無(wú)請(qǐng)求時(shí)空 ZSET 占用內(nèi)存。return 0:如果未觸發(fā)限流,則返回 0,表示允許請(qǐng)求通過(guò)。
我們從命令行可以看到,每一次請(qǐng)求后,都會(huì)在ZSET中多一條記錄,并且每次都會(huì)重置過(guò)期時(shí)間,當(dāng)觸發(fā)限流后,不再允許訪問(wèn)。
127.0.0.1:6379> ZRANGE rate_limit:#email.email:6888@example.com 0 -1 1) "fbd525dd-0e1e-4abf-a578-8c1207e8f6f0" 127.0.0.1:6379> TTL rate_limit:#email.email:6888@example.com (integer) 355 127.0.0.1:6379> ZRANGE rate_limit:#email.email:6888@example.com 0 -1 1) "fbd525dd-0e1e-4abf-a578-8c1207e8f6f0" 2) "a7c8d5c2-f4da-46ce-9f98-472ad702e1ad" 127.0.0.1:6379> TTL rate_limit:#email.email:6888@example.com (integer) 355 127.0.0.1:6379> ZRANGE rate_limit:#email.email:6888@example.com 0 -1 1) "fbd525dd-0e1e-4abf-a578-8c1207e8f6f0" 2) "a7c8d5c2-f4da-46ce-9f98-472ad702e1ad" 3) "12ed502c-6225-45bd-b85e-1d3ed9e46ef6" 127.0.0.1:6379> TTL rate_limit:#email.email:6888@example.com (integer) 355

到此這篇關(guān)于基于AOP+Redis的簡(jiǎn)易滑動(dòng)窗口限流的文章就介紹到這了,更多相關(guān)AOP+Redis滑動(dòng)窗口限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
@SpringBootConfiguration重復(fù)加載報(bào)錯(cuò)問(wèn)題解決
@SpringBootApplication?注解的?exclude?屬性用于排除特定的自動(dòng)配置類(lèi),而不是用于排除主配置類(lèi)本身,因此,不能通過(guò)?exclude?屬性來(lái)排除主配置類(lèi)的加載,這篇文章主要介紹了@SpringBootConfiguration重復(fù)加載報(bào)錯(cuò),需要的朋友可以參考下2024-08-08
Java8 中使用Stream 讓List 轉(zhuǎn) Map使用問(wèn)題小結(jié)
這篇文章主要介紹了Java8 中使用Stream 讓List 轉(zhuǎn) Map使用總結(jié),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-06-06
Hikari連接池使用SpringBoot配置JMX監(jiān)控實(shí)現(xiàn)
Hikari是Spring Boot默認(rèn)的數(shù)據(jù)庫(kù)連接池。區(qū)別于C3P0直接通過(guò)連接池對(duì)象獲取各項(xiàng)狀態(tài)指標(biāo),Hikari需要通過(guò)JMX來(lái)獲取。本文就詳細(xì)的來(lái)介紹一下,感興趣的可以了解一下2021-07-07
Spring?BeanFactory?與?FactoryBean?的區(qū)別詳情
這篇文章主要介紹了Spring?BeanFactory?與?FactoryBean?的區(qū)別詳情,BeanFactory?和?FactoryBean?的區(qū)別卻是一個(gè)很重要的知識(shí)點(diǎn),在本文中將結(jié)合源碼進(jìn)行分析講解,需要的小伙伴可以參考一下2022-05-05
Java8的Stream()與ParallelStream()的區(qū)別說(shuō)明
這篇文章主要介紹了Java8的Stream()與ParallelStream()的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Spring中配置文件XML驗(yàn)證錯(cuò)誤全面解決指南
這篇文章主要為大家詳細(xì)介紹了Spring中配置文件XML驗(yàn)證錯(cuò)誤全面解決方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-11-11
SpringBoot整合RabbitMQ實(shí)現(xiàn)延遲隊(duì)列和死信隊(duì)列
RabbitMQ的死信隊(duì)列用于接收其他隊(duì)列中的“死信”消息,所謂“死信”,是指滿(mǎn)足一定條件而無(wú)法被消費(fèi)者正確處理的消息,死信隊(duì)列通常與RabbitMQ的延遲隊(duì)列一起使用,本文給大家介紹了SpringBoot整合RabbitMQ實(shí)現(xiàn)延遲隊(duì)列和死信隊(duì)列,需要的朋友可以參考下2024-06-06
java實(shí)現(xiàn)的根據(jù)概率隨機(jī)中獎(jiǎng)測(cè)試類(lèi)
這篇文章主要介紹了java實(shí)現(xiàn)的根據(jù)概率隨機(jī)中獎(jiǎng)測(cè)試類(lèi),結(jié)合完整實(shí)例形式詳細(xì)分析了java隨機(jī)數(shù)實(shí)現(xiàn)概率運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2019-09-09

