# scaffold 项目之请求限流
# 简介
请求限流是一种通过控制单位时间内系统处理的请求数量,防止服务过载的技术手段。其核心目标包括:
- 保护系统稳定性:避免突发流量导致资源耗尽(如 CPU、内存、数据库连接)。
- 防止恶意攻击:如 DDoS 攻击或 API 滥用。
- 公平分配资源:确保所有用户或服务平等访问,避免少数用户占用过多资源。
# 常见的限流算法
| 算法 | 原理 | 特点 |
|---|---|---|
| 固定窗口 | 在固定时间窗口(如 1 分钟)内统计请求数,超过阈值则拒绝请求。 | 实现简单,但窗口切换时可能产生突发流量(如两窗口交界处请求翻倍)。 |
| 滑动窗口 | 将时间划分为更细粒度的子窗口(如每秒),动态统计最近 N 个子窗口的请求。 | 缓解固定窗口的突发问题,但计算复杂度稍高。 |
| 漏桶算法 | 请求以任意速率进入漏桶,以恒定速率流出(如每秒 10 次),桶满则拒绝请求。 | 输出速率恒定,适合需严格平滑流量的场景(如视频流处理)。 |
| 令牌桶算法 | 系统以固定速率生成令牌,请求需获取令牌才能处理,桶空则拒绝。 | 允许突发流量(如一次性消耗桶内所有令牌),适合秒杀等高并发场景。 |
# 典型应用场景
- API 限流:防止用户频繁调用接口(如第三方 API 调用次数限制)。
- 微服务保护:避免下游服务故障引发雪崩(如结合熔断机制)。
- 分布式系统协调:全局限制跨节点的总请求量(如使用 Redis 实现分布式限流)。
- 防止爬虫滥用:限制同一 IP 的访问频率。
# 开始使用
# 说明
本项目封装了 scaffold-spring-boot-starter-protection 组件, 由它的 ratelimiter 包来做幂等性,它提供了 声明式 的限流特性,可以防止请求过多。列如说,用户恶意疯狂点击某个按钮,导致发送了大量的请求。
声明式注解代码如下:
package com.tz.scaffold.framework.ratelimiter.core.annotation; | |
import com.tz.scaffold.framework.common.exception.enums.GlobalErrorCodeConstants; | |
import com.tz.scaffold.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; | |
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; | |
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* <p> Project: scaffold - RateLimiter </p> | |
* | |
* 限流注解 | |
* @author Tz | |
* @version 1.0.0 | |
* @date 2025/04/08 20:53 | |
* @since 1.0.0 | |
*/ | |
@Target({ElementType.METHOD}) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface RateLimiter { | |
/** | |
* 限流的时间,默认为 1 秒 | |
*/ | |
int time() default 1; | |
/** | |
* 时间单位,默认为 SECONDS 秒 | |
*/ | |
TimeUnit timeUnit() default TimeUnit.SECONDS; | |
/** | |
* 限流次数 | |
*/ | |
int count() default 100; | |
/** | |
* 提示信息,请求过快的提示 | |
* | |
* 为空时,使用 TOO_MANY_REQUESTS 错误提示 | |
* @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS | |
*/ | |
String message() default ""; | |
/** | |
* 使用的 Key 解析器 | |
* | |
* @see DefaultRateLimiterKeyResolver 全局级别 | |
* @see UserRateLimiterKeyResolver 用户 ID 级别 | |
* @see ClientIpRateLimiterKeyResolver 用户 IP 级别 | |
* @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别 | |
* @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg ()} 计算 | |
*/ | |
Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class; | |
/** | |
* 使用的 Key 参数 | |
*/ | |
String keyArg() default ""; | |
} |
实际使用
@RateLimiter(timeout = 10, timeUnit = TimeUnit.SECONDS, count = 2) | |
@Post("/user/add") | |
public String createUser(User user) { | |
userService.add(user); | |
return "添加成功"; | |
} |
上面注释的解释,10 秒钟内,所有用户只能操作两次
问题:如何指定用户或者 IP 在指定时间内限制请求呢?
自定义规则:
可设置该注解的 keyResolver 属性,可选择的有:
- DefaultRateLimiterKeyResolver: 全局级别
- UserRateLimiterKeyResolver: 用户 ID 级别
- ClientlpRateLimiterKeyResolver: 用户 IP 级别
- ServerNodeRateLimiterKeyResolver: 服务器 Node 级别
- ExpressionldempotentKeyResolver: 自定义级别,通过 keyArg 属性指定 Spring EL 表达式
# 实现原理
设计限流关键的几个点
- 限流粒度:按用户、IP、接口或全局维度控制。
- 超时策略:直接拒绝(返回 429 状态码)、排队等待或降级处理。
- 动态调整:根据系统负载自动调整阈值(如 CPU 使用率 >80% 时触发限流)。
- 监控与告警:记录限流事件,实时报警(如 Prometheus + Grafana)。
实现原理是针对相同的参数,一段时间内,有且仅能执行一次,这里能想到的就算用 redis,执行流程如下:
在执行方法前,判断参数对应的 key 是否超过限制:
- 如果
超过,则进行报错。 - 如果
未超过,则使用 Redis 计数 + 1
默认参数的 Redis Key 的计算规则由 DefaultRateLimiter 实现,使用 MD5 (方法名 + 方法参数),避免 RediskevresolverKey 过长。
# @RateLimiter 注解
package com.tz.scaffold.framework.ratelimiter.core.annotation; | |
import com.tz.scaffold.framework.common.exception.enums.GlobalErrorCodeConstants; | |
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; | |
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.impl.*; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* <p> Project: scaffold - RateLimiter </p> | |
* | |
* 限流注解 | |
* @author Tz | |
* @version 1.0.0 | |
* @date 2025/04/08 20:53 | |
* @since 1.0.0 | |
*/ | |
@Target({ElementType.METHOD}) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface RateLimiter { | |
/** | |
* 限流的时间,默认为 1 秒 | |
*/ | |
int time() default 1; | |
/** | |
* 时间单位,默认为 SECONDS 秒 | |
*/ | |
TimeUnit timeUnit() default TimeUnit.SECONDS; | |
/** | |
* 限流次数 | |
*/ | |
int count() default 100; | |
/** | |
* 提示信息,请求过快的提示 | |
* | |
* 为空时,使用 TOO_MANY_REQUESTS 错误提示 | |
* @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS | |
*/ | |
String message() default ""; | |
/** | |
* 使用的 Key 解析器 | |
* | |
* @see DefaultRateLimiterKeyResolver 全局级别 | |
* @see UserRateLimiterKeyResolver 用户 ID 级别 | |
* @see ClientIpRateLimiterKeyResolver 用户 IP 级别 | |
* @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别 | |
* @see ExpressionRateLimiterKeyResolver 自定义表达式,通过 {@link #keyArg ()} 计算 | |
*/ | |
Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class; | |
/** | |
* 使用的 Key 参数 | |
*/ | |
String keyArg() default ""; | |
} |
对应的切面处理:
package com.tz.scaffold.framework.ratelimiter.core.aop; | |
import cn.hutool.core.lang.Assert; | |
import cn.hutool.core.util.StrUtil; | |
import com.tz.scaffold.framework.common.exception.ServiceException; | |
import com.tz.scaffold.framework.common.exception.enums.GlobalErrorCodeConstants; | |
import com.tz.scaffold.framework.common.util.collection.CollectionUtils; | |
import com.tz.scaffold.framework.ratelimiter.core.annotation.RateLimiter; | |
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; | |
import com.tz.scaffold.framework.ratelimiter.core.redis.RateLimiterRedisDAO; | |
import lombok.extern.slf4j.Slf4j; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.annotation.Aspect; | |
import org.aspectj.lang.annotation.Before; | |
import java.util.List; | |
import java.util.Map; | |
/** | |
* <p> Project: scaffold - RateLimiterAspect </p> | |
* | |
* 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作 | |
* @author Tz | |
* @version 1.0.0 | |
* @date 2025/04/08 21:04 | |
* @since 1.0.0 | |
*/ | |
@Aspect | |
@Slf4j | |
public class RateLimiterAspect { | |
/** | |
* RateLimiterKeyResolver 集合 | |
*/ | |
private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers; | |
private final RateLimiterRedisDAO rateLimiterRedisDAO; | |
public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { | |
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass); | |
this.rateLimiterRedisDAO = rateLimiterRedisDAO; | |
} | |
@Before("@annotation(rateLimiter)") | |
public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) { | |
// 获得 IdempotentKeyResolver 对象 | |
RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver()); | |
Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver"); | |
// 解析 Key | |
String key = keyResolver.resolver(joinPoint, rateLimiter); | |
// 获取 1 次限流 | |
boolean success = rateLimiterRedisDAO.tryAcquire(key, | |
rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit()); | |
if (!success) { | |
log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs()); | |
String message = StrUtil.blankToDefault(rateLimiter.message(), | |
GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg()); | |
throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message); | |
} | |
} | |
} |
生成 key 和对应的写入 redis 操作:
package com.tz.scaffold.framework.ratelimiter.core.redis; | |
import lombok.AllArgsConstructor; | |
import org.redisson.api.*; | |
import java.util.Objects; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* <p> Project: scaffold - RateLimiterRedisDAO </p> | |
* | |
* 限流 Redis DAO | |
* @author Tz | |
* @version 1.0.0 | |
* @date 2025/04/09 9:22 | |
* @since 1.0.0 | |
*/ | |
@AllArgsConstructor | |
public class RateLimiterRedisDAO { | |
/** | |
* 限流操作 | |
* | |
* KEY 格式:rate_limiter:% s // 参数为 uuid | |
* VALUE 格式:String | |
* 过期时间:不固定 | |
*/ | |
private static final String RATE_LIMITER = "rate_limiter:%s"; | |
private final RedissonClient redissonClient; | |
public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) { | |
// 1. 获得 RRateLimiter,并设置 rate 速率 | |
RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit); | |
// 2. 尝试获取 1 个 | |
return rateLimiter.tryAcquire(); | |
} | |
private static String formatKey(String key) { | |
return String.format(RATE_LIMITER, key); | |
} | |
private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) { | |
String redisKey = formatKey(key); | |
RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey); | |
long rateInterval = timeUnit.toSeconds(time); | |
// 1. 如果不存在,设置 rate 速率 | |
RateLimiterConfig config = rateLimiter.getConfig(); | |
if (config == null) { | |
rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); | |
rateLimiter.expire(rateInterval, TimeUnit.SECONDS); | |
return rateLimiter; | |
} | |
// 2. 如果存在,并且配置相同,则直接返回 | |
if (config.getRateType() == RateType.OVERALL | |
&& Objects.equals(config.getRate(), count) | |
&& Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) { | |
return rateLimiter; | |
} | |
// 3. 如果存在,并且配置不同,则进行新建 | |
rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); | |
rateLimiter.expire(rateInterval, TimeUnit.SECONDS); | |
return rateLimiter; | |
} | |
} |
# 使用示例
在需要使用的地方引入组件
<dependency> | |
<groupId>com.tz.boot</groupId> | |
<artifactId>scaffold-spring-boot-starter-protection</artifactId> | |
</dependency> |
声明式注解
@RateLimiter(timeout = 10, timeUnit = TimeUnit.SECONDS, count = 2) |
# 总结
请求限流是保障系统高可用的关键措施,需结合业务场景选择合适的算法和工具。平衡系统保护与用户体验,通过监控和动态调整实现智能化流量控制。