🚀 什么是限流
限流是一种控制系统访问频率的技术手段,就像高速公路的收费站控制车流量一样。
生活场景类比:
- 银行ATM机:每张卡每天最多取款5次
- 手机验证码:每个手机号每分钟最多发送1条
- 网站登录:每个IP每分钟最多尝试5次
技术价值:
- 防止恶意攻击:阻止暴力破解、恶意爬虫
- 保护系统稳定:避免瞬间大量请求压垮服务器
- 提升用户体验:确保正常用户的访问质量
- 节约成本:减少不必要的资源消耗
🏗️ 系统架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 用户请求 │───→│ 限流切面 │───→│ 业务接口 │
│ (HTTP API) │ │ (AOP拦截) │ │ (Controller) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 限流服务 │
│ (核心逻辑处理) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 缓存存储 │
│ (EhCache/Redis) │
└─────────────────┘
工作流程:
- 用户发起HTTP请求
- Spring AOP切面拦截带有@RateLimiter注解的方法
- 限流服务根据注解配置生成限流键
- 从缓存中获取当前访问次数
- 判断是否超过限制,决定放行或拒绝
- 更新缓存中的计数器
🔧 核心组件详解
1. 限流注解 (@RateLimiter)
这是系统的核心注解,定义了限流的各种参数:
package cn.jbolt.config.anno.rateLimiter;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RateLimiter {
/**
* 缓存前缀 - 用于区分不同业务的限流数据
*/
String prefix() default "jblimit:";
/**
* 时间窗口(秒) - 限流的时间范围
*/
int time() default 60;
/**
* 允许访问次数 - 时间窗口内最大访问次数
*/
@AliasFor(attribute = "count")
int value() default 12;
/**
* 限制类型 - 决定按什么维度限流
*/
RateLimitType limitType() default RateLimitType.DEFAULT;
/**
* 限制提示消息 - 触发限流时返回的错误信息
*/
String msg() default "操作过于频繁,请稍后重试";
/**
* 允许访问次数 - 与value互为别名
*/
@AliasFor(attribute = "value")
int count() default 12;
/**
* 自定义键 - 当limitType为CUSTOM时使用
*/
String customKey() default "";
/**
* 是否启用 - 可用于动态开关限流功能
*/
boolean enabled() default true;
/**
* 额外的时间窗口限制(秒)
* 实现双重限流:比如1秒最多1次 + 1分钟最多10次
*/
int extraTime() default -1;
/**
* 额外时间窗口内的允许访问次数
*/
int extraCount() default -1;
/**
* 额外限制的提示消息
*/
String extraMsg() default "";
}
2. 限流类型枚举 (RateLimitType)
package cn.jbolt.config.anno.rateLimiter;
public enum RateLimitType {
/**
* 默认限制(全局)
* 所有请求共享一个计数器
*/
DEFAULT,
/**
* 基于IP地址限制
* 每个IP独立计数
*/
IP,
/**
* 基于用户ID限制
* 每个登录用户独立计数
*/
USER,
/**
* 基于自定义KEY限制
* 根据业务逻辑自定义限流维度
*/
CUSTOM
}
3. 限流异常类 (RateLimitException)
package cn.jbolt.config.exception;
public class RateLimitException extends RuntimeException {
private final String message;
private final int retryAfter;
public RateLimitException(String message) {
this(message, 0);
}
public RateLimitException(String message, int retryAfter) {
super(message);
this.message = message;
this.retryAfter = retryAfter;
}
@Override
public String getMessage() {
return message;
}
public int getRetryAfter() {
return retryAfter;
}
}
4. 全局异常处理器 (RateLimitExceptionHandler)
package cn.jbolt.config.handler;
import cn.jbolt.config.exception.RateLimitException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class RateLimitExceptionHandler {
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<Map<String, Object>> handleRateLimitException(
RateLimitException e, HttpServletResponse response) {
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
result.put("message", e.getMessage());
result.put("data", null);
// 设置HTTP响应头,告诉客户端多久后可以重试
if (e.getRetryAfter() > 0) {
response.setHeader("Retry-After", String.valueOf(e.getRetryAfter()));
}
response.setHeader("X-RateLimit-Window", "60");
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result);
}
}
5. IP工具类 (IpUtils)
package cn.jbolt.util;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
private static final String[] IP_HEADER_NAMES = {
"X-Forwarded-For",
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IPV4 = "127.0.0.1";
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
/**
* 获取客户端真实IP地址
* 处理代理服务器、负载均衡器等场景
*/
public static String getClientIp(HttpServletRequest request) {
if (request == null) {
return UNKNOWN;
}
String ip = null;
// 依次检查各种可能的IP头
for (String header : IP_HEADER_NAMES) {
ip = request.getHeader(header);
if (isValidIp(ip)) {
break;
}
}
// 如果头信息中没有找到,则使用getRemoteAddr
if (!isValidIp(ip)) {
ip = request.getRemoteAddr();
if (LOCALHOST_IPV6.equals(ip)) {
ip = LOCALHOST_IPV4;
}
}
// 处理多个IP的情况(X-Forwarded-For可能包含多个IP)
if (StringUtils.hasText(ip) && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return StringUtils.hasText(ip) ? ip : UNKNOWN;
}
/**
* 检查IP是否有效
*/
private static boolean isValidIp(String ip) {
return StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip);
}
}
🎯 技术实现原理
1. AOP切面拦截
系统使用Spring AOP在方法执行前进行拦截,这是一个核心的限流切面类:
package cn.jbolt.config.aspect;
import cn.jbolt.config.anno.rateLimiter.RateLimiter;
import cn.jbolt.config.anno.rateLimiter.RateLimitType;
import cn.jbolt.config.exception.RateLimitException;
import cn.jbolt.util.IpUtils;
import cn.jbolt.util.cache.RateLimiterCache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class RateLimiterAspect {
private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class);
@Around("@annotation(rateLimiter)")
public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
// 检查是否启用限流
if (!rateLimiter.enabled()) {
return point.proceed();
}
// 获取HTTP请求对象
HttpServletRequest request = getCurrentRequest();
if (request == null) {
logger.warn("无法获取HttpServletRequest,跳过限流检查");
return point.proceed();
}
// 生成限流键
String limitKey = generateLimitKey(point, rateLimiter, request);
// 执行主要限流检查
checkRateLimit(limitKey, rateLimiter.time(), rateLimiter.count(), rateLimiter.msg());
// 执行额外限流检查(如果配置了)
if (rateLimiter.extraTime() > 0 && rateLimiter.extraCount() > 0) {
String extraLimitKey = limitKey + ":extra";
String extraMsg = rateLimiter.extraMsg().isEmpty() ? rateLimiter.msg() : rateLimiter.extraMsg();
checkRateLimit(extraLimitKey, rateLimiter.extraTime(), rateLimiter.extraCount(), extraMsg);
}
// 所有限流检查通过,继续执行业务方法
return point.proceed();
}
/**
* 执行限流检查
*/
private void checkRateLimit(String key, int timeWindow, int maxCount, String message) {
try {
// 增加计数器并获取当前访问次数
int currentCount = RateLimiterCache.incrementAndGet(key, timeWindow, TimeUnit.SECONDS);
logger.debug("限流检查: key={}, 当前次数={}, 限制次数={}", key, currentCount, maxCount);
// 检查是否超过限制
if (currentCount > maxCount) {
long ttl = RateLimiterCache.getTtl(key);
logger.warn("触发限流: key={}, 当前次数={}, 限制次数={}, 剩余时间={}秒",
key, currentCount, maxCount, ttl);
throw new RateLimitException(message, (int) ttl);
}
} catch (RateLimitException e) {
throw e;
} catch (Exception e) {
logger.error("限流检查异常: key={}", key, e);
// 限流服务异常时,选择放行而不是阻塞
}
}
/**
* 生成限流键
*/
private String generateLimitKey(ProceedingJoinPoint point, RateLimiter rateLimiter, HttpServletRequest request) {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(rateLimiter.prefix());
// 添加方法签名
String methodSignature = point.getSignature().toShortString();
keyBuilder.append(methodSignature);
// 根据限流类型添加不同的标识
switch (rateLimiter.limitType()) {
case IP:
keyBuilder.append(":ip:").append(IpUtils.getClientIp(request));
break;
case USER:
String userId = getCurrentUserId(request);
keyBuilder.append(":user:").append(userId != null ? userId : "anonymous");
break;
case CUSTOM:
keyBuilder.append(":custom:").append(rateLimiter.customKey());
break;
case DEFAULT:
default:
keyBuilder.append(":default:global");
break;
}
// 添加时间窗口,确保不同时间窗口的限流独立
keyBuilder.append(":").append(rateLimiter.time());
String finalKey = keyBuilder.toString();
logger.debug("生成限流键: {}", finalKey);
return finalKey;
}
/**
* 获取当前HTTP请求
*/
private HttpServletRequest getCurrentRequest() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attrs != null ? attrs.getRequest() : null;
} catch (Exception e) {
logger.warn("获取HttpServletRequest失败", e);
return null;
}
}
/**
* 获取当前用户ID
* 这里需要根据实际的用户认证体系来实现
*/
private String getCurrentUserId(HttpServletRequest request) {
// 方案1:从Session中获取
Object userId = request.getSession().getAttribute("userId");
if (userId != null) {
return userId.toString();
}
// 方案2:从JWT Token中获取
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
// 解析JWT获取用户ID
// return JwtUtils.getUserIdFromToken(token);
}
// 方案3:从请求参数中获取
String userIdParam = request.getParameter("userId");
if (userIdParam != null) {
return userIdParam;
}
return null;
}
}
2. 缓存数据结构
系统使用一个包装类来存储缓存数据:
package cn.jbolt.util.cache;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
public class CacheWrapper implements Serializable {
private static final long serialVersionUID = 1L;
private Object value;
private long timestamp;
private long durationMillis;
public CacheWrapper() {
}
public CacheWrapper(Object value, long duration, TimeUnit unit) {
this.value = value;
this.timestamp = System.currentTimeMillis();
this.durationMillis = unit.toMillis(duration);
}
/**
* 检查是否已过期
*/
public boolean isExpired() {
return System.currentTimeMillis() - timestamp > durationMillis;
}
/**
* 获取剩余过期时间(毫秒)
*/
public long getRemainingTime() {
long elapsed = System.currentTimeMillis() - timestamp;
return Math.max(0, durationMillis - elapsed);
}
// getter和setter方法
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public long getDurationMillis() {
return durationMillis;
}
public void setDurationMillis(long durationMillis) {
this.durationMillis = durationMillis;
}
}
📝 完整代码示例
1. 控制器示例
package cn.jbolt.controller;
import cn.jbolt.config.anno.rateLimiter.RateLimiter;
import cn.jbolt.config.anno.rateLimiter.RateLimitType;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class DemoController {
/**
* 登录接口 - 防止暴力破解
* 每个IP每分钟最多尝试5次
*/
@PostMapping("/login")
@RateLimiter(
limitType = RateLimitType.IP,
time = 60,
count = 5,
msg = "登录尝试过于频繁,请1分钟后重试"
)
public Result login(@RequestBody LoginRequest request) {
// 登录逻辑
if (isValidUser(request.getUsername(), request.getPassword())) {
return Result.success("登录成功");
} else {
return Result.error("用户名或密码错误");
}
}
/**
* 发送验证码 - 防止恶意发送
* 每个IP每分钟最多3次
*/
@PostMapping("/sms/send")
@RateLimiter(
limitType = RateLimitType.IP,
time = 60,
count = 3,
msg = "验证码发送过于频繁,请稍后重试"
)
public Result sendSms(@RequestBody SmsRequest request) {
// 发送短信逻辑
boolean success = smsService.sendCode(request.getPhone());
return success ? Result.success("发送成功") : Result.error("发送失败");
}
/**
* 查询接口 - 防止爬虫
* 每个IP每分钟最多100次
*/
@GetMapping("/products")
@RateLimiter(
limitType = RateLimitType.IP,
time = 60,
count = 100,
msg = "查询过于频繁,请稍后重试"
)
public Result getProducts(@RequestParam(defaultValue = "1") int page) {
// 查询商品逻辑
List<Product> products = productService.getProducts(page);
return Result.success(products);
}
/**
* 用户操作 - 防止频繁操作
* 每个用户每分钟最多30次
*/
@PostMapping("/user/update")
@RateLimiter(
limitType = RateLimitType.USER,
time = 60,
count = 30,
msg = "操作过于频繁,请稍后重试"
)
public Result updateUser(@RequestBody UserUpdateRequest request) {
// 更新用户信息逻辑
boolean success = userService.updateUser(request);
return success ? Result.success("更新成功") : Result.error("更新失败");
}
/**
* 关键操作 - 严格限流
* 1秒最多1次 + 1分钟最多5次
*/
@PostMapping("/transfer")
@RateLimiter(
limitType = RateLimitType.USER,
time = 1, count = 1, msg = "操作过于频繁,请稍后再试",
extraTime = 60, extraCount = 5, extraMsg = "您在1分钟内的操作次数已达上限"
)
public Result transfer(@RequestBody TransferRequest request) {
// 转账逻辑
boolean success = transferService.transfer(request);
return success ? Result.success("转账成功") : Result.error("转账失败");
}
/**
* 自定义限流 - 按商品限制
* 每个商品每分钟最多下单20次
*/
@PostMapping("/order/{productId}")
@RateLimiter(
limitType = RateLimitType.CUSTOM,
customKey = "product_order",
time = 60,
count = 20,
msg = "该商品下单过于频繁,请稍后重试"
)
public Result createOrder(@PathVariable String productId, @RequestBody OrderRequest request) {
// 创建订单逻辑
Order order = orderService.createOrder(productId, request);
return Result.success(order);
}
// 辅助方法
private boolean isValidUser(String username, String password) {
// 实际的用户验证逻辑
return "admin".equals(username) && "123456".equals(password);
}
}
2. 统一返回对象
package cn.jbolt.common;
public class Result {
private int code;
private String message;
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.code = 200;
result.message = "success";
result.data = data;
return result;
}
public static Result error(String message) {
Result result = new Result();
result.code = 500;
result.message = message;
result.data = null;
return result;
}
// getter和setter方法
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
🎮 使用指南
1. 基本使用
// 最简单的用法 - 使用默认配置
@RateLimiter(limitType = RateLimitType.IP)
public String simpleApi() {
return "success";
}
// 自定义时间窗口和次数
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, // 60秒
count = 100 // 最多100次
)
public String customApi() {
return "success";
}
2. 不同场景的配置建议
// 登录接口 - 严格限制
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, count = 5,
msg = "登录尝试过于频繁,请1分钟后重试"
)
// 查询接口 - 适中限制
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, count = 100,
msg = "查询过于频繁,请稍后重试"
)
// 用户操作 - 按用户限制
@RateLimiter(
limitType = RateLimitType.USER,
time = 60, count = 30,
msg = "操作过于频繁,请稍后重试"
)
// 全局保护 - 系统级限制
@RateLimiter(
limitType = RateLimitType.DEFAULT,
time = 60, count = 200,
msg = "系统繁忙,请稍后重试"
)
3. 双重限流配置
// 严格的双重限流:秒级 + 分钟级
@RateLimiter(
limitType = RateLimitType.IP,
time = 1, count = 1, msg = "请求过于频繁,请稍后再试",
extraTime = 60, extraCount = 10, extraMsg = "您在1分钟内的请求次数已达上限"
)
// 适中的双重限流:分钟级 + 小时级
@RateLimiter(
limitType = RateLimitType.IP,
time = 60, count = 100, msg = "1分钟内请求过多",
extraTime = 3600, extraCount = 1000, extraMsg = "1小时内请求过多"
)