1. 为什么API限流是微服务架构的必修课
上周五凌晨2点,我被一阵急促的报警短信惊醒。监控系统显示我们的优惠券发放接口QPS突然从200飙升到8000,数据库连接池瞬间被打满。当我手忙脚乱地登录服务器时,整个集群已经像多米诺骨牌一样接连崩溃。事后排查发现,原来是某个合作方在循环调用接口时没有做请求间隔控制——这个价值百万的教训让我深刻理解了API限流的重要性。
在微服务架构中,限流就像交通信号灯,它能:
- 防止突发流量击穿系统(比如秒杀场景)
- 避免某个API被恶意刷量(如短信接口)
- 公平分配服务器资源(保障核心业务)
- 为服务降级提供缓冲时间(熔断前先限流)
SpringBoot生态中常见的限流方案对比:
| 方案类型 | 代表实现 | 适用场景 | 性能损耗 |
|---|---|---|---|
| 计数器算法 | AtomicInteger | 简单低频控制 | 最低 |
| 滑动窗口 | Redis+Lua | 分布式环境 | 中等 |
| 令牌桶 | Guava RateLimiter | 突发流量控制 | 较低 |
| 漏桶算法 | Resilience4j | 平滑流量输出 | 中等 |
经验之谈:生产环境推荐组合使用令牌桶和滑动窗口,前者应对突发流量,后者防止分布式环境下的总量超限
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基于Guava的本地限流实战
2.1 基础令牌桶实现
先引入Guava依赖(注意版本兼容性):
xml复制<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version> <!-- 与SpringBoot 3.x兼容 -->
</dependency>
创建一个针对用户注册接口的限流过滤器:
java复制@Configuration
public class RateLimitConfig {
// 每秒钟允许2个请求(预热期3秒)
private final RateLimiter limiter = RateLimiter.create(2.0, 3, TimeUnit.SECONDS);
@Bean
public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
FilterRegistrationBean<RateLimitFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RateLimitFilter(limiter));
registration.addUrlPatterns("/api/v1/users");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
2.2 动态参数调优技巧
实际项目中往往需要动态调整限流参数,我常用这种SPEL表达式方式:
java复制@RestController
@RequestMapping("/api")
public class UserController {
// 从配置中心读取限流值
@Value("${rate.limit.user.query:5}")
private double queryRate;
private RateLimiter dynamicLimiter;
@PostConstruct
public void init() {
dynamicLimiter = RateLimiter.create(queryRate);
}
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
if (!dynamicLimiter.tryAcquire()) {
throw new TooManyRequestsException("请稍后再试");
}
return userService.findById(id);
}
}
踩坑记录:Guava的RateLimiter在集群环境下会失效,曾经因为没意识到这点导致某次促销活动时部分节点被压垮。解决方案见下一章。
3. Redis+Lua实现分布式限流
3.1 滑动窗口算法实现
在resources目录下创建limit.lua脚本:
lua复制local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)
local now = tonumber(ARGV[3])
if current == false then
redis.call('SET', key, 1, 'EX', window)
return 1
else
local hits = tonumber(current)
if hits >= limit then
return 0
else
redis.call('INCR', key)
return 1
end
end
对应的Java调用代码:
java复制public boolean tryAcquire(String key, int limit, int windowSec) {
List<String> keys = Collections.singletonList(key);
List<String> args = Arrays.asList(
String.valueOf(limit),
String.valueOf(windowSec),
String.valueOf(System.currentTimeMillis() / 1000)
);
Long result = redisTemplate.execute(
new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) {
return connection.eval(
loadScript("classpath:limit.lua").getBytes(),
ReturnType.INTEGER,
1,
keys.get(0).getBytes(),
args.get(0).getBytes(),
args.get(1).getBytes(),
args.get(2).getBytes()
);
}
}
);
return result != null && result == 1;
}
3.2 性能优化实践
在千万级QPS的金融项目中,我们通过以下优化将Redis限流性能提升3倍:
- 使用Redis Pipeline批量执行命令
- 采用本地缓存+Redis的二级限流策略
- 对不同的API分级配置限流阈值(核心接口宽松,非核心严格)
java复制// 二级缓存示例
public boolean isAllowed(String apiKey) {
// 先检查本地缓存
LocalCacheValue cacheValue = localCache.get(apiKey);
if (cacheValue != null && cacheValue.isAllowed()) {
return true;
}
// 再走Redis限流
boolean redisAllowed = redisLimiter.tryAcquire(apiKey);
if (redisAllowed) {
localCache.put(apiKey, new LocalCacheValue(true, 500)); // 500ms本地缓存
}
return redisAllowed;
}
4. SpringCloud Gateway集成方案
4.1 全局过滤器配置
对于SpringCloud体系,更推荐在网关层做统一限流:
yaml复制# application.yml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 每秒令牌数
redis-rate-limiter.burstCapacity: 200 # 突发容量
key-resolver: "#{@userKeyResolver}" # 限流维度
自定义限流维度解析器:
java复制@Bean
KeyResolver userKeyResolver() {
return exchange -> {
// 按用户ID限流
String userId = exchange.getRequest()
.getHeaders()
.getFirst("X-User-ID");
return Mono.just(Optional.ofNullable(userId).orElse("anonymous"));
};
}
4.2 熔断降级策略
当触发限流时,建议返回429状态码和友好提示:
java复制@Configuration
public class GatewayConfig {
@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(500))
.build())
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.slidingWindowSize(20)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.build())
.build());
}
@Bean
public RouterFunction<ServerResponse> routerFunction() {
return RouterFunctions.route()
.GET("/fallback", request -> ServerResponse.status(429)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(
Map.of("code", 429, "message", "请求过于频繁,请稍后再试")
)))
.build();
}
}
5. 生产环境监控与调优
5.1 Prometheus监控指标
暴露限流相关指标到监控系统:
java复制@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "user-service",
"region", System.getenv().getOrDefault("REGION", "unknown")
);
}
@Bean
public Counter rateLimitCounter(MeterRegistry registry) {
return Counter.builder("api.rate_limit.count")
.description("被限流的请求计数")
.tags("type", "global")
.register(registry);
}
}
对应的Grafana监控面板配置建议:
- 被限流请求的QPS趋势图
- 各API的限流阈值与实际流量对比
- 按业务分组的限流事件统计
5.2 动态规则热更新
通过Nacos配置中心实现规则热更新:
java复制@RefreshScope
@Configuration
public class DynamicRateLimitConfig {
@Value("${rate.limit.config:#{null}}")
private String limitConfig;
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 5000)
public void refreshLimiters() {
if (limitConfig != null) {
JsonNode config = Json.parse(limitConfig);
config.fields().forEachRemaining(entry -> {
String api = entry.getKey();
double rate = entry.getValue().asDouble();
limiters.computeIfAbsent(api, k -> RateLimiter.create(rate))
.setRate(rate);
});
}
}
public boolean tryAcquire(String apiPath) {
RateLimiter limiter = limiters.get(apiPath);
return limiter == null || limiter.tryAcquire();
}
}
在最近的一次大促中,我们通过动态调整限流阈值,在保证系统稳定的情况下比固定限流策略多承载了40%的流量。关键技巧是:
- 根据CPU负载动态下调非核心接口限流值
- 在业务低峰期自动放宽限制
- 对爬虫流量实施阶梯式限流策略
