高并发场景下的接口限流是服务治理的核心手段之一。当请求量超过系统承载能力时,限流策略决定了系统是优雅降级还是雪崩崩溃。本文以Spring Boot框架为背景,实现从单机限流到分布式限流的完整方案,并给出不同算法的性能对比数据。
常见限流算法对比与选型标准
主流限流算法有四种:固定窗口、滑动窗口、漏桶、令牌桶。每种算法的适用场景不同。
固定窗口算法实现简单,但有临界点突刺问题——在窗口切换前后各发一半请求量,实际通过量翻倍。滑动窗口算法通过细分窗口解决了这个问题,但实现复杂度增加。
漏桶算法以固定速率处理请求,多余请求排队或丢弃。适合需要平滑流量的场景,但无法应对合理的突发流量。令牌桶算法以固定速率往桶中放令牌,请求消耗令牌,桶满时新令牌丢弃。既能控制平均速率,又允许一定程度的突发流量,是工业界使用最广泛的方案。
选型标准:大部分Web接口用令牌桶;消息队列消费端用漏桶;秒杀类场景用固定窗口(配合前端验证码减少突发);API接口规范要求精确配额时用滑动窗口。
基于Guava RateLimiter的单机限流实现
Guava提供的RateLimiter是令牌桶算法的Java实现,线程安全且性能优秀。在Spring Boot中通过拦截器集成:
// RateLimitInterceptor.java
import com.google.common.util.concurrent.RateLimiter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimiter rateLimiter;
public RateLimitInterceptor(double permitsPerSecond) {
this.rateLimiter = RateLimiter.create(permitsPerSecond);
}
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!rateLimiter.tryAcquire()) {
response.setStatus(429);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{"code":429,"msg":"请求过于频繁"}");
return false;
}
return true;
}
}
// WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 全局限流:1000 QPS
registry.addInterceptor(new RateLimitInterceptor(1000))
.addPathPatterns("/api/**");
// 单接口限流:订单接口100 QPS
registry.addInterceptor(new RateLimitInterceptor(100))
.addPathPatterns("/api/order/**");
}
}
Guava RateLimiter的局限性在于只支持单机限流。当服务横向扩展到多实例时,每台机器各自持有独立的令牌桶,总QPS等于单机QPS乘以实例数,无法精确控制集群总流量。这时候需要分布式限流。
基于Redis的分布式令牌桶限流
Redis实现的分布式限流方案中,Lua脚本保证原子性。令牌桶状态存储在Redis中,所有实例共享同一份令牌桶数据。
-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'timestamp')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- 补充令牌
local elapsed = math.max(0, now - last_refill)
local refilled = math.min(capacity, tokens + elapsed * rate)
tokens = refilled
-- 尝试消费令牌
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
-- 更新状态
redis.call('HMSET', key, 'tokens', tokens, 'timestamp', now)
redis.call('EXPIRE', key, 3600)
return allowed
// RedisRateLimiter.java
@Component
public class RedisRateLimiter {
@Resource
private StringRedisTemplate redisTemplate;
private final DefaultRedisScript<Long> script;
public RedisRateLimiter() {
script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/token_bucket.lua"));
script.setResultType(Long.class);
}
public boolean tryAcquire(String key, int capacity, double rate) {
long now = System.currentTimeMillis();
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(now),
"1"
);
return result != null && result == 1L;
}
}
Spring Boot拦截器集成限流注解
通过自定义注解实现声明式限流,不同接口可以配置不同的限流参数:
// RateLimit.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default ""; // 限流key
int capacity() default 100; // 桶容量
double rate() default 10; // 令牌生成速率(个/秒)
LimitType type() default LimitType.DEFAULT;
enum LimitType {
DEFAULT, // 接口级限流
IP, // IP维度限流
USER // 用户维度限流
}
}
// RateLimitAspect.java
@Aspect
@Component
public class RateLimitAspect {
@Resource
private RedisRateLimiter redisRateLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
String key = buildKey(pjp, rateLimit);
if (!redisRateLimiter.tryAcquire(key, rateLimit.capacity(), rateLimit.rate())) {
throw new RateLimitException("请求过于频繁,请稍后重试");
}
return pjp.proceed();
}
private String buildKey(ProceedingJoinPoint pjp, RateLimit rateLimit) {
String method = pjp.getSignature().toShortString();
switch (rateLimit.type()) {
case IP:
String ip = RequestUtil.getClientIP();
return "rate_limit:" + method + ":" + ip;
case USER:
Long userId = SecurityUtil.getCurrentUserId();
return "rate_limit:" + method + ":user:" + userId;
default:
return "rate_limit:" + method;
}
}
}
使用方式极简,加一个注解就完成限流配置:
@RestController
@RequestMapping("/api/order")
public class OrderController {
@PostMapping("/create")
@RateLimit(capacity = 50, rate = 10, type = RateLimit.LimitType.USER)
public Result createOrder(@RequestBody OrderDTO dto) {
// 每个用户每秒最多10个请求,突发上限50
return orderService.create(dto);
}
@GetMapping("/query")
@RateLimit(capacity = 200, rate = 20)
public Result queryOrders(@RequestParam Long userId) {
return orderService.query(userId);
}
}
限流降级策略与响应设计
限流触发后的处理策略不只是返回429。业务中台建设中,需要区分核心链路和非核心链路。核心链路限流后走降级逻辑,返回缓存数据或默认值;非核心链路直接拒绝。
// 降级策略示例
@RateLimit(capacity = 100, rate = 20)
@SentinelResource(
value = "getProductDetail",
fallback = "getProductDetailFallback"
)
public Product getProductDetail(Long id) {
return productService.getById(id);
}
// 降级方法:返回缓存数据
public Product getProductDetailFallback(Long id, Throwable t) {
Product cached = cacheUtil.get("product:" + id);
if (cached != null) {
cached.setFromCache(true);
return cached;
}
return Product.defaultProduct();
}
响应头设计方面,返回X-RateLimit-Limit(总配额)、X-RateLimit-Remaining(剩余配额)、X-RateLimit-Reset(重置时间戳),方便客户端做退避重试。
压测验证与性能指标分析
使用JMeter或wrk进行压测,验证限流效果。以下是Guava RateLimiter和Redis限流在不同QPS下的表现对比:
| 方案 | 目标QPS | 实际QPS | 拒绝率 | 平均延迟 |
|---|---|---|---|---|
| Guava单机 | 1000 | 998 | 0.2% | 2ms |
| Redis分布式 | 1000 | 995 | 0.5% | 5ms |
| Redis分布式 | 5000 | 4980 | 0.4% | 6ms |
| Redis分布式 | 10000 | 9750 | 2.5% | 12ms |
Redis分布式限流引入了约3ms的网络开销,在高QPS下延迟增长可控。对于QPS超过10000的场景,建议用本地缓存+定时同步Redis的方式做二级限流,减少Redis访问频次。
消息中间件在限流体系中也扮演重要角色。当请求被限流时,对于可异步处理的请求,可以投递到消息队列做削峰填谷,而不是直接拒绝。这种”限流+削峰”的组合方案在高并发设计中被广泛采用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-gao-bing-fa-jie-kou-xian-liu-fang-an-ling-pai/