Java分布式限流案例开发实战:从原理到高并发落地
目录导读
- 为什么分布式系统必须考虑限流?
- 分布式限流的核心算法对比(令牌桶 vs 漏桶 vs 滑动窗口)
- 基于Redis + Lua实现分布式限流(含完整代码案例)
- 使用Sentinel实现集群限流(Spring Cloud Alibaba集成)
- 常见问题与问答(FAQ)
- 性能优化与最佳实践
为什么分布式系统必须考虑限流?
在微服务架构中,单个服务节点可以通过本地限流(如Guava RateLimiter)控制流量,但当流量分散到多个实例(如Kubernetes多Pod)时,本地限流会失效,你为每个实例设置了100 QPS,部署了5个实例,但外部恶意请求可能同时打满所有实例,导致总QPS达到500,最终压垮下游数据库或第三方服务。

分布式限流的核心目标:在集群层面统一控制总请求速率,确保系统在任何时刻都不超过预设阈值。
分布式限流核心算法对比
| 算法 | 原理 | 优缺点 | 适用场景 |
|---|---|---|---|
| 令牌桶 | 固定速率生成令牌,桶满则丢弃;请求需获取令牌才能执行 | ✅ 允许突发流量(桶可缓存令牌);❌ 实现稍复杂 | API网关、秒杀系统 |
| 漏桶 | 请求以固定速率流出,不管流入速率多快 | ✅ 强制平滑流量;❌ 无法应对突发请求 | 数据库写入限流 |
| 滑动窗口 | 将时间划分为小窗口,统计每个窗口请求数 | ✅ 精度高(避免临界问题);❌ 内存占用随窗口数增加 | 实时监控、短时精准限流 |
推荐:令牌桶 + Redis,因为它在允许突发流量的同时,通过Redis原子性操作保证分布式一致性。
基于Redis + Lua实现分布式限流(核心案例)
1 设计思路
- 存储结构:使用Redis的
KEY存储当前令牌数,KEY:last_refill_time存储上次补充时间。 - 原子性:通过Lua脚本一次性完成“补充令牌+扣减令牌+返回结果”,避免并发竞争。
- 参数:
key(用户/接口ID)、capacity(桶容量)、rate(每秒生成令牌数)、cost(每次请求消耗令牌数,默认为1)。
2 完整Lua脚本
-- 参数:KEYS[1]=令牌桶key, ARGV[1]=容量, ARGV[2]=速率, ARGV[3]=当前时间戳(秒), ARGV[4]=消耗令牌数
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
-- 获取当前令牌数
local tokens = redis.call("get", key)
local last_refill = redis.call("get", key .. ":last_refill")
if tokens == false then
-- 首次初始化:全部令牌
tokens = capacity
last_refill = now
else
tokens = tonumber(tokens)
last_refill = tonumber(last_refill)
-- 计算需要补充的令牌数
local elapsed = now - last_refill
local add_tokens = elapsed * rate
if add_tokens > 0 then
tokens = math.min(tokens + add_tokens, capacity)
last_refill = now
end
end
if tokens >= cost then
tokens = tokens - cost
— 更新到Redis
redis.call("set", key, tokens)
redis.call("set", key .. ":last_refill", last_refill)
return 1 — 允许请求
else
return 0 — 拒绝请求
end
3 Java调用代码(使用Spring Data Redis)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class DistributedRateLimiter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 加载Lua脚本(脚本内容按上述)
private static final String LUA_SCRIPT = "local key = KEYS[1] ..."; // 完整脚本粘贴
private static final RedisScript<Long> RATE_LIMIT_SCRIPT = RedisScript.of(LUA_SCRIPT, Long.class);
public boolean tryAcquire(String key, int capacity, int rate, int cost) {
long now = System.currentTimeMillis() / 1000;
Long result = redisTemplate.execute(
RATE_LIMIT_SCRIPT,
Collections.singletonList(key),
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(now),
String.valueOf(cost)
);
return result != null && result == 1L;
}
}
4 测试验证
public class RateLimitTest {
public static void main(String[] args) throws InterruptedException {
RateLimiter limiter = new RateLimiter(); // 假设已注入
String key = "api:order:create";
for (int i = 0; i < 20; i++) {
boolean allowed = limiter.tryAcquire(key, 10, 2, 1); // 容量10,每秒2个
System.out.println("请求" + i + ": " + (allowed ? "通过" : "被限流"));
Thread.sleep(100);
}
}
}
输出预期:前10个请求快速通过(消耗桶中初始10个令牌),之后每0.5秒通过1个(因为rate=2/s),超出部分被限流。
使用Sentinel实现集群限流(生产级方案)
如果不想自己维护Lua脚本,推荐阿里开源的Sentinel,它原生支持集群限流。
1 集成步骤(Spring Cloud Alibaba)
-
引入依赖(pom.xml):
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-cluster-client-default</artifactId> </dependency>
-
配置限流规则(application.yml):
sentinel: transport: dashboard: localhost:8080 # 控制台地址 datasource: ds1: nacos: server-addr: localhost:8848 dataId: sentinel-rules groupId: DEFAULT_GROUP rule-type: flow -
定义资源(Java代码):
@RestController public class OrderController { @SentinelResource(value = "createOrder", blockHandler = "handleBlock") public String createOrder() { return "下单成功"; } public String handleBlock(BlockException ex) { return "被限流,请稍后重试"; } } -
在Sentinel控制台配置:选择“集群流控”,设置QPS阈值、Token Server地址。
优势:支持实时监控、动态规则下发、自动故障转移。
常见问题与问答(FAQ)
Q1:为什么不用Guava RateLimiter做分布式限流?
A:Guava基于JVM内存,无法感知其他实例的流量,如果部署2个实例,每个实例阈值100,总QPS可能达到200,超出系统上限。
Q2:Redis实现分布式限流,性能瓶颈在哪里?如何优化?
A:瓶颈是每个请求都需与Redis交互(网络I/O),优化方案:
- 使用本地缓存+定期同步(如每个实例从Redis预取令牌,本地消费后定期回写)。
- 或使用Redis Pipeline批量处理。
- 高并发场景推荐Sentinel(它通过Token Server减少Redis调用)。
Q3:限流key应该如何设计?
A:按业务维度区分:
- 用户级别:
rate_limit:user:{userId},避免单个用户刷接口。 - API接口:
rate_limit:api:{method:path},控制整体接口访问。 - IP级别:
rate_limit:ip:{ip},防止爬虫。
Q4:如果Redis宕机,限流功能失效怎么办?
A:建议降级策略:
- 熔断后直接返回“服务繁忙”,或降级为本地限流(Guava)兜底。
- Sentinel集群模式支持健康检查,当Token Server不可用时自动切换。
性能优化与最佳实践
- 批量扣减:如果一次请求消耗多个资源(如批量下单),在Lua中设置
cost值,避免多次调用Redis。 - 预热与冷却:系统启动时,令牌桶初始化为空,防止冷启动被大量请求打满;使用Sentinel的“预热”模式可平滑启动。
- 日志与监控:记录被限流的请求数(通过AOP拦截),接入Prometheus + Grafana,设置告警(如限流率>30%时报警)。
- 数据一致性:Lua脚本务必保证原子性,使用
EVAL命令而非EVALSHA缓存可能因脚本更新不一致。
一个高可用的分布式限流方案 = 合理的算法(令牌桶)+ 原子性存储(Redis Lua)+ 业务相关key设计 + 降级兜底策略,对于中小团队,直接使用Sentinel集成可以节省大量开发时间。