从零实现单机限流,Java 后端必备技能
一、为什么要限流
- 防止雪崩(单机限流能避免大量请求瞬间打满线程、数据库连接与 IO 资源,拖垮自身及下游服务,通过舍弃少量请求,防止系统整体雪崩,保住核心业务可用。)
- 防止资源耗尽(单机限流可以把请求量控制在服务器承受范围内,避免因 CPU 爆满、内存溢出 OOM、线程池耗尽而导致系统直接崩溃。)
- 防御天降流量(单机限流能自动拦截前端 BUG、爬虫误抓、测试忘关脚本、用户狂点等各类意外突发流量,避免服务被非恶意流量打崩。)
- 保证公平(单机限流可限制单用户 / 单 IP 的访问频率,避免个别用户耗尽全部资源,保障其他用户正常使用,实现公平访问。)
- 流量削峰(单机限流可通过令牌桶等方式削峰填谷,把瞬间突增的流量毛刺拉平,让系统以稳定速度处理请求,避免响应剧烈抖动。)
- 二、具体实现
这里我们将采用AOP(面向切面编程)+ 自定义注解,我们可以将限流逻辑从业务代码中剥离,实现声明式限流。
1、pom.xml引入jar包
com.google.guava guava 33.0.0-jre <!-- 请使用最新稳定版 --> 2、定义自定义注解 @RateLimit
import java.lang.annotation.*;@Target(ElementType.METHOD) // 只能用在方法上@Retention(RetentionPolicy.RUNTIME) // 运行时可见@Documentedpublic @interface RateLimit { /** * 每秒允许的请求数 (QPS) * 默认值为 5 */ double qps() default 5.0; /** * 超时等待时间 (毫秒),如果为 0 则立即拒绝,大于 0 则尝试等待 * 默认 0 (立即拒绝) */ long timeout() default 0;}3、创建 AOP 切面 RateLimitAspect
import com.google.common.util.concurrent.RateLimiter;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.web.server.ResponseStatusException;import java.lang.reflect.Method;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;@Aspect@Componentpublic class RateLimitAspect { // 存储每个方法对应的 RateLimiter 实例 // Key: 方法签名 (类名 + 方法名), Value: RateLimiter private final Map limiterMap = new ConcurrentHashMap<>(); @Around("@annotation(com.example.demo.RateLimit)") // 替换为你的包路径 public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 获取注解信息 RateLimit rateLimit = method.getAnnotation(RateLimit.class); double qps = rateLimit.qps(); long timeout = rateLimit.timeout(); // 生成唯一的 Key (类名 + 方法名) // 注意:如果同一个方法需要不同的 QPS,这里逻辑需调整(通常注解值固定,所以没问题) String key = signature.getDeclaringTypeName() + "." + signature.getName(); // 获取或创建 RateLimiter (双重检查锁或直接 computeIfAbsent) RateLimiter limiter = limiterMap.computeIfAbsent(key, k -> RateLimiter.create(qps)); // 【重要】动态调整:如果注解的 QPS 变了,更新现有的 RateLimiter // Guava 的 setRate 是线程安全的,可以动态调整 limiter.setRate(qps); boolean acquired; if (timeout > 0) { // 尝试等待获取令牌 acquired = limiter.tryAcquire(timeout, TimeUnit.MILLISECONDS); } else { // 立即尝试获取,拿不到就失败 acquired = limiter.tryAcquire(); } if (!acquired) { // 限流触发,抛出异常或返回特定结果 throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "请求过于频繁,当前限制为 " + qps + " QPS"); } // 获取到令牌,执行目标方法 return joinPoint.proceed(); }} 温馨提示:要记得引入aop的jar包哦!
org.springframework.boot spring-boot-starter-aop 4、自定义注解使用
import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class FlexibleController { /** * 场景 1:严格限流,每秒最多 2 个请求,超出的直接拒绝 */ @GetMapping("/strict-api") @RateLimit(qps = 2.0, timeout = 0) public String strictApi() { return "严格模式:每秒仅限 2 次访问"; } /** * 场景 2:宽松限流,每秒 10 个请求,如果满了最多等待 500ms * 适用于用户体验优先,稍微慢点也能接受的场景 */ @GetMapping("/smooth-api") @RateLimit(qps = 10.0, timeout = 500) public String smoothApi() throws InterruptedException { // 模拟业务耗时 Thread.sleep(100); return "平滑模式:每秒 10 次,支持排队等待"; } /** * 场景 3:使用默认值 (默认 qps=5, timeout=0) */ @GetMapping("/default-api") @RateLimit public String defaultApi() { return "默认模式:每秒 5 次访问"; }}三、扩展
@RateLimit 通常是针对“整个接口”的全局限流(即所有请求共享一个令牌桶)。
1、扩展1:实现“单 IP 限流”

@Around("@annotation(rateLimit)")public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { // 1. 获取当前请求对象 (需要注入 RequestAttributes 或 HttpServletRequest) HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); // 2. 获取客户端 IP (处理代理情况) String ip = getClientIp(request); // 3. 【关键修改】构造唯一的 Key:方法名 + IP // 这样每个 IP 都会生成一个独立的 RateLimiter 实例 String uniqueKey = joinPoint.getSignature().toShortString() + ":" + ip; // 4. 获取或创建该 IP 专属的限流器 // 注意:这里需要设置缓存过期策略,否则内存会爆(见下文注意事项) RateLimiter limiter = limiterMap.computeIfAbsent(uniqueKey, k -> RateLimiter.create(rateLimit.qps())); if (!limiter.tryAcquire()) { throw new RuntimeException("您的访问过于频繁,请稍后再试 (IP: " + ip + ")"); } return joinPoint.proceed();}// 辅助方法:获取真实 IPprivate String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); // 最终兜底 } // 如果是多级代理,取第一个 IP if (ip != null && ip.contains(",")) { ip = ip.split(",")[0]; } return ip;}⚠️ 重要隐患:内存泄漏风险
优化方案:使用带过期时间的缓存,不要直接用ConcurrentHashMap,改用 Guava Cache或 Caffeine,它们支持自动过期。
// 定义一个带过期时间的缓存:key 存在 60 秒后自动删除// 如果某个 IP 60 秒内没再访问,它的限流器就被回收了,节省内存LoadingCache limiterCache = CacheBuilder.newBuilder() .expireAfterAccess(60, TimeUnit.SECONDS) // 关键:60 秒无访问自动移除 .build(new CacheLoader() { @Override public RateLimiter load(String key) { // 这里从 key 中解析出配置的 qps,或者使用默认值 return RateLimiter.create(10.0); } });// 在切面中使用RateLimiter limiter = limiterCache.get(uniqueKey); // 自动处理创建和过期 2、扩展2:实现“限制 用户 ID限流”
// 伪代码:AOP 切面逻辑public Object around(ProceedingJoinPoint joinPoint) { // 1. 尝试获取用户 ID (从 Token 解析) String userId = UserContext.getCurrentUserId(); // 2. 如果没登录,降级为 IP 限流 if (userId == null) { userId = getClientIp(request); } // 3. 构造 Key:方法名 + 用户标识 String key = joinPoint.getSignature().toShortString() + ":" + userId; // 4. 执行限流 (配合 Caffeine/Guava Cache 做过期处理) RateLimiter limiter = cache.get(key); if (!limiter.tryAcquire()) { throw new TooManyRequestsException("操作太频繁了,请休息一下"); } return joinPoint.proceed();}今天,你学废了吗
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有