限流算法又双叒叕被击穿了?这6种算法让你的系统固若金汤!
限流算法又双叒叕被击穿了?这6种算法让你的系统固若金汤!
大家好,我是服务端技术精选的小编。今天来聊聊一个让无数后端程序员夜不能寐的话题——限流算法。
你是不是也遇到过这种情况:明明服务器配置不错,但一到高峰期就各种超时、宕机?流量一大系统就歇菜,像纸糊的一样脆弱?
别慌!老司机今天就给你盘点6种限流算法,从最简单的计数器到最优雅的令牌桶,让你的系统从此固若金汤!
一、不限流的系统,就是定时炸弹
先说说为啥需要限流。想象一下这个场景:你开了个小面馆,平时每天能接待100个客人。突然有一天,来了1000个客人,你会怎么办?
如果硬接,厨师累死,客人等到天荒地老,最后谁都不满意;如果合理控制,虽然损失一些生意,但至少能保证现有客人的体验。
限流就是在保护系统的同时,保证服务质量!
不限流的后果
我曾经见过一个真实案例:某电商平台搞秒杀活动,平时QPS只有几百,结果活动开始瞬间涌入10万QPS。因为没有做限流保护,整个系统直接崩溃,不仅秒杀商品没卖出去,连正常的商品详情页都打不开了。
常见的系统被"击穿"场景:
- 热点事件:突发新闻导致流量暴涨
- 恶意攻击:DDoS攻击、爬虫恶意抓取
- 促销活动:双11、618等大促期间流量激增
- 系统故障:某个节点挂了,流量全部涌向其他节点
二、6种限流算法大揭秘
算法1:固定窗口计数器 - 最简单的"门卫"
原理:在固定时间窗口内,统计请求数量,超过阈值就拒绝。
public class FixedWindowCounter {
private final int limit;
private final long windowSize;
private volatile long windowStart;
private volatile int counter;
public FixedWindowCounter(int limit, long windowSize) {
this.limit = limit;
this.windowSize = windowSize;
this.windowStart = System.currentTimeMillis();
this.counter = 0;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - windowStart >= windowSize) {
windowStart = now;
counter = 0;
}
if (counter < limit) {
counter++;
return true;
}
return false;
}
}
优点:实现简单,内存占用小,性能高
缺点:存在"突刺现象",不够平滑
适用场景:流量平稳的系统,对精度要求不高
算法2:滑动窗口计数器 - 更精准的"计时员"
原理:将时间窗口分成多个小格子,滑动统计,避免固定窗口的突刺问题。
public class SlidingWindowCounter {
private final int limit;
private final long windowSize;
private final int bucketCount;
private final AtomicInteger[] buckets;
public SlidingWindowCounter(int limit, long windowSize, int bucketCount) {
this.limit = limit;
this.windowSize = windowSize;
this.bucketCount = bucketCount;
this.buckets = new AtomicInteger[bucketCount];
for (int i = 0; i < bucketCount; i++) {
buckets[i] = new AtomicInteger(0);
}
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
cleanExpiredBuckets(now);
int totalCount = Arrays.stream(buckets)
.mapToInt(AtomicInteger::get)
.sum();
if (totalCount < limit) {
int bucketIndex = (int) ((now / (windowSize / bucketCount)) % bucketCount);
buckets[bucketIndex].incrementAndGet();
return true;
}
return false;
}
}
优点:更平滑,避免突刺现象
缺点:内存占用较大,实现复杂
适用场景:对平滑性要求较高的场景
算法3:令牌桶算法 - 最优雅的"发令员"
原理:系统以恒定速率产生令牌放入桶中,请求需要获取令牌才能通过。
public class TokenBucket {
private final long capacity;
private final double refillRate;
private double tokens;
private long lastRefillTime;
public TokenBucket(long capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
refillTokens();
if (tokens >= 1) {
tokens -= 1;
return true;
}
return false;
}
private void refillTokens() {
long now = System.currentTimeMillis();
if (now > lastRefillTime) {
double tokensToAdd = (now - lastRefillTime) * refillRate / 1000.0;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
}
}
Guava RateLimiter使用:
// 使用Google Guava的令牌桶实现
RateLimiter rateLimiter = RateLimiter.create(2.0); // 每秒2个令牌
public void handleRequest() {
if (rateLimiter.tryAcquire()) {
// 处理请求
processRequest();
} else {
// 拒绝请求
rejectRequest();
}
}
优点:允许突发流量,流量整形效果好
缺点:实现相对复杂
适用场景:需要应对突发流量的系统
算法4:漏桶算法 - 最稳定的"水龙头"
原理:请求进入漏桶,以恒定速率流出,超过桶容量的请求被丢弃。
public class LeakyBucket {
private final long capacity;
private final double leakRate;
private double currentVolume;
private long lastLeakTime;
public LeakyBucket(long capacity, double leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
this.currentVolume = 0;
this.lastLeakTime = System.currentTimeMillis();
}
public synchronized boolean tryAdd(double volume) {
leak();
if (currentVolume + volume <= capacity) {
currentVolume += volume;
return true;
}
return false;
}
private void leak() {
long now = System.currentTimeMillis();
if (now > lastLeakTime) {
double leaked = (now - lastLeakTime) * leakRate / 1000.0;
currentVolume = Math.max(0, currentVolume - leaked);
lastLeakTime = now;
}
}
}
优点:出口流量恒定,削峰效果好
缺点:无法应对合理的突发流量
适用场景:需要严格控制出口流量
算法5:滑动窗口限流 - 最精确的"监控员"
原理:维护一个滑动的时间窗口,精确统计窗口内的请求数。
public class SlidingWindowRateLimiter {
private final int limit;
private final long windowSize;
private final Queue<Long> requestTimes;
public SlidingWindowRateLimiter(int limit, long windowSize) {
this.limit = limit;
this.windowSize = windowSize;
this.requestTimes = new LinkedList<>();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 移除窗口外的请求
while (!requestTimes.isEmpty() &&
now - requestTimes.peek() > windowSize) {
requestTimes.poll();
}
if (requestTimes.size() < limit) {
requestTimes.offer(now);
return true;
}
return false;
}
}
优点:精度最高,避免突刺现象
缺点:内存占用最大,性能开销较高
适用场景:对精度要求极高的场景
算法6:分布式限流 - 多节点的"协调员"
原理:在分布式环境下,多个节点协同限流。
@Component
public class DistributedRateLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean fixedWindowLimit(String key, int limit, long windowSize) {
long window = System.currentTimeMillis() / windowSize;
String redisKey = key + ":" + window;
String luaScript =
"local current = redis.call('incr', KEYS[1]) " +
"if current == 1 then " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
"end " +
"if current <= tonumber(ARGV[1]) then " +
" return 1 " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(redisKey),
String.valueOf(limit),
String.valueOf(windowSize / 1000));
return result != null && result == 1;
}
}
三、实战案例:电商平台限流方案
业务背景
某电商平台关键接口:
- 商品详情:QPS 1万,可突发到3万
- 用户登录:QPS 5千,需防刷
- 下单接口:QPS 2千,绝对不能超
分层限流策略
// 网关层 - 全局限流
@Component
public class GatewayRateLimiter {
private final TokenBucket globalLimiter = new TokenBucket(50000, 40000);
public boolean checkGlobalLimit() {
return globalLimiter.tryAcquire();
}
}
// 应用层 - 接口限流
@RestController
public class ProductController {
private final TokenBucket productLimiter = new TokenBucket(30000, 10000);
@GetMapping("/product/{id}")
public ResponseEntity<?> getProduct(@PathVariable Long id) {
if (!productLimiter.tryAcquire()) {
return ResponseEntity.status(429).body("商品查询繁忙,请稍后重试");
}
return ResponseEntity.ok(productService.getById(id));
}
}
// 用户级限流
@Component
public class UserRateLimiter {
@Autowired
private DistributedRateLimiter distributedLimiter;
public boolean checkUserLimit(Long userId, String operation) {
String key = "user:" + userId + ":" + operation;
switch (operation) {
case "login":
return distributedLimiter.fixedWindowLimit(key, 10, 3600000);
case "order":
return distributedLimiter.fixedWindowLimit(key, 3, 60000);
default:
return true;
}
}
}
四、限流算法选择指南
选择决策树
是否允许突发流量?
↓ ↓
是 否
↓ ↓
令牌桶算法 漏桶/固定窗口
↓ ↓
需要高精度? 需要高精度?
↓ ↓
滑动窗口 滑动窗口计数器
选择建议
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定窗口 | 简单、高性能 | 突刺现象 | 简单场景 |
| 滑动窗口 | 平滑、精度好 | 内存占用大 | 中等精度要求 |
| 令牌桶 | 允许突发、灵活 | 实现复杂 | API限流 |
| 漏桶 | 流量平滑 | 不允许突发 | 保护下游 |
| 精确滑动 | 精度最高 | 性能开销大 | 金融系统 |
| 分布式 | 集群协调 | 依赖外部存储 | 微服务 |
五、限流最佳实践
1. 分层限流
CDN层 → 网关层 → 服务层 → 接口层 → 用户层
2. 优雅降级
@Component
public class GracefulDegradation {
public ResponseEntity<?> handleRateLimit(String operation) {
switch (operation) {
case "product":
return getCachedProduct();
case "search":
return getHotRecommendations();
default:
return ResponseEntity.status(429).body("系统繁忙,请稍后重试");
}
}
}
3. 监控告警
# Prometheus告警规则
groups:
- name: rate_limit
rules:
- alert: HighRateLimitRejection
expr: rate(rate_limit_rejected_total[5m]) > 100
for: 2m
annotations:
summary: "限流拒绝率过高"
六、总结:限流的核心要领
限流不是万能药,但是系统稳定性的重要保障。记住这几个核心要领:
核心原则
- 预防为主:在系统设计阶段就要考虑限流
- 分层防护:从网关到应用的多层限流
- 优雅降级:被限流时要有合理的降级策略
- 监控驱动:基于监控数据动态调整限流策略
选择建议
- 简单场景:固定窗口计数器
- 一般场景:令牌桶算法
- 严格场景:漏桶算法
- 高精度场景:滑动窗口限流
- 分布式场景:Redis分布式限流
最佳实践清单
- 根据业务特点选择合适的算法
- 配置合理的限流阈值
- 实现优雅的降级策略
- 建立完善的监控体系
- 支持动态调整限流参数
记住老司机的三句话:
- "限流是手段,不是目的,最终要保证用户体验"
- "没有完美的算法,只有合适的选择"
- "限流要有温度,让用户感受到系统的关怀"
关注"服务端技术精选",不迷路!
持续分享Java后端实战干货!
点赞、转发、收藏就是对我最大的支持!
下期预告:《缓存击穿又双叒叕发生了?这5种解决方案让你的系统永不宕机!》
限流算法选择思维导图:
业务需求 → 算法选择 → 参数调优 → 监控告警 → 持续优化
↓ ↓ ↓ ↓ ↓
突发流量 实现复杂度 阈值设置 关键指标 性能调优
标题:限流算法又双叒叕被击穿了?这6种算法让你的系统固若金汤!
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/21/1766304294832.html