📖 开场:高速公路收费站
想象高速公路收费站 🛣️:
没有限流(拥堵):
国庆假期:
100万辆车同时上高速
↓
收费站只有10个通道
↓
全部堵死!💥
结果:
- 收费站瘫痪
- 高速公路瘫痪
- 所有车都走不了 😱
有限流(顺畅):
国庆假期:
100万辆车排队
↓
收费站:每分钟只放行1000辆 🚦
↓
超过1000辆 → 排队等待 ⏰
结果:
- 收费站正常运行 ✅
- 高速公路流畅 ✅
- 虽然慢,但不瘫痪 ✅
这就是限流的作用:保护系统,防止过载!
🤔 为什么需要限流?
场景1:秒杀活动 🛒
没有限流:
10:00:00 秒杀开始
↓
100万用户同时点击
↓
服务器QPS:100万
↓
服务器承受:1万QPS
↓
服务器崩溃!💀
有限流:
10:00:00 秒杀开始
↓
100万用户同时点击
↓
限流:每秒只处理1万请求 🚦
↓
超过的请求:返回"系统繁忙,请稍后再试" ⏰
↓
服务器稳定运行 ✅
场景2:API接口保护 🔒
没有限流:
恶意用户:疯狂调用API
↓
每秒10万次
↓
服务器崩溃 💀
正常用户无法访问 ❌
有限流:
每个用户:每秒最多100次 🚦
↓
恶意用户被限制
↓
正常用户正常使用 ✅
🎯 限流算法
算法1:固定窗口计数器 🪟
原理
时间窗口:1秒
限流阈值:100次
0-1秒:计数器 = 0
↓
请求1 → 计数器+1 = 1 → 通过 ✅
请求2 → 计数器+1 = 2 → 通过 ✅
...
请求100 → 计数器+1 = 100 → 通过 ✅
请求101 → 计数器+1 = 101 → 拒绝 ❌
1秒后,计数器重置为0
代码实现
@Component
public class FixedWindowRateLimiter {
private Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
private Map<String, Long> windows = new ConcurrentHashMap<>();
private final int limit = 100; // 限流阈值
private final long windowSize = 1000; // 窗口大小(毫秒)
/**
* ⭐ 尝试获取令牌
*/
public boolean tryAcquire(String key) {
long now = System.currentTimeMillis();
// 获取当前窗口的起始时间
Long windowStart = windows.get(key);
if (windowStart == null || now - windowStart >= windowSize) {
// 新窗口,重置计数器
windows.put(key, now);
counters.put(key, new AtomicInteger(0));
}
// 计数器+1
AtomicInteger counter = counters.get(key);
int count = counter.incrementAndGet();
if (count <= limit) {
// 未超过限制
return true;
} else {
// 超过限制
return false;
}
}
}
问题:临界问题
假设限流:100次/秒
0.5秒:99次请求 ✅
1.0秒:窗口重置
1.5秒:99次请求 ✅
0.5秒-1.5秒这1秒内:
实际通过了 99 + 99 = 198次 ❌
超过了限流阈值!
缺点:
- 临界问题(窗口边界)
- 突刺流量(瞬间大量请求)
算法2:滑动窗口计数器 🪟🪟
原理
窗口大小:1秒
划分:10个小窗口(每个100ms)
当前时间:1000ms
滑动窗口:[100ms-1000ms]
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 10│ 12│ 15│ 8 │ 20│ 18│ 10│ 5 │ 2 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
100 200 300 400 500 600 700 800 900 1000
总请求数:10+12+15+8+20+18+10+5+2+0 = 100
下一个100ms:
- 丢弃最左边的窗口(10)
- 添加新窗口(右边)
优点:
- 解决临界问题
- 更平滑
代码实现(Redis)
@Component
public class SlidingWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private final int limit = 100; // 限流阈值
private final long windowSize = 1000; // 窗口大小(毫秒)
/**
* ⭐ 尝试获取令牌
*/
public boolean tryAcquire(String key) {
long now = System.currentTimeMillis();
String redisKey = "rate_limit:" + key;
// ⭐ 1. 删除窗口外的数据
long windowStart = now - windowSize;
redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
// ⭐ 2. 统计窗口内的请求数
Long count = redisTemplate.opsForZSet().zCard(redisKey);
if (count != null && count >= limit) {
// 超过限制
return false;
}
// ⭐ 3. 添加当前请求
redisTemplate.opsForZSet().add(redisKey, String.valueOf(now), now);
// ⭐ 4. 设置过期时间
redisTemplate.expire(redisKey, windowSize + 1000, TimeUnit.MILLISECONDS);
return true;
}
}
算法3:漏桶算法(Leaky Bucket)💧
原理
漏桶:
- 容量:100个请求
- 流出速率:10个/秒(固定)
请求进入漏桶:
┌─────────────┐
│ │ ← 请求
│ 漏桶 │
│ 💧💧💧 │
│ 💧💧💧 │
│ 💧💧💧 │
└──────┬──────┘
│ 10个/秒(固定速率)
↓
处理请求
特点:
- 流出速率固定 ✅
- 平滑处理 ✅
- 桶满则拒绝 ❌
代码实现(Guava RateLimiter)
@Component
public class LeakyBucketRateLimiter {
private final RateLimiter rateLimiter;
public LeakyBucketRateLimiter() {
// ⭐ 创建限流器:每秒10个令牌
this.rateLimiter = RateLimiter.create(10);
}
/**
* ⭐ 尝试获取令牌(阻塞)
*/
public boolean tryAcquire() {
// 阻塞等待,直到获取到令牌
rateLimiter.acquire();
return true;
}
/**
* ⭐ 尝试获取令牌(非阻塞,超时返回)
*/
public boolean tryAcquire(long timeout, TimeUnit unit) {
// 尝试获取,最多等待timeout时间
return rateLimiter.tryAcquire(timeout, unit);
}
}
优缺点
优点 ✅:
- 流出速率固定,平滑处理
- 防止突刺流量
缺点 ❌:
- 无法应对短时间的突发流量
- 响应慢(需要等待)
算法4:令牌桶算法(Token Bucket)🪙
原理
令牌桶:
- 容量:100个令牌
- 生成速率:10个/秒
令牌生成:
每秒生成10个令牌,放入桶中
桶满则丢弃
请求到来:
1. 尝试从桶中取1个令牌
2. 取到 → 处理请求 ✅
3. 取不到 → 拒绝请求 ❌
特点:
- 允许突发流量(桶中有令牌就能处理)✅
- 长期平均速率受限(10个/秒)✅
图解对比
漏桶 vs 令牌桶:
漏桶(Leaky Bucket):
请求 → 漏桶 → 固定速率流出
特点:流出速率固定
令牌桶(Token Bucket):
令牌按固定速率生成 → 桶
请求 → 取令牌 → 处理
特点:允许突发流量(桶中有令牌)
例子:
突然来100个请求:
- 漏桶:只能每秒处理10个,其他排队
- 令牌桶:桶中有100个令牌,可以立即处理100个 ✅
代码实现(Redis + Lua)
Lua脚本(原子操作):
-- ⭐ 令牌桶算法(Lua脚本)
-- KEYS[1]: 桶的key
-- ARGV[1]: 令牌生成速率(tokens/秒)
-- ARGV[2]: 桶容量
-- ARGV[3]: 当前时间戳(秒)
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间
-- 获取桶中的令牌数和上次更新时间
local tokens = tonumber(redis.call('hget', key, 'tokens')) or capacity
local last_time = tonumber(redis.call('hget', key, 'last_time')) or now
-- 计算时间间隔
local delta = now - last_time
-- 生成新令牌
local new_tokens = math.min(capacity, tokens + delta * rate)
-- 尝试获取1个令牌
if new_tokens >= 1 then
-- 有令牌,扣除1个
new_tokens = new_tokens - 1
-- 更新桶状态
redis.call('hset', key, 'tokens', new_tokens)
redis.call('hset', key, 'last_time', now)
redis.call('expire', key, 10)
return 1 -- 成功
else
-- 没有令牌
return 0 -- 失败
end
Java代码:
@Component
public class TokenBucketRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private final int rate = 10; // 令牌生成速率:10个/秒
private final int capacity = 100; // 桶容量:100个
private final String luaScript =
"local key = KEYS[1]\n" +
"local rate = tonumber(ARGV[1])\n" +
"local capacity = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"\n" +
"local tokens = tonumber(redis.call('hget', key, 'tokens')) or capacity\n" +
"local last_time = tonumber(redis.call('hget', key, 'last_time')) or now\n" +
"\n" +
"local delta = now - last_time\n" +
"local new_tokens = math.min(capacity, tokens + delta * rate)\n" +
"\n" +
"if new_tokens >= 1 then\n" +
" new_tokens = new_tokens - 1\n" +
" redis.call('hset', key, 'tokens', new_tokens)\n" +
" redis.call('hset', key, 'last_time', now)\n" +
" redis.call('expire', key, 10)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
/**
* ⭐ 尝试获取令牌
*/
public boolean tryAcquire(String key) {
String redisKey = "token_bucket:" + key;
// 执行Lua脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(redisKey),
String.valueOf(rate),
String.valueOf(capacity),
String.valueOf(System.currentTimeMillis() / 1000)
);
return result != null && result == 1;
}
}
优缺点
优点 ✅:
- 允许突发流量
- 灵活(可以瞬间处理多个请求)
- 适合大部分场景
缺点 ❌:
- 实现复杂(相比固定窗口)
推荐 ⭐⭐⭐:令牌桶是最常用的限流算法
🎯 分布式限流
方案1:Redis + Lua ⭐⭐⭐
原理:
- Redis存储限流状态
- Lua脚本保证原子性
- 支持多服务器
代码示例(已在上面的令牌桶算法中展示)
方案2:Nginx限流 🔧
配置:
http {
# ⭐ 定义限流规则
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
# ↑ ↑ ↑
# 按IP限流 共享内存10MB 每秒10个请求
server {
location /api/ {
# ⭐ 应用限流
limit_req zone=mylimit burst=20 nodelay;
# ↑ ↑
# 突发20个 不延迟
proxy_pass http://backend;
}
}
}
优点:
- 性能高(Nginx C语言)
- 配置简单
缺点:
- 功能有限(只支持固定窗口)
- 无法动态调整
方案3:Sentinel(阿里开源)☁️
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel控制台
port: 8719
使用注解:
@RestController
@RequestMapping("/api")
public class OrderController {
/**
* ⭐ 限流注解
*/
@GetMapping("/order")
@SentinelResource(
value = "getOrder",
blockHandler = "handleBlock" // 限流后的处理方法
)
public Result<Order> getOrder(@RequestParam Long orderId) {
Order order = orderService.getById(orderId);
return Result.success(order);
}
/**
* 限流后的处理方法
*/
public Result<Order> handleBlock(Long orderId, BlockException ex) {
return Result.fail("系统繁忙,请稍后再试");
}
}
编程式限流:
@Service
public class OrderService {
public Order createOrder(Order order) {
// ⭐ 定义限流规则
Entry entry = null;
try {
entry = SphU.entry("createOrder");
// 业务逻辑
orderDao.insert(order);
return order;
} catch (BlockException ex) {
// 限流了
throw new RuntimeException("系统繁忙,请稍后再试");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
动态配置规则:
@PostConstruct
public void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("getOrder"); // 资源名
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS模式
rule.setCount(100); // 限流阈值:100 QPS
rules.add(rule);
// ⭐ 加载规则
FlowRuleManager.loadRules(rules);
}
优点:
- 功能强大(限流、熔断、降级)
- 可视化控制台
- 动态配置
📊 架构总结
分布式限流系统架构
┌──────────────────────────────────────┐
│ 客户端 │
└─────────────┬────────────────────────┘
│
↓
┌──────────────────────────────────────┐
│ Nginx(第一道防线) │
│ - IP限流 │
│ - 粗粒度限流 │
└─────────────┬────────────────────────┘
│
↓
┌──────────────────────────────────────┐
│ 应用服务器(第二道防线) │
│ │
│ - 接口级限流 │
│ - 用户级限流 │
│ - 细粒度限流 │
└───────┬──────────────────────────────┘
│
↓
┌──────────────┐
│ Redis │
│ │
│ - 限流状态 │
│ - Lua脚本 │
└──────────────┘
🎓 面试题速答
Q1: 限流算法有哪些?
A: 四种算法:
-
固定窗口计数器:
- 简单,但有临界问题
-
滑动窗口计数器:
- 解决临界问题,更平滑
-
漏桶算法:
- 流出速率固定,平滑处理
-
令牌桶算法(推荐)⭐:
- 允许突发流量,最常用
Q2: 令牌桶和漏桶的区别?
A: 核心区别:
漏桶:
- 流出速率固定
- 请求匀速处理
- 不能应对突发流量
令牌桶:
- 令牌生成速率固定
- 允许突发流量(桶中有令牌就能处理)
- 更灵活 ✅
例子:
突然来100个请求:
- 漏桶:每秒只能处理10个,其他排队
- 令牌桶:桶中有100个令牌,立即处理100个 ✅
Q3: 如何实现分布式限流?
A: Redis + Lua:
// Lua脚本(原子操作)
String luaScript =
"local tokens = redis.call('hget', KEYS[1], 'tokens')\n" +
"if tokens >= 1 then\n" +
" redis.call('hincrby', KEYS[1], 'tokens', -1)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
// 执行
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList("rate_limit:" + key)
);
优点:
- 原子性(Lua脚本)
- 支持多服务器
- 性能高
Q4: 固定窗口的临界问题是什么?
A: 临界问题:
假设限流:100次/秒
0.5秒:99次 ✅
1.0秒:窗口重置
1.5秒:99次 ✅
0.5秒-1.5秒这1秒内:
实际通过了 198次 ❌
超过了限流阈值!
解决:滑动窗口计数器
Q5: Sentinel有什么优势?
A: 三大优势:
-
功能强大:
- 限流
- 熔断
- 降级
-
可视化控制台:
- 实时监控
- 动态配置规则
-
易用性:
- 注解式
- 编程式
- 零侵入
使用:
@SentinelResource(value = "getOrder", blockHandler = "handleBlock")
public Result<Order> getOrder(Long orderId) {
// 业务逻辑
}
Q6: 如何选择限流算法?
A: 根据场景选择:
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| API接口 | 令牌桶 ⭐ | 允许突发流量 |
| 秒杀 | 令牌桶 ⭐ | 控制QPS |
| 消息队列 | 漏桶 | 平滑处理 |
| 简单场景 | 固定窗口 | 实现简单 |
推荐:令牌桶算法(最常用)⭐⭐⭐
🎬 总结
限流算法对比
┌────────────────────────────────────┐
│ 固定窗口计数器 │
│ - 简单,但有临界问题 ⭐ │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 滑动窗口计数器 │
│ - 解决临界问题 ⭐⭐ │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 漏桶算法 │
│ - 流出速率固定 ⭐⭐ │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 令牌桶算法(推荐)⭐⭐⭐ │
│ - 允许突发流量 │
│ - 最常用 │
└────────────────────────────────────┘
令牌桶是最佳选择!✅
🎉 恭喜你!
你已经完全掌握了限流系统的设计!🎊
核心要点:
- 令牌桶算法:允许突发流量,最常用
- Redis + Lua:分布式限流,原子性
- Sentinel:功能强大,可视化
- Nginx限流:第一道防线
下次面试,这样回答:
"限流系统采用令牌桶算法。令牌按固定速率生成(如每秒10个),请求到来时尝试获取令牌,获取成功则处理,失败则拒绝。允许突发流量,如果桶中有100个令牌,可以瞬间处理100个请求。
分布式限流使用Redis + Lua脚本实现。Lua脚本计算令牌数量并扣除,保证原子性。支持多服务器,状态存储在Redis中。
实际项目中,Nginx作为第一道防线做IP级别的粗粒度限流,应用服务器做接口级别的细粒度限流。使用Sentinel框架,通过注解@SentinelResource标记资源,动态配置限流规则,并通过可视化控制台实时监控。
我们项目的秒杀接口使用令牌桶限流,QPS限制在1万,超过的请求返回'系统繁忙',保护系统稳定运行。"
面试官:👍 "很好!你对限流系统的设计理解很深刻!"
本文完 🎬
上一篇: 205-设计一个分布式延迟任务调度系统.md
下一篇: 207-设计一个分布式配置中心.md
作者注:写完这篇,我都想去高速公路当收费员了!🚦
如果这篇文章对你有帮助,请给我一个Star⭐!