本文目录导读:

下面系统地介绍 Java 分布式系统中常用的限流方案及其优化思路,我会从基本原理、常用算法、分布式实现、最佳实践与优化四个层面展开。
限流基本原理
限流的核心目标:控制单位时间内请求的速率,保护后端服务不被突发流量冲垮。
关键参数:
- 时间窗口(1秒、1分钟等)
- 最大请求数(如100次/秒)
- 等待/拒绝策略(排队、直接拒绝、降级)
常用限流算法
固定窗口计数器
原理: 将时间划分为固定窗口(如1秒),每个窗口内计数,超限则拒绝。
优点: 实现简单
缺点: 窗口边界易产生突刺(如在0.9秒和1.1秒各发送100次,实际1秒内可能200次)
滑动窗口计数器
原理: 将时间窗口细分为多个小格子(如1秒分为10个100ms格子),滑动统计,避免突刺。
数据结构: 常使用时间轮或环形缓冲区
优点: 比固定窗口更平滑
缺点: 存储成本稍高
令牌桶算法
原理: 以固定速率向桶中放入令牌,请求到达时从桶中取令牌,取到则通过,否则限流。
特点:
- 可以处理短时突发(桶中有存量令牌)
- 持续超过速率后开始限流
漏桶算法
原理: 以固定速率从桶中流出请求,请求进入桶中排队(超过桶容量则丢弃)。
特点:
- 强制平滑流出,不可突发
- 适合保护数据库等脆弱资源
自适应限流(推荐)
原理: 根据系统当前负载(CPU、内存、RT、QPS、GC等)动态调整限流阈值。
代表算法:
- TCP Vegas 思想的延时自适应
- Netflix Concurrency Limits(基于CPU或延迟)
- Sentinel 的系统自适应限流
分布式限流的实现方式
中间件集中式存储
| 方案 | 说明 | 优缺点 |
|---|---|---|
| Redis + Lua | 在Redis中存储计数器,用Lua脚本保证原子性 | 通用、性能好;需考虑Redis高可用 |
Redis + INCR + EXPIRE |
简单计数器,配合过期时间 | 实现简单,但窗口边界不好控制(可用Redis Sorted Set改进) |
| MySQL + 乐观锁 | 数据库行锁控制 | 性能差,仅低并发可用 |
Redis + Lua 示例(滑动窗口 + 令牌桶混合)
-- 限流key: rate_limit:{目标}:{时间窗口}
-- 参数: KEY[1]=限流key, ARGV[1]=最大请求数, ARGV[2]=窗口大小(ms)
local key = KEYS[1]
local maxRequests = tonumber(ARGV[1])
local windowMs = tonumber(ARGV[2])
local now = redis.call('TIME')[1] * 1000 -- 毫秒
local oldest = now - windowMs
-- 移除过期数据
redis.call('ZREMRANGEBYSCORE', key, 0, oldest)
-- 统计当前窗口请求数
local current = redis.call('ZCARD', key)
if current < maxRequests then
redis.call('ZADD', key, now, now) -- 添加当前请求
redis.call('EXPIRE', key, windowMs/1000 + 1)
return 1 -- 允许
else
return 0 -- 拒绝
end
分布式限流框架
| 框架 | 特点 |
|---|---|
| Sentinel | 阿里开源,支持QPS/线程数/自适应限流,支持集群模式(需Token Server) |
| Resilience4j | 轻量级,可与Spring Cloud集成,缺点是分布式需要自己实现存储(Redis) |
| Guava RateLimiter | 单机令牌桶,不能直接用于分布式,可作为本地预检 |
流量染色与限流中心
网关层做统一限流(如Spring Cloud Gateway + Redis),业务层做精细限流。
推荐架构:
Request → API Gateway(全局限流) → 业务服务(本地预检+服务间调用限流)
限流优化策略
本地预检 + 分布式兜底(高性能方案)
思路:
- 每台机器持有单机令牌桶(Guava RateLimiter),允许突发但总量控制
- 超过本地阈值后,再向Redis集群请求验证
优点: 减少Redis调用次数(降低延迟),适合读多写少的场景
public class HybridRateLimiter {
private final RateLimiter localLimiter; // 本地令牌桶
private final DistributedRateLimiter redisLimiter; // 分布式限流
private final double localRatio; // 本地允许占比
public boolean tryAcquire() {
// 1. 本地预检
if (localLimiter.tryAcquire()) {
return true;
}
// 2. 本地超限 -> 请求Redis兜底检查
return redisLimiter.tryAcquire();
}
}
缓存热点规避
- 限流Key设计避免热点:如
rate_limit:user:{userId}改为rate_limit:user:{hash(userId) % bucketCount} - 使用一致性哈希将限流数据分散到不同Redis分片
限流精度优化
| 策略 | 说明 |
|---|---|
| 批处理 | 将多个限流请求合并为一个Redis调用(减少网络IO) |
| 异步持久化 | 限流计数先写本地内存,异步批量同步到Redis |
| 近实时补偿 | 本地计数+定期与Redis对账(适合可容忍少量误差的场景) |
降级与熔断联动
- 当限流触发时,返回友好的降级信息(如“请求过载,请稍后重试”)
- 结合熔断器(如Hystrix/Resilience4j):当限流率达到阈值时熔断,避免雪崩
动态调整阈值
- 根据CPU使用率、RT、GC次数实时调整限流阈值(Sentinel系统保护)
- 支持配置中心(Apollo/Nacos)动态修改限流参数
生产最佳实践推荐
首选方案:Sentinel
- 支持单机/集群限流
- 内置自适应限流(系统规则)
- 支持热点参数限流(同一用户ID限流,不同用户不干扰)
- 控制台可实时监控
轻量方案:Redis + Lua
适合已有Redis基础设施、不想引入新框架的项目。
注意点:
- Redis必须主从+哨兵或Cluster
- 限流Lua脚本要加 加锁(单key)或使用Redlock(跨分片时)
- 避免热点key打爆单个Redis节点
极致性能方案:DPU/硬件卸载
- 在云原生环境下,可使用 eBPF + XDP 在网卡层做限流
- 适用于超大规模(如百万QPS)、毫秒级限流要求
常见限流场景对比
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 接口QPS限流(如1000/秒) | Sentinel 或 Redis+滑动窗口 | 精度要求高,分布式协同 |
| 用户维度的限流(如每人10次/分钟) | Redis + 自过期计数器 | 存储方便,Key可分散 |
| 保护MySQL/DB连接池 | 漏桶 + 连接池线程数控制 | 强制平滑,防止连接池打满 |
| 保护下游慢接口 | 自适应限流(延迟反馈) | 根据下游实际能力动态调整 |
| 秒杀/抢购场景 | 令牌桶 + 本地预检 | 允许短时突发,降低Redis压力 |
代码示例:基于 Redis Sorted Set 的滑动窗口
@Slf4j
public class RedisSlidingWindowRateLimiter {
private final StringRedisTemplate redisTemplate;
private final String keyPrefix = "ratelimit:";
public boolean tryAcquire(String resource, int maxRequests, long windowSeconds) {
String key = keyPrefix + resource;
long now = System.currentTimeMillis();
long oldest = now - windowSeconds * 1000;
List<String> keys = Collections.singletonList(key);
String luaScript =
"redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]);" +
"local count = redis.call('ZCARD', KEYS[1]);" +
"if count < tonumber(ARGV[2]) then " +
" redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3]);" +
" redis.call('EXPIRE', KEYS[1], ARGV[4]);" +
" return 1;" +
"else " +
" return 0;" +
"end";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(script, keys,
String.valueOf(oldest),
String.valueOf(maxRequests),
String.valueOf(now),
String.valueOf(windowSeconds + 1));
return result != null && result == 1;
}
}
总结优化金字塔
最高性能 最低延迟
▲ 本地预检 + 分布式兜底
│ ↑
│ Redis + Lua 原子操作
│ ↑
│ Sentinel 自适应
│ ↑
│ 熔断 + 降级联动
│ ↑
最低性能 最高可靠
建议:
- 先选算法:滑动窗口或令牌桶(90%场景足够)
- 后选存储:Redis(分布式)或本地(单体快速启动)
- 再看扩展:是否需要动态调整、熔断、监控、热点限流
- 最终验证:压测QPS曲线是否平滑,是否有突刺
如果需要某个具体方案(如 Sentinel 集群部署、基于 CPU 的自适应限流代码)的详细实现,可以继续追问。