何為請(qǐng)求限流?
請(qǐng)求限流是一種控制API或其他Web服務(wù)的流量的技術(shù)。它的目的是限制客戶端對(duì)服務(wù)器發(fā)出的請(qǐng)求的數(shù)量或速率,以防止服務(wù)器過載或響應(yīng)時(shí)間變慢,從而提高系統(tǒng)的可用性和穩(wěn)定性。
中小型項(xiàng)目請(qǐng)求限流的需求
- 按IP、用戶、全局限流
- 基于不同實(shí)現(xiàn)的限流設(shè)計(jì)(基于Redis或者LRU緩存)
- 基于注解標(biāo)注哪些接口限流
完整限流設(shè)計(jì)實(shí)現(xiàn)在開源項(xiàng)目中:https://github.com/valarchie/AgileBoot-Back-End
注解設(shè)計(jì)
聲明一個(gè)注解類,主要有以下幾個(gè)屬性
- key(緩存的key)
- time(時(shí)間范圍)
- maxCount(時(shí)間范圍內(nèi)最大的請(qǐng)求次數(shù))
- limitType(按IP/用戶/全局進(jìn)行限流)
- cacheType(基于Redis或者M(jìn)ap來實(shí)現(xiàn)限流)
/**
* 限流注解
*
* @author valarchie
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流key
*/
String key() default "None";
/**
* 限流時(shí)間,單位秒
*/
int time() default 60;
/**
* 限流次數(shù)
*/
int maxCount() default 100;
/**
* 限流條件類型
*/
LimitType limitType() default LimitType.GLOBAL;
/**
* 限流使用的緩存類型
*/
CacheType cacheType() default CacheType.REDIS;
}
LimitType枚舉,我們可以將不同限制類型的邏輯直接放在枚舉類當(dāng)中。推薦將邏輯直接放置在枚舉類中,代碼的組織形式會(huì)更好。
enum LimitType {
/**
* 默認(rèn)策略全局限流 不區(qū)分IP和用戶
*/
GLOBAL{
@Override
public String generateCombinedKey(RateLimit rateLimiter) {
return rateLimiter.key() + this.name();
}
},
/**
* 根據(jù)請(qǐng)求者IP進(jìn)行限流
*/
IP {
@Override
public String generateCombinedKey(RateLimit rateLimiter) {
String clientIP = ServletUtil.getClientIP(ServletHolderUtil.getRequest());
return rateLimiter.key() + clientIP;
}
},
/**
* 按用戶限流
*/
USER {
@Override
public String generateCombinedKey(RateLimit rateLimiter) {
LoginUser loginUser = AuthenticationUtils.getLoginUser();
if (loginUser == null) {
throw new ApiException(ErrorCode.Client.COMMON_NO_AUTHORIZATION);
}
return rateLimiter.key() + loginUser.getUsername();
}
};
public abstract String generateCombinedKey(RateLimit rateLimiter);
}
CacheType, 主要分為Redis和Map, 后續(xù)有新的類型可以新增。
enum CacheType {
/**
* 使用redis做緩存
*/
REDIS,
/**
* 使用map做緩存
*/
Map
}
RateLimitChecker設(shè)計(jì)
聲明一個(gè)抽象類,然后將具體實(shí)現(xiàn)放在實(shí)現(xiàn)類中,便于擴(kuò)展
/**
* @author valarchie
*/
public abstract class AbstractRateLimitChecker {
/**
* 檢查是否超出限流
* @param rateLimiter
*/
public abstract void check(RateLimit rateLimiter);
}
Redis限流實(shí)現(xiàn)
/**
* @author valarchie
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class RedisRateLimitChecker extends AbstractRateLimitChecker{
@NonNull
private RedisTemplate<Object, Object> redisTemplate;
private final RedisScript<Long> limitScript = new DefaultRedisScript<>(limitScriptText(), Long.class);
@Override
public void check(RateLimit rateLimiter) {
int maxCount = rateLimiter.maxCount();
String combineKey = rateLimiter.limitType().generateCombinedKey(rateLimiter);
Long currentCount;
try {
currentCount = redisTemplate.execute(limitScript, ListUtil.of(combineKey), maxCount, rateLimiter.time());
log.info("限制請(qǐng)求:{}, 當(dāng)前請(qǐng)求次數(shù):{}, 緩存key:{}", combineKey, currentCount, rateLimiter.key());
} catch (Exception e) {
throw new RuntimeException("redis限流器異常,請(qǐng)確保redis啟動(dòng)正常");
}
if (currentCount == null) {
throw new RuntimeException("redis限流器異常,請(qǐng)稍后再試");
}
if (currentCount.intValue() > maxCount) {
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_TOO_OFTEN);
}
}
/**
* 限流腳本
*/
private static String limitScriptText() {
return "local key = KEYS[1]\n" +
"local count = tonumber(ARGV[1])\n" +
"local time = tonumber(ARGV[2])\n" +
"local current = redis.call('get', key);\n" +
"if current and tonumber(current) > count then\n" +
" return tonumber(current);\n" +
"end\n" +
"current = redis.call('incr', key)\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', key, time)\n" +
"end\n" +
"return tonumber(current);";
}
}
Map + Guava RateLimiter實(shí)現(xiàn)
/**
* @author valarchie
*/
@SuppressWarnings("UnstableApiUsage")
@Component
@RequiredArgsConstructor
@Slf4j
public class MapRateLimitChecker extends AbstractRateLimitChecker{
/**
* 最大僅支持4096個(gè)key 超出這個(gè)key 限流將可能失效
*/
private final LRUCache<String, RateLimiter> cache = new LRUCache<>(4096);
@Override
public void check(RateLimit rateLimit) {
String combinedKey = rateLimit.limitType().generateCombinedKey(rateLimit);
RateLimiter rateLimiter = cache.get(combinedKey,
() -> RateLimiter.create((double) rateLimit.maxCount() / rateLimit.time())
);
if (!rateLimiter.tryAcquire()) {
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_TOO_OFTEN);
}
log.info("限制請(qǐng)求key:{}, combined key:{}", rateLimit.key(), combinedKey);
}
}
限流切面
我們需要在切面中,讀取限流注解標(biāo)注的信息,然后選擇不同的限流實(shí)現(xiàn)來進(jìn)行限流。
/**
* 限流切面處理
*
* @author valarchie
*/
@Aspect
@Component
@Slf4j
@ConditionalOnExpression("'${agileboot.embedded.redis}' != 'true'")
@RequiredArgsConstructor
public class RateLimiterAspect {
@NonNull
private RedisRateLimitChecker redisRateLimitChecker;
@NonNull
private MapRateLimitChecker mapRateLimitChecker;
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimit rateLimiter) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
log.info("當(dāng)前限流方法:" + method.toGenericString());
switch (rateLimiter.cacheType()) {
case REDIS:
redisRateLimitChecker.check(rateLimiter);
break;
case Map:
mapRateLimitChecker.check(rateLimiter);
return;
default:
redisRateLimitChecker.check(rateLimiter);
}
}
}
注解使用
以下是我們標(biāo)注的注解例子。
time=10,maxCount=10表明10秒內(nèi)最多10次請(qǐng)求。
cacheType=Redis表明使用Redis來實(shí)現(xiàn)。
limitType=IP表明基于IP來限流。文章來源:http://www.zghlxwxcb.cn/news/detail-409888.html
/**
* 生成驗(yàn)證碼
*/
@Operation(summary = "驗(yàn)證碼")
@RateLimit(key = RateLimitKey.LOGIN_CAPTCHA_KEY, time = 10, maxCount = 10, cacheType = CacheType.REDIS,
limitType = LimitType.IP)
@GetMapping("/captchaImage")
public ResponseDTO<CaptchaDTO> getCaptchaImg() {
CaptchaDTO captchaImg = loginService.generateCaptchaImg();
return ResponseDTO.ok(captchaImg);
}
這是筆者關(guān)于中小型項(xiàng)目關(guān)于請(qǐng)求限流的實(shí)現(xiàn),如有不足歡迎大家評(píng)論指正。文章來源地址http://www.zghlxwxcb.cn/news/detail-409888.html
全棧技術(shù)交流群:1398880
到了這里,關(guān)于中小型項(xiàng)目請(qǐng)求限流設(shè)計(jì)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!