高并发场景下限流算法对比与实践指南
在电商秒杀、API 网关、大规模微服务调用等高并发场景中,服务端往往需要对请求进行限流保护,以防止资源耗尽和系统崩溃。本文将围绕常见的限流算法进行对比分析,并提供 Java 与 Redis 实战示例,帮助你根据业务场景选型并落地。
一、问题背景介绍
- 秒杀场景:在秒杀活动开始瞬间,瞬间涌入的请求数可能达到上万、几十万,如果不进行有效限流,后端服务与数据库将难以承受。
- API 网关:作为统一入口,需要保证各下游服务的稳定,避免单个业务异常导致整体雪崩。
- 微服务调用:在分布式调用链中,上游服务爆发式请求可能导致下游服务压力过大,需要主动降级或限流。
限流方案主要用于控制请求速率、并发量等,并可与熔断、降级等机制配合,提升系统稳定性。
二、多种限流方案对比
本文将重点对比以下几种限流算法:
- 令牌桶(Token Bucket)
- 漏桶(Leaky Bucket)
- 固定时间窗口(Fixed Window)
- 滑动时间窗口(Sliding Window)
- 分布式计数(基于 Redis + Lua)
- Sentinel 流控
| 方案 | 维度 | 优点 | 缺点 | |----------------|----------------|------------------------------------|------------------------------------| | 令牌桶 | 速率控制 | 支持突发流量,平滑放行 | 需定期生成令牌,内存中实现适合单机 | | 漏桶 | 并发控制 | 实现简单,可稳定输出 | 不支持突发,大流量瞬间丢弃 | | 固定时间窗口 | 计数 | 算法简单,性能高 | 窗口边界效应,大并发时容易突发 | | 滑动时间窗口 | 计数 | 统计更精确,避免固定窗口缺陷 | 内存或 Redis 存储粒度较大,计算复杂 | | Redis + Lua | 分布式计数 | 多实例共享状态,支持集群 | 脚本执行阻塞,网络抖动影响精度 | | Sentinel 流控 | 多维度+隔离策略 | 动态规则下发,支持 QPS/线程/调用链等多维度 | 需引入依赖,学习成本较高 |
三、各方案优缺点分析
3.1 令牌桶(Token Bucket)
- 原理:以固定速率往桶中放入令牌,请求到来时尝试获取令牌,如果获取成功则放行,否则拒绝或等待。
- 适用场景:允许突发流量,但总速率受控;如 API 网关入口。
- 示例代码(Guava RateLimiter):
// 每秒生成 100 个令牌
RateLimiter limiter = RateLimiter.create(100);
public void handleRequest() {
// 尝试获取令牌,最多等待 100 毫秒
if (limiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS)) {
// 执行业务逻辑
} else {
// 拒绝或限流处理
System.out.println("限流:无可用令牌");
}
}
3.2 漏桶(Leaky Bucket)
- 原理:将请求排队进入漏桶,以固定速率“漏出”,如果队列满则拒绝新请求。
- 适用场景:对等量持续流量场景,如消息队列消费。
- 示例:可使用阻塞队列实现,或中间件自带功能。
3.3 固定时间窗口 & 滑动时间窗口
固定窗口(Fixed Window):以固定时长(如 1 秒)为单位计数,超出阈值则拒绝。
- 简洁高效,但存在窗口边界突发。
滑动窗口(Sliding Window):将时间分为多个小窗口,统计多个分片计数值,平滑度更高。
// 简化示例:需要按时间槽存储计数,生产环境可用 Redis ZSET 或 Hash
class SlidingWindowRateLimiter {
private final long windowSizeInMillis;
private final int maxCount;
private final Deque<Long> timestamps = new LinkedList<>();
public SlidingWindowRateLimiter(long windowSizeInMillis, int maxCount) {
this.windowSizeInMillis = windowSizeInMillis;
this.maxCount = maxCount;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 清理过期记录
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowSizeInMillis) {
timestamps.pollFirst();
}
if (timestamps.size() < maxCount) {
timestamps.addLast(now);
return true;
}
return false;
}
}
3.4 分布式计数(Redis + Lua)
- 原理:利用 Redis 原子脚本执行计数与过期,支持多实例同步限流状态。
- 示例脚本:
-- KEYS[1]: 限流 key
-- ARGV[1]: 窗口时间(秒)
-- ARGV[2]: 最大请求数
local current = redis.call('incr', KEYS[1])
if tonumber(current) == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if tonumber(current) > tonumber(ARGV[2]) then
return 0
end
return 1
String script = "<上面 Lua 脚本内容>";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
public boolean isAllowed(String key, int windowSec, int maxCount) {
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(key),
String.valueOf(windowSec), String.valueOf(maxCount));
return result != null && result == 1;
}
3.5 Sentinel 流控
- 原理:基于热点参数治理与多维度度量,动态下发流控规则。
- 优势:支持业务规则配置,如热点接口、微服务调用链、QPS、线程数多维度。
// 规则示例:单机 QPS 不超过 500
FlowRule rule = new FlowRule();
rule.setResource("/seckill");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(500);
FlowRuleManager.loadRules(Collections.singletonList(rule));
四、选型建议与适用场景
| 方案 | 适用场景 | 建议 | |------------------|--------------------------|-----------------------------------------| | Token Bucket | API 网关突发限流 | 对突发场景友好,配合延迟队列更佳 | | Leaky Bucket | 均衡消费场景 | 稳定输出,适合消息消费、日志写入等 | | Fixed Window | 内部管理或低精度限流 | 实现最简单,推荐阈值不敏感场景 | | Sliding Window | 精确限流场景 | 对精度要求高,业务稳定性要求高的 API | | Redis + Lua | 分布式微服务限流 | 多机共享限流状态;注意 Redis 可用性并发量 | | Sentinel | 大规模微服务熔断+限流 | 对系统有全面防护能力,建议与熔断、降级结合 |
五、实际应用效果验证
以秒杀系统为例,使用 Redis + Lua 实现的固定窗口限流,通过压测工具(如 JMeter)模拟 5000 并发场景:
- 无限流保护:后端 QPS 飙升至 6000+/s,数据库连接池耗尽,出现大量超时。
- Fixed Window(5000/s):请求被平滑限制在 5000/s,系统稳定,无异常。
- Sliding Window(5000/s):输出更平滑,响应时间均值下降 15%。
- Token Bucket(5000/s, maxBurst=200):突发允许200请求,但整体 QPS 同样受控。
通过以上对比,可以根据业务特性选择不同方案,兼顾平滑度和实现复杂度。
综上所述,限流算法在高并发场景中非常关键。本文对比了多种常见方案,并给出实践示例和选型建议。希望对你在秒杀、电商、API 网关等业务中落地限流策略有所帮助。