在分布式系统架构中,流量控制是保障系统稳定性的关键防线。作为一名经历过多次大促流量冲击的架构师,我深刻体会到合理的限流策略对于系统的重要性。本文将基于我在多个千万级用户项目中的实践经验,深入剖析主流限流算法的实现原理,并分享在API网关中的落地实践。
想象一下高速公路的收费站:如果没有限流措施,节假日高峰时段所有车辆同时涌入,必然导致系统瘫痪。同理,在软件系统中:
这些场景都会导致:
通过限流,我们可以:
计数器算法就像体育场的入场检票:
java复制public class CounterLimiter {
private final int limit = 100; // 窗口阈值
private final long windowSize = 60_000; // 1分钟窗口
private int counter = 0;
private long windowStart = System.currentTimeMillis();
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - windowStart > windowSize) {
counter = 0;
windowStart = now;
}
if (counter < limit) {
counter++;
return true;
}
return false;
}
}
该算法存在明显的时间窗口临界问题:
优化方案:使用滑动窗口算法
将时间窗口细分为多个格子(如6秒一个格子),统计最近N个格子的请求总数:
code复制窗口:60秒 | 格子:6秒/个
[20][15][25][30][18][22][28][32][35][40]
↑ 最近75次请求(格子9+10)
java复制public class SlidingWindowLimiter {
private final int limit = 100;
private final int slotCount = 10;
private final AtomicInteger[] slots = new AtomicInteger[slotCount];
private volatile long lastRotateTime = System.currentTimeMillis();
public SlidingWindowLimiter() {
for (int i = 0; i < slotCount; i++) {
slots[i] = new AtomicInteger(0);
}
}
public synchronized boolean tryAcquire() {
rotateWindow();
int total = Arrays.stream(slots).mapToInt(AtomicInteger::get).sum();
if (total < limit) {
slots[slotCount-1].incrementAndGet();
return true;
}
return false;
}
private void rotateWindow() {
long now = System.currentTimeMillis();
long elapsed = now - lastRotateTime;
int rotateSlots = (int)(elapsed / (60_000/slotCount));
if (rotateSlots > 0) {
for (int i = 0; i < rotateSlots && i < slotCount; i++) {
slots[(slotCount - rotateSlots + i) % slotCount].set(0);
}
lastRotateTime = now;
}
}
}
漏桶就像一个有固定出水速率的水桶:
java复制public class LeakyBucketLimiter {
private final int capacity = 100;
private final long leakRate = 10; // 10次/秒
private long water = 0;
private long lastLeakTime = System.currentTimeMillis();
public synchronized boolean tryAcquire() {
leakWater();
if (water < capacity) {
water++;
return true;
}
return false;
}
private void leakWater() {
long now = System.currentTimeMillis();
long elapsed = now - lastLeakTime;
long leaked = elapsed * leakRate / 1000;
if (leaked > 0) {
water = Math.max(0, water - leaked);
lastLeakTime = now;
}
}
}
令牌桶就像一个定期发放通行证的售票处:
java复制public class TokenBucketLimiter {
private final int capacity = 100;
private final long refillRate = 10; // 10个/秒
private long tokens = capacity;
private long lastRefillTime = System.currentTimeMillis();
public synchronized boolean tryAcquire() {
refillTokens();
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
private void refillTokens() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
long newTokens = elapsed * refillRate / 1000;
if (newTokens > 0) {
tokens = Math.min(capacity, tokens + newTokens);
lastRefillTime = now;
}
}
}
实际生产环境中,建议使用Redis+Lua实现分布式令牌桶:
lua复制-- KEYS[1]: 限流key
-- ARGV[1]: 填充速率
-- ARGV[2]: 容量
-- ARGV[3]: 当前时间(ms)
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local lastTime = redis.call('hget', key, 'time') or now
local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)
local elapsed = math.max(0, now - lastTime)
tokens = math.min(capacity, tokens + elapsed * rate / 1000)
if tokens >= 1 then
redis.call('hset', key, 'time', now)
redis.call('hset', key, 'tokens', tokens - 1)
redis.call('expire', key, 3600)
return 1
else
return 0
end
| 算法 | 空间复杂度 | 时间复杂度 | 平滑度 | 突发处理 | 分布式实现难度 |
|---|---|---|---|---|---|
| 计数器 | O(1) | O(1) | 差 | 不支持 | 中等 |
| 滑动窗口 | O(N) | O(N) | 较好 | 部分支持 | 较高 |
| 漏桶 | O(1) | O(1) | 极好 | 不支持 | 中等 |
| 令牌桶 | O(1) | O(1) | 好 | 支持 | 中等 |
mermaid复制graph TD
A[需要精确统计?] -->|是| B[滑动窗口]
A -->|否| C{需要处理突发?}
C -->|是| D[令牌桶]
C -->|否| E[漏桶]
F[分布式环境?] -->|是| G[Redis实现]
F -->|否| H[本地内存]
yaml复制spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 每秒补充的令牌数
redis-rate-limiter.burstCapacity: 200 # 令牌桶容量
redis-rate-limiter.requestedTokens: 1 # 每次请求消耗令牌数
key-resolver: "#{@ipKeyResolver}"
java复制@Bean
public KeyResolver apiKeyResolver() {
return exchange -> {
String path = exchange.getRequest().getPath().toString();
String method = exchange.getRequest().getMethodValue();
return Mono.just(method + ":" + path);
};
}
java复制@PostConstruct
public void initRules() {
// API分组
Set<ApiDefinition> definitions = new HashSet<>();
definitions.add(new ApiDefinition("user_api")
.setPredicateItems(Set.of(
new ApiPathPredicateItem().setPattern("/api/user/**")
)));
// 流控规则
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("user_api")
.setCount(50)
.setIntervalSec(1)
.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)
.setMaxQueueingTimeMs(500));
GatewayRuleManager.loadRules(rules);
}
java复制rules.add(new GatewayFlowRule("hot_product")
.setParamItem(new GatewayParamFlowItem()
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
.setFieldName("productId")
)
.setCount(10));
java复制public boolean tryAcquire(String key, long rate, long interval) {
RRateLimiter limiter = redissonClient.getRateLimiter(key);
limiter.trySetRate(RateType.OVERALL, rate, interval, RateIntervalUnit.SECONDS);
return limiter.tryAcquire();
}
对于大规模集群,建议采用一致性哈希将相同key路由到固定节点,避免Redis集群的跨节点操作。
code复制 [全局限流 10000 QPS]
|
+-------+-------+
| |
[IP限流 100 QPS] [用户限流 50 QPS]
| |
+----+----+ +----+----+
| | | |
[API限流] [业务限流] [VIP不限流] [新用户限流]
java复制@Scheduled(fixedRate = 5000)
public void adjustRate() {
double cpuLoad = getCpuLoad();
double memUsage = getMemoryUsage();
// 根据负载动态调整
if (cpuLoad > 0.8 || memUsage > 0.75) {
double factor = 0.7; // 降为70%
updateAllRules(factor);
}
}
| 指标名称 | 类型 | 说明 |
|---|---|---|
| rate_limit_total | Counter | 总请求数 |
| rate_limit_passed | Counter | 通过请求数 |
| rate_limit_blocked | Counter | 被限流请求数 |
| rate_limit_wait_time | Timer | 排队等待时间(漏桶算法) |
| rate_limit_tokens | Gauge | 当前令牌数 |
限流不生效:
限流过于激进:
Redis性能瓶颈:
经过多个项目的实战验证,我总结出以下经验:
分层防护:
动态规则:
优雅降级:
压测验证:
限流不是万能的,但没有限流是万万不能的。合理的限流策略需要结合业务特点不断调整优化,这也是系统稳定性建设中持续迭代的过程。