外观
Spring Boot 接口限流
约 2335 字大约 8 分钟
2025-08-16
一、限流基础概念
1. 为什么要限流
限流定义:当高并发或瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或延迟处理请求为代价,保证系统整体服务可用。
典型场景:
- 流量洪峰:秒杀活动、促销活动导致的流量激增
- 系统瓶颈:后端服务处理能力有限
- 恶意攻击:CC 攻击、爬虫等
- 依赖服务故障:下游服务响应变慢导致请求堆积
限流 vs 降级 vs 熔断:
| 概念 | 目的 | 实现方式 |
|---|---|---|
| 限流 | 控制进入系统的请求数量 | 令牌桶、漏桶等 |
| 降级 | 在系统压力过大时,暂时关闭非核心功能 | 返回默认值、简化逻辑 |
| 熔断 | 防止故障扩散,快速失败 | 断路器模式 |
提示:限流是保障系统稳定性的重要手段,应在系统设计初期就考虑限流策略。
2. 限流的粒度
常见限流粒度:
- 全局限流:限制整个应用的总流量
- 接口级限流:限制特定接口的流量
- 用户级限流:限制单个用户的请求频率
- IP级限流:限制单个IP的请求频率
- 业务维度限流:根据业务参数进行限流(如按商品ID、用户ID)
选择原则:
- 优先保护核心接口
- 根据业务特点选择合适的粒度
- 避免过度细化导致性能开销
二、限流常见算法
1. 令牌桶算法(最常用)
原理:
- 以固定速率向桶中添加令牌
- 每次请求需获取令牌,获取成功则处理请求
- 桶满时不再添加令牌
- 允许突发流量(最多桶容量的请求)
特点:
- 平滑处理请求
- 允许一定程度的突发流量
- 实现简单,性能好
Guava 实现:
// 创建一个每秒允许5个请求的令牌桶
RateLimiter limiter = RateLimiter.create(5.0);
// 尝试获取令牌,不等待
if (limiter.tryAcquire()) {
// 处理请求
} else {
// 限流处理
}2. 漏桶算法
原理:
- 请求以任意速率流入水桶
- 水桶以固定速率流出请求
- 桶满后新请求被拒绝
特点:
- 请求处理速率恒定
- 无法应对突发流量
- 平滑请求处理
3. 计数器算法
原理:
- 在时间窗口内统计请求数量
- 超过阈值则拒绝请求
- 时间窗口结束后重置计数器
特点:
- 实现简单
- 存在临界问题(窗口切换时可能两倍流量)
- 适合简单场景
示例:
// 每分钟最多100次请求
if (requestCount.incrementAndGet() > 100) {
// 限流处理
}
// 每分钟重置计数器
scheduler.scheduleAtFixedRate(() -> requestCount.set(0), 0, 1, TimeUnit.MINUTES);提示:90% 的实际场景中,令牌桶算法已经足够使用,是目前最常用的限流算法。
三、单实例限流实现
1. Guava RateLimiter
优势:
- 轻量级,无外部依赖
- 实现简单,性能好
- 支持平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)
基本使用:
// 创建每秒5个请求的限流器
RateLimiter limiter = RateLimiter.create(5.0);
// 阻塞等待获取令牌
limiter.acquire(); // 会阻塞直到获取到令牌
// 处理请求
// 非阻塞方式
if (limiter.tryAcquire()) {
// 处理请求
} else {
// 限流处理
}2. AOP 实现接口限流
定义限流注解:
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 每秒允许的请求数
*/
double permitsPerSecond() default 10.0;
/**
* 获取令牌超时时间(毫秒)
*/
long timeout() default 0;
/**
* 限流提示信息
*/
String message() default "请求过于频繁,请稍后再试";
}AOP 拦截器实现:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.RateLimiter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Aspect
@Component
public class RateLimitAspect {
// 缓存限流器,key为方法全名
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = getMethodKey(joinPoint);
// 获取或创建限流器
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.permitsPerSecond()));
// 尝试获取令牌
if (rateLimit.timeout() > 0) {
if (!limiter.tryAcquire(rateLimit.timeout(), TimeUnit.MILLISECONDS)) {
throw new RateLimitException(rateLimit.message());
}
} else {
if (!limiter.tryAcquire()) {
throw new RateLimitException(rateLimit.message());
}
}
return joinPoint.proceed();
}
private String getMethodKey(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().getDeclaringTypeName() + "."
+ joinPoint.getSignature().getName();
}
}全局异常处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
@ExceptionHandler(RateLimitException.class)
public Result<?> handleRateLimitException(RateLimitException ex) {
return Result.fail(HttpStatus.TOO_MANY_REQUESTS.value(), ex.getMessage());
}
}自定义异常:
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}
}使用示例:
@RestController
public class UserController {
@RateLimit(permitsPerSecond = 2.0, timeout = 100)
@GetMapping("/api/user/{id}")
public User getUser(@PathVariable Long id) {
// 业务逻辑
return userService.getUser(id);
}
}提示:单实例限流适用于单机部署场景,无法应对分布式环境下的全局限流需求。
四、分布式限流实现
1. Redis + Lua 实现
原理:
- 使用 Redis 存储限流计数
- 使用 Lua 脚本保证原子操作
- 基于令牌桶或滑动窗口算法
Lua 脚本示例(令牌桶):
-- 限流脚本
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local interval = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local count = redis.call('get', key)
if not count then
count = 0
end
-- 检查是否超过限制
if tonumber(count) >= limit then
return 0
end
-- 增加计数
redis.call('incrby', key, 1)
-- 设置过期时间
redis.call('expire', key, interval)
return 1Java 调用:
public boolean tryAcquire(String key, int limit, int interval) {
long now = System.currentTimeMillis();
List<String> keys = Collections.singletonList(key);
List<String> args = Arrays.asList(String.valueOf(limit), String.valueOf(interval), String.valueOf(now));
Long result = (Long) redisTemplate.execute(
rateLimitScript,
keys,
args.toArray()
);
return result != null && result == 1;
}2. Redisson 分布式限流器
优势:
- 基于 Redis 的分布式限流
- 提供了 RRateLimiter 接口
- 支持平滑限流
使用示例:
@Autowired
private RedissonClient redissonClient;
public boolean tryAcquire(String key, int permits, int timeout) {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 初始化:每秒10个令牌,最多20个令牌
rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);
return rateLimiter.tryAcquire(permits, timeout, TimeUnit.MILLISECONDS);
}3. 分布式限流最佳实践
限流粒度选择:
- 全局限流:
rate:global:${interfaceName} - 用户级限流:
rate:user:${userId}:${interfaceName} - IP级限流:
rate:ip:${ipAddress}:${interfaceName} - 业务维度限流:
rate:business:${businessKey}:${interfaceName}
Redis 键设计:
private String buildKey(JoinPoint joinPoint, RateLimit rateLimit, String keyType, String keyValue) {
String interfaceName = joinPoint.getSignature().toShortString();
return String.format("rate:%s:%s:%s", keyType, keyValue, interfaceName);
}动态配置:
- 通过配置中心动态调整限流阈值
- 根据业务时段调整限流策略
- 监控系统负载自动调整限流参数
五、实用技巧与最佳实践
1. 限流策略设计
核心原则:
- 核心接口优先保护:对关键业务接口设置更严格的限流
- 分级限流:不同用户等级设置不同限流阈值
- 动态调整:根据系统负载动态调整限流阈值
- 渐进式限流:逐步收紧限流阈值,避免突然拒绝大量请求
典型策略:
| 接口类型 | 限流策略 | 示例 |
|---|---|---|
| 核心接口 | 严格限流 | 100 QPS |
| 次要接口 | 适度限流 | 500 QPS |
| 后台接口 | 宽松限流 | 1000 QPS |
| 公共接口 | 按IP限流 | 10 QPS/IP |
2. 限流响应设计
友好提示:
- 返回明确的错误码和提示信息
- 提供重试建议(如 Retry-After 头)
- 对不同客户端返回不同格式的响应
HTTP 响应头:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1625683200自定义响应体:
{
"code": 429,
"message": "请求过于频繁,请1秒后重试",
"retryAfter": 1
}3. 限流监控与告警
关键指标:
- 限流触发次数
- 限流率(触发次数/总请求数)
- 各接口限流情况
- 限流异常分布
监控实现:
// 在限流拦截器中添加监控
if (!limiter.tryAcquire()) {
metricsCounter.increment("rate_limit_triggered_total",
"interface", interfaceName,
"userId", userId);
throw new RateLimitException("请求过于频繁");
}告警规则:
- 限流率超过 10% 触发警告
- 核心接口限流率超过 5% 触发严重告警
- 限流持续时间超过 5 分钟触发告警
六、常见问题与解决方案
1. 单实例限流问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 重启后限流失效 | 限流状态未持久化 | 使用分布式限流 |
| 多实例部署无效 | 限流状态隔离 | 使用 Redis 等共享状态 |
| 预热问题 | 突发流量被限制 | 使用 SmoothWarmingUp |
| 冷启动问题 | 初始令牌不足 | 设置初始令牌数 |
2. 分布式限流问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Redis 单点故障 | 依赖单点 Redis | 使用 Redis 集群 |
| 网络延迟影响 | Redis 网络开销 | 本地缓存+Redis 校准 |
| 精度问题 | 时间同步问题 | 使用相对时间 |
| 脚本超时 | Lua 脚本执行时间长 | 优化脚本逻辑 |
3. 业务场景适配
登录接口限流:
@RateLimit(
permitsPerSecond = 5.0,
keyType = RateLimitKeyType.IP,
message = "登录过于频繁,请稍后再试"
)
@PostMapping("/login")
public Result<?> login(@RequestBody LoginRequest request) {
// ...
}支付接口限流:
@RateLimit(
permitsPerSecond = 10.0,
keyType = RateLimitKeyType.USER,
keyExpression = "#request.userId",
timeout = 500
)
@PostMapping("/pay")
public Result<?> pay(@RequestBody PayRequest request) {
// ...
}秒杀接口限流:
@RateLimit(
permitsPerSecond = 100.0,
keyType = RateLimitKeyType.GLOBAL,
interval = 1,
intervalUnit = RateLimitIntervalUnit.SECONDS
)
@PostMapping("/seckill")
public Result<?> seckill(@RequestBody SeckillRequest request) {
// ...
}七、实用技巧速查表
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 单机应用 | Guava RateLimiter | 注意方法级限流的 key 设计 |
| 分布式应用 | Redis + Lua | 确保 Lua 脚本原子性 |
| 核心接口 | 严格限流(10-100 QPS) | 优先保障核心业务 |
| 次要接口 | 适度限流(100-500 QPS) | 根据业务重要性调整 |
| 用户级限流 | 按用户ID限流 | 区分VIP用户和普通用户 |
| IP级限流 | 按IP地址限流 | 防止恶意攻击 |
| 动态调整 | 配置中心+监控 | 实现自动弹性调整 |
| 限流监控 | Prometheus+Grafana | 可视化限流指标 |
