写在前面:说实话,限流这个事儿我在生产环境踩过太多次坑了。早些年做活动大促,固定窗口限流看着挺稳,结果一到零点就崩。后来啃了Sentinel源码,才真正搞明白滑动窗口的精髓。这篇文章把我这些年的踩坑经验和源码理解都掏出来了,希望能帮你少走点弯路。

文章目录
-
- 一、为什么需要限流?
-
- [1.1 高并发场景下的系统保护](#1.1 高并发场景下的系统保护)
- [1.2 固定窗口的边界突发问题](#1.2 固定窗口的边界突发问题)
- 二、滑动窗口算法原理
-
- [2.1 核心思想:把大窗口切成小格子](#2.1 核心思想:把大窗口切成小格子)
- [2.2 滑动过程详解](#2.2 滑动过程详解)
- [2.3 关键参数](#2.3 关键参数)
- 三、三种限流算法对比
-
- [3.1 滑动窗口 vs 固定窗口 vs 令牌桶](#3.1 滑动窗口 vs 固定窗口 vs 令牌桶)
- [3.2 怎么选?](#3.2 怎么选?)
- 四、Java实现滑动窗口限流
-
- [4.1 基于本地内存的实现(环形数组)](#4.1 基于本地内存的实现(环形数组))
- [4.2 基于Redis ZSet的分布式实现](#4.2 基于Redis ZSet的分布式实现)
- 五、Sentinel中的滑动窗口实现
-
- [5.1 LeapArray设计](#5.1 LeapArray设计)
- [5.2 Sentinel的实际应用](#5.2 Sentinel的实际应用)
- 六、问题与解答
-
- [Q1: 滑动窗口的格子数怎么选?](#Q1: 滑动窗口的格子数怎么选?)
- [Q2: 滑动窗口和令牌桶哪个更好?](#Q2: 滑动窗口和令牌桶哪个更好?)
- [Q3: Redis ZSet实现有什么缺点?](#Q3: Redis ZSet实现有什么缺点?)
- 七、面试高频考点
- 八、模拟面试官提问
- 九、互动话题
- 十、参考资料
一、为什么需要限流?
1.1 高并发场景下的系统保护
我见过太多人觉得"我的系统扛得住"。结果流量一来,数据库CPU飙到100%,Redis连接打满,整个链路雪崩。
限流的核心目的就三个:
- 保护后端资源:数据库、缓存、下游服务都有容量上限
- 防止雪崩效应:一个接口拖垮整个系统
- 保证服务质量:让大部分用户获得稳定的响应
踩坑提醒:限流不是万能的!我见过有人设置了限流就不做降级,结果限流阈值配高了,系统照样崩。限流+降级+熔断,这三件套缺一不可。
1.2 固定窗口的边界突发问题
固定窗口限流是最简单的实现,比如"1秒内最多100个请求"。但它有个致命的边界问题:
时间轴:|-------第1秒-------|-------第2秒-------|
请求: [95个] [95个]
^ ^
最后10ms 开头10ms
假设阈值是1秒100个请求:
- 第1秒的最后10ms涌入了95个请求
- 第2秒的开头10ms又涌入了95个请求
这两个时间段加起来只有20ms,却通过了190个请求!系统直接被打爆。
这就是固定窗口的边界突发问题,也是滑动窗口要解决的核心痛点。
二、滑动窗口算法原理
2.1 核心思想:把大窗口切成小格子
滑动窗口的做法很直观:把1秒的大窗口切成10个100ms的小格子,每个格子独立计数。
当前时间:1.35秒
窗口范围:[0.35s ~ 1.35s](共1秒)
格子划分:
| 0.35-0.45 | 0.45-0.55 | 0.55-0.65 | 0.65-0.75 | 0.75-0.85 | 0.85-0.95 | 0.95-1.05 | 1.05-1.15 | 1.15-1.25 | 1.25-1.35 |
| 8 | 12 | 5 | 20 | 15 | 10 | 25 | 18 | 7 | 10 |
总请求数 = 8+12+5+20+15+10+25+18+7+10 = 130
如果阈值是100,当前请求就会被拒绝。
2.2 滑动过程详解
时间每往前走一格,窗口就"滑动"一下:
T1时刻窗口:[0.0 ~ 1.0] 包含格子 G1,G2,G3,G4,G5,G6,G7,G8,G9,G10
T2时刻(过了100ms):
- 丢弃 G1(已经滑出窗口)
- 新增 G11(进入窗口)
- 新窗口:[0.1 ~ 1.1] 包含 G2~G11
用代码思维理解就是:只统计当前时间往前推1秒内的所有格子请求数之和。
2.3 关键参数
| 参数 | 说明 | 建议值 |
|---|---|---|
| 窗口大小 | 统计的时间范围 | 1秒 |
| 格子数量 | 窗口切分的粒度 | 10个 |
| 格子时长 | 每个格子代表的时间 | 100ms |
| 阈值 | 窗口内允许的最大请求数 | 根据压测结果设定 |
格子数量越多,精度越高,但内存占用和计算量也越大。后面会详细讲怎么选。
三、三种限流算法对比
3.1 滑动窗口 vs 固定窗口 vs 令牌桶
| 维度 | 固定窗口 | 滑动窗口 | 令牌桶 |
|---|---|---|---|
| 精度 | 低,有边界突发问题 | 高,平滑过渡 | 高,支持突发流量 |
| 内存占用 | 极低,只需一个计数器 | 中等,需维护多个格子 | 低,只需记录令牌数 |
| 实现复杂度 | 简单,几行代码搞定 | 中等,需维护窗口状态 | 中等,需定时生成令牌 |
| 突发流量处理 | 差,边界会双倍突发 | 一般,窗口内平滑 | 好,允许瞬间突发 |
| 适用场景 | 简单统计、日志限流 | API接口限流、Sentinel | 网络流量整形、带宽控制 |
| 分布式实现 | 容易 | 较复杂(Redis ZSet) | 较复杂(需同步令牌) |
3.2 怎么选?
- 固定窗口:内部监控、非核心接口、日志上报,对精度要求不高的场景
- 滑动窗口:电商秒杀、API网关限流、Sentinel场景,需要精确控制QPS
- 令牌桶:视频流、文件下载、需要允许合理突发的场景
说实话,大部分互联网公司的核心接口限流,用的都是滑动窗口或者令牌桶。固定窗口基本只出现在日志限流这种非关键场景。
四、Java实现滑动窗口限流
4.1 基于本地内存的实现(环形数组)
这个实现适合单机限流,性能最好,无外部依赖。
java
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* 基于环形数组的滑动窗口限流器
*
* 核心设计:
* 1. 用环形数组存储每个格子的请求计数
* 2. 通过时间戳计算当前请求落在哪个格子
* 3. 滑动时清空过期格子的计数
*/
public class SlidingWindowRateLimiter {
/** 窗口大小(毫秒) */
private final long windowSizeMs;
/** 格子数量 */
private final int gridCount;
/** 每个格子的时长(毫秒) */
private final long gridDurationMs;
/** 限流阈值 */
private final int threshold;
/** 环形数组,存储每个格子的请求数 */
private final AtomicInteger[] grids;
/** 每个格子最后更新的时间戳 */
private final long[] gridTimestamps;
/** 保护数组更新的锁 */
private final ReentrantLock lock = new ReentrantLock();
public SlidingWindowRateLimiter(long windowSizeMs, int gridCount, int threshold) {
this.windowSizeMs = windowSizeMs;
this.gridCount = gridCount;
this.gridDurationMs = windowSizeMs / gridCount;
this.threshold = threshold;
this.grids = new AtomicInteger[gridCount];
this.gridTimestamps = new long[gridCount];
for (int i = 0; i < gridCount; i++) {
grids[i] = new AtomicInteger(0);
gridTimestamps[i] = -1;
}
}
/**
* 尝试获取一个请求配额
* @return true-允许通过,false-被限流
*/
public boolean tryAcquire() {
long now = System.currentTimeMillis();
// 计算当前时间落在哪个格子
int currentGridIndex = (int) ((now / gridDurationMs) % gridCount);
lock.lock();
try {
// 检查当前格子是否过期(不是本次窗口的)
long gridStartTime = (now / gridDurationMs) * gridDurationMs;
if (gridTimestamps[currentGridIndex] != gridStartTime) {
// 格子过期,重置计数
grids[currentGridIndex].set(0);
gridTimestamps[currentGridIndex] = gridStartTime;
}
// 计算当前窗口内的总请求数
int total = calculateCurrentWindowCount(now);
if (total < threshold) {
grids[currentGridIndex].incrementAndGet();
return true;
}
return false;
} finally {
lock.unlock();
}
}
/**
* 计算当前滑动窗口内的请求总数
*/
private int calculateCurrentWindowCount(long now) {
int total = 0;
long windowStart = now - windowSizeMs;
for (int i = 0; i < gridCount; i++) {
if (gridTimestamps[i] > windowStart) {
total += grids[i].get();
}
}
return total;
}
/**
* 获取当前窗口的实时QPS(用于监控)
*/
public int getCurrentQps() {
lock.lock();
try {
return calculateCurrentWindowCount(System.currentTimeMillis());
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return "SlidingWindowRateLimiter{" +
"windowSizeMs=" + windowSizeMs +
", gridCount=" + gridCount +
", threshold=" + threshold +
", currentQps=" + getCurrentQps() +
'}';
}
// ==================== 测试代码 ====================
public static void main(String[] args) throws InterruptedException {
// 1秒窗口,10个格子,阈值100
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(1000, 10, 100);
System.out.println("=== 测试1:正常请求 ===");
int pass = 0, reject = 0;
for (int i = 0; i < 120; i++) {
if (limiter.tryAcquire()) {
pass++;
} else {
reject++;
}
}
System.out.println("通过: " + pass + ", 拒绝: " + reject);
// 预期:通过100,拒绝20
System.out.println("\n=== 测试2:等待窗口滑动后再次请求 ===");
Thread.sleep(200); // 等待200ms
pass = 0; reject = 0;
for (int i = 0; i < 50; i++) {
if (limiter.tryAcquire()) {
pass++;
} else {
reject++;
}
}
System.out.println("通过: " + pass + ", 拒绝: " + reject);
System.out.println("\n=== 测试3:模拟突发流量 ===");
SlidingWindowRateLimiter limiter2 = new SlidingWindowRateLimiter(1000, 10, 10);
// 快速发送20个请求
pass = 0; reject = 0;
for (int i = 0; i < 20; i++) {
if (limiter2.tryAcquire()) {
pass++;
} else {
reject++;
}
}
System.out.println("通过: " + pass + ", 拒绝: " + reject);
// 预期:通过10,拒绝10
System.out.println("\n=== 运行结果 ===");
System.out.println("本地内存版滑动窗口限流器测试完成!");
}
}
运行结果:
=== 测试1:正常请求 ===
通过: 100, 拒绝: 20
=== 测试2:等待窗口滑动后再次请求 ===
通过: 50, 拒绝: 0
=== 测试3:模拟突发流量 ===
通过: 10, 拒绝: 10
=== 运行结果 ===
本地内存版滑动窗口限流器测试完成!
4.2 基于Redis ZSet的分布式实现
分布式场景下,多个实例需要共享限流状态,Redis ZSet是经典方案。
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
/**
* 基于Redis ZSet的分布式滑动窗口限流器
*
* 核心原理:
* 1. 用ZSet的score存储请求时间戳
* 2. 每次请求时,先删除窗口外的旧记录(ZREMRANGEBYSCORE)
* 3. 统计当前窗口内的记录数(ZCARD)
* 4. 如果未超限,添加当前请求记录(ZADD)
*
* 踩坑提醒:这个实现需要Redis 2.6+支持lua脚本才能保证原子性!
* 下面提供的是Pipeline版本,生产环境建议用lua脚本版本。
*/
public class RedisSlidingWindowRateLimiter {
private final JedisPool jedisPool;
private final String keyPrefix;
private final long windowSizeMs;
private final int threshold;
public RedisSlidingWindowRateLimiter(String redisHost, int redisPort,
String keyPrefix, long windowSizeMs, int threshold) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(20);
this.jedisPool = new JedisPool(config, redisHost, redisPort);
this.keyPrefix = keyPrefix;
this.windowSizeMs = windowSizeMs;
this.threshold = threshold;
}
/**
* 尝试获取配额(Pipeline版本,非原子性,适合容忍少量误差的场景)
*/
public boolean tryAcquire(String resource) {
String key = keyPrefix + ":" + resource;
long now = System.currentTimeMillis();
long windowStart = now - windowSizeMs;
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
// 1. 删除窗口外的旧记录
pipeline.zremrangeByScore(key, 0, windowStart);
// 2. 统计当前窗口内的记录数
Response<Long> countResponse = pipeline.zcard(key);
// 3. 执行前两条命令
pipeline.sync();
long currentCount = countResponse.get();
if (currentCount < threshold) {
// 4. 添加当前请求记录
jedis.zadd(key, now, now + "_" + Thread.currentThread().getId());
// 5. 设置过期时间,防止key残留
jedis.pexpire(key, windowSizeMs);
return true;
}
return false;
}
}
/**
* 原子性版本:使用Lua脚本
* 生产环境强烈推荐用这个!
*/
public boolean tryAcquireAtomic(String resource) {
String key = keyPrefix + ":" + resource;
long now = System.currentTimeMillis();
long windowStart = now - windowSizeMs;
// Lua脚本保证整个操作的原子性
String luaScript =
"local key = KEYS[1] " +
"local now = tonumber(ARGV[1]) " +
"local windowStart = tonumber(ARGV[2]) " +
"local threshold = tonumber(ARGV[3]) " +
"local expire = tonumber(ARGV[4]) " +
"-- 删除窗口外的记录 " +
"redis.call('ZREMRANGEBYSCORE', key, 0, windowStart) " +
"-- 获取当前窗口内记录数 " +
"local count = redis.call('ZCARD', key) " +
"if count < threshold then " +
" redis.call('ZADD', key, now, ARGV[5]) " +
" redis.call('PEXPIRE', key, expire) " +
" return 1 " +
"else " +
" return 0 " +
"end";
try (Jedis jedis = jedisPool.getResource()) {
Object result = jedis.eval(luaScript,
Arrays.asList(key),
Arrays.asList(
String.valueOf(now),
String.valueOf(windowStart),
String.valueOf(threshold),
String.valueOf(windowSizeMs),
now + "_" + Thread.currentThread().getId() + "_" + Math.random()
)
);
return Long.valueOf(1).equals(result);
}
}
/**
* 获取当前QPS(用于监控)
*/
public long getCurrentQps(String resource) {
String key = keyPrefix + ":" + resource;
long windowStart = System.currentTimeMillis() - windowSizeMs;
try (Jedis jedis = jedisPool.getResource()) {
jedis.zremrangeByScore(key, 0, windowStart);
return jedis.zcard(key);
}
}
public void close() {
if (jedisPool != null && !jedisPool.isClosed()) {
jedisPool.close();
}
}
// ==================== 测试代码 ====================
public static void main(String[] args) {
// 注意:需要本地启动Redis,端口6379
RedisSlidingWindowRateLimiter limiter = new RedisSlidingWindowRateLimiter(
"localhost", 6379, "rate_limit", 1000, 10
);
System.out.println("=== Redis分布式滑动窗口限流测试 ===");
String resource = "api:/order/create";
int pass = 0, reject = 0;
for (int i = 0; i < 15; i++) {
if (limiter.tryAcquireAtomic(resource)) {
pass++;
System.out.println("请求 " + (i+1) + " -> 通过");
} else {
reject++;
System.out.println("请求 " + (i+1) + " -> 被拒绝");
}
}
System.out.println("\n总计:通过=" + pass + ", 拒绝=" + reject);
System.out.println("当前QPS:" + limiter.getCurrentQps(resource));
limiter.close();
}
}
踩坑提醒:Redis ZSet实现有个坑------如果请求量极大,ZSet会不断膨胀,即使设置了过期时间,在过期前内存占用也会很高。建议配合
ZREMRANGEBYSCORE及时清理,或者考虑用Redis Cell模块。
五、Sentinel中的滑动窗口实现
5.1 LeapArray设计
Sentinel的滑动窗口实现叫LeapArray,设计非常精巧。
核心结构:
java
// 简化的LeapArray核心结构
public abstract class LeapArray<T> {
// 窗口时长(默认1秒)
protected int windowLengthInMs;
// 采样数量(默认2个,即每个窗口500ms)
protected int sampleCount;
// 窗口数组(环形)
protected final AtomicReferenceArray<WindowWrap<T>> array;
// 计算当前时间对应的窗口索引
private int calculateTimeIdx(long timeMillis) {
long timeId = timeMillis / windowLengthInMs;
return (int) (timeId % array.length());
}
// 计算窗口开始时间
protected long calculateWindowStart(long timeMillis) {
return timeMillis - timeMillis % windowLengthInMs;
}
}
Sentinel默认把1秒分成2个500ms的窗口,而不是更细的粒度。这是精度和性能的平衡:
| 参数 | 默认值 | 说明 |
|---|---|---|
| sampleCount | 2 | 1秒分2个窗口 |
| windowLengthInMs | 500 | 每个窗口500ms |
| intervalInMs | 1000 | 统计周期1秒 |
5.2 Sentinel的实际应用
Sentinel用滑动窗口统计什么?
- QPS统计:每秒请求数,用于限流判断
- RT统计:平均响应时间,用于熔断判断
- 异常比例:异常请求占比,用于熔断判断
- 并发线程数:当前处理的线程数,用于线程隔离
java
// Sentinel中滑动窗口的使用示例
Entry entry = null;
try {
entry = SphU.entry("orderCreate");
// 业务逻辑
} catch (BlockException e) {
// 被限流或熔断
return "系统繁忙,请稍后再试";
} finally {
if (entry != null) {
entry.exit();
}
}
Sentinel的StatisticSlot会在entry.exit()时更新滑动窗口的统计数据,后续的FlowSlot(限流)和DegradeSlot(熔断)基于这些统计数据做决策。
六、问题与解答
Q1: 滑动窗口的格子数怎么选?
格子数越多,精度越高,但内存和CPU开销也越大。
经验法则:
- 普通API限流:1秒分10个格子(100ms粒度)
- 秒杀等高敏感场景:1秒分100个格子(10ms粒度)
- 日志限流:1秒分2个格子(500ms粒度)就够了
Sentinel默认2个格子,因为它还要兼顾RT、异常数等多种统计,不能搞太细。
Q2: 滑动窗口和令牌桶哪个更好?
没有绝对的好坏,看场景:
- 滑动窗口胜在:精确控制时间窗口内的请求数,适合"1秒内最多N次"这种严格限制
- 令牌桶胜在:允许合理的突发流量,适合"平均速率不超过N"的柔性限制
电商下单接口用滑动窗口,视频播放带宽用令牌桶。
Q3: Redis ZSet实现有什么缺点?
三个主要缺点:
- 内存开销大:每个请求都要在ZSet里存一条记录,QPS高的时候内存爆炸
- 清理开销 :每次请求都要
ZREMRANGEBYSCORE清理过期数据,高并发时Redis压力大 - 精度问题:Pipeline版本非原子性,极端情况下可能超卖
优化方案:
- 用Redis Cell模块(
CL.THROTTLE命令),内部用更紧凑的数据结构 - 或者用Redis Stream + 定时任务聚合统计
七、面试高频考点
考点1:滑动窗口限流原理是什么?
答案: 滑动窗口将时间窗口划分为多个小格子,每个格子独立计数。每次请求时,只统计当前时间往前推一个窗口周期内的所有格子请求数之和,如果总和超过阈值则拒绝请求。相比固定窗口,它解决了边界突发问题,因为窗口是连续滑动的,不会出现两个相邻窗口边界处请求翻倍的情况。
考点2:三种限流算法(固定窗口、滑动窗口、令牌桶)有什么区别?
答案:
- 固定窗口:按固定时间周期计数,实现简单但有边界突发问题
- 滑动窗口:将窗口切分为多个格子,平滑统计,精度高但实现复杂
- 令牌桶:以固定速率生成令牌,桶满则丢弃,允许突发流量,适合带宽控制
考点3:Sentinel中的LeapArray是怎么实现的?
答案: Sentinel使用LeapArray实现滑动窗口统计。核心是一个环形数组,每个元素是一个时间窗口。默认1秒分为2个500ms的窗口。通过calculateTimeIdx计算当前时间对应的窗口索引,通过CAS操作保证线程安全。LeapArray统计的数据包括QPS、RT、异常数、并发线程数等,供限流和熔断规则使用。
考点4:如何用Redis实现分布式滑动窗口限流?
答案: 使用Redis ZSet,以请求时间戳作为score,请求唯一标识作为member。每次请求时:
- 用
ZREMRANGEBYSCORE删除窗口外的过期记录 - 用
ZCARD统计当前窗口内的记录数 - 如果未超限,用
ZADD添加当前请求记录 - 用
PEXPIRE设置key过期时间
生产环境必须用Lua脚本保证原子性,避免竞态条件。
考点5:限流算法选择要考虑哪些因素?
答案:
- 精度要求:固定窗口<滑动窗口≈令牌桶
- 突发容忍:令牌桶>滑动窗口>固定窗口
- 实现复杂度:固定窗口<令牌桶<滑动窗口
- 分布式支持:固定窗口最容易,滑动窗口和令牌桶都需要外部存储
- 内存占用:固定窗口最低,滑动窗口与格子数成正比
八、模拟面试官提问
场景题1:设计一个接口限流系统
面试官: 假设你要给公司的API网关设计一套限流系统,支持按用户、按接口、按IP多维度限流,你会怎么设计?
参考答案:
- 分层限流:网关层做粗粒度限流(IP、全局QPS),业务层做细粒度限流(用户ID、接口级别)
- 存储选型:本地内存做第一层过滤(Guava RateLimiter或自研滑动窗口),Redis做分布式共享状态
- 维度设计 :限流key设计为
rate_limit:{维度}:{标识}:{接口},如rate_limit:ip:192.168.1.1:/api/order - 动态配置:接入配置中心(Nacos/Apollo),支持限流阈值实时调整
- 降级策略:Redis不可用时,降级为本地限流,保证服务可用性
场景题2:秒杀场景限流
面试官: 秒杀活动,预计每秒10万人点击,但只有100个库存,怎么设计限流?
参考答案:
- 多层过滤 :
- CDN层:静态资源缓存,过滤大部分无效请求
- Nginx层:IP限流,单IP每秒最多10次
- 网关层:用户ID限流,单用户每秒最多1次
- 业务层:库存预扣,用Redis原子减库存,减成功才放行
- 滑动窗口设置:用户维度1秒1个请求,全局维度根据库存和持续时间计算
- 排队机制:超过限流的请求进入MQ排队,而不是直接拒绝,提升用户体验
- 热点隔离:对秒杀商品ID做哈希分散,避免单点热点
场景题3:分布式限流的一致性
面试官: 10个服务实例,每个实例本地限流100QPS,理论上全局是1000QPS。但如果负载不均,某个实例承担了50%流量,怎么办?
参考答案:
- 问题本质:本地限流无法感知全局流量分布
- 方案一:Redis集中式限流,所有实例共享计数器,但引入网络开销和Redis单点压力
- 方案二:令牌桶+本地缓存,定期从中心节点同步令牌,平衡一致性和性能
- 方案三(推荐):自适应限流,每个实例根据CPU、内存、RT等指标动态调整本地阈值,配合全局熔断
- Sentinel做法:支持集群流控,通过Token Server集中管理,但有一定延迟
场景题4:滑动窗口内存优化
面试官: 滑动窗口需要维护每个格子的计数,如果服务有10万个限流key,每个key 10个格子,内存占用会很大,怎么优化?
参考答案:
- 惰性创建:不是预先分配所有key的窗口,而是请求到来时才创建对应的滑动窗口
- 过期清理:用WeakReference或定时任务清理长时间无请求的key的窗口数据
- 内存压缩 :如果格子数固定且较少,可以用
long[]数组代替对象数组 - 分层存储:热key放在本地内存,冷key降级到Redis或Caffeine缓存
- Sentinel方案:LeapArray的窗口数组是共享的,不同资源复用同一套时间窗口结构
场景题5:限流与熔断结合
面试官: 限流和熔断有什么区别?实际项目中怎么配合用?
参考答案:
- 区别 :
- 限流:主动防御,防止流量过大压垮系统,基于请求速率
- 熔断:被动保护,当错误率过高时切断请求,基于错误统计
- 配合方式 :
- 流量正常时:限流控制并发,保证系统稳定运行
- 流量突增时:限流拒绝超额请求,返回降级结果
- 下游故障时:熔断触发,直接返回降级结果,不再调用下游
- 下游恢复时:熔断半开,逐步放行请求探测
- Sentinel实践 :
FlowSlot做限流,DegradeSlot做熔断,SystemSlot做系统保护,三者按优先级依次执行
九、互动话题
你在生产环境用过哪种限流方案?有没有遇到过固定窗口的边界突发问题?欢迎在评论区分享你的踩坑经历!