![图片[1]-Sentinel 高级限流:黑白名单授权与热点参数限流完全指南 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/cover-2106.png)
本文导读
在前面的文章中,我们系统学习了 Sentinel 的流量控制、熔断降级和系统自适应限流。这些都是比较通用的限流手段。但在实际业务场景中,我们经常需要更精细化的限流能力:
- 如何只允许特定的服务调用方访问?
- 如何拦截某些恶意 IP 的请求?
- 如何对热门商品的下单请求单独限流?
- 如何针对接口的不同参数值实现差异化限流?
这就需要用到 Sentinel 的两大高级限流功能:
- 黑白名单限流:基于调用来源的授权控制
- 热点参数限流:基于请求参数的精细化限流
本文将从原理、配置、源码实现等多个维度,深入剖析这两大功能的实现机制,帮助你掌握 Sentinel 的高级限流技巧。
一、黑白名单限流:基于调用来源的授权控制
1.1 什么是黑白名单限流
黑白名单过滤是软件开发中最经典的过滤规则之一,应用场景非常广泛:
- IP 黑白名单:用于接口安全防护
- 手机号黑白名单:用于短信、来电的防骚扰
- 服务黑白名单:用于微服务间的调用授权
Sentinel 中的黑白名单限流,本质上是一种授权机制。它将调用来源简单地分为”有权限”和”无权限”两种情况:
- 黑名单模式:来源在黑名单中 → 拒绝请求
- 白名单模式:来源在白名单中 → 放行请求;否则拒绝
Sentinel 不支持一个规则同时配置黑名单和白名单,因此不存在优先级冲突的问题。
Sentinel 在命名上使用的是 Authority(授权)而非 BlackWhiteList,这也体现了它的定位——黑白名单本质上是一种授权限流。
1.2 核心类一览
在深入源码之前,我们先认识几个关键类:
| 类名 | 作用 |
| AuthoritySlot | 实现黑白名单授权功能的切入点(ProcessorSlot) |
| AuthorityRule | 授权规则类,定义黑白名单规则 |
| AuthorityRuleChecker | 授权检测类,实现具体的判断逻辑 |
| AuthorityRuleManager | 授权规则管理者,提供规则加载 API |
| AuthorityException | 授权检测异常,继承自 BlockException |
1.3 授权规则配置
AuthorityRule 是 Sentinel 中最容易理解的规则之一,配置项非常简洁:
public class AuthorityRule extends AbstractRule {
private int strategy = RuleConstant.AUTHORITY_WHITE;
}
从父类继承和自身定义的核心配置项:
| 配置项 | 说明 |
| resource | 资源名称,从父类继承 |
| limitApp | 限制的来源名称,支持多个,用英文逗号分隔 |
| strategy | 限流策略:白名单(AUTHORITY_WHITE)或黑名单(AUTHORITY_BLACK) |
策略说明:
- 当
strategy = AUTHORITY_WHITE时,limitApp即为白名单 - 当
strategy = AUTHORITY_BLACK时,limitApp即为黑名单
使用示例:
AuthorityRule rule = new AuthorityRule();
// 资源名称
rule.setResource("GET:/hello");
// 白名单策略
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
// 只允许 serviceA 和 serviceC 访问
rule.setLimitApp("serviceA,serviceC");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
上述规则表示:资源”GET:/hello”只允许来自 serviceA 和 serviceC 的请求访问。
1.4 AuthoritySlot 的执行时机
在使用默认的 SlotChainBuilder 情况下,AuthoritySlot 被放在 SystemSlot、FlowSlot、DegradeSlot 的前面,优先级最高。
为什么这么设计?主要有两个原因:
- 授权限流不需要统计指标数据,可以快速判断
- 性能考虑:未授权的请求没必要继续判断熔断、系统负载、QPS 等,直接拒绝即可
这和用户授权功能的设计思路是一样的:未登录的用户不需要判断是否有权限访问某个具体资源,直接拦截。
AuthoritySlot 的实现源码如下:
public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkBlackWhiteAuthority(resourceWrapper, context);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
// (1) 获取所有授权规则
Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
if (authorityRules == null) {
return;
}
// (2) 获取当前资源的授权规则
Set<AuthorityRule> rules = authorityRules.get(resource.getName());
if (rules == null) {
return;
}
// (3) 遍历规则逐一检查
for (AuthorityRule rule : rules) {
if (!AuthorityRuleChecker.passCheck(rule, context)) {
throw new AuthorityException(context.getOrigin(), rule);
}
}
}
}
整个流程非常清晰:
- 从
AuthorityRuleManager获取当前配置的所有授权规则 - 根据资源名称获取该资源对应的授权规则
- 遍历规则,调用
AuthorityRuleChecker#passCheck判断是否拒绝,拒绝则抛出AuthorityException
1.5 AuthorityRuleChecker 核心逻辑
AuthorityRuleChecker 负责实现黑白名单的具体过滤逻辑,其 passCheck 方法是核心:
static boolean passCheck(AuthorityRule rule, Context context) {
// 获取调用来源
String requester = context.getOrigin();
// 来源为空或规则未配置 limitApp,直接放行
if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
return true;
}
// 第一步:使用 indexOf 快速过滤
int pos = rule.getLimitApp().indexOf(requester);
boolean contain = pos > -1;
// 第二步:精确匹配
if (contain) {
boolean exactlyMatch = false;
String[] appArray = rule.getLimitApp().split(",");
for (String app : appArray) {
if (requester.equals(app)) {
exactlyMatch = true;
break;
}
}
contain = exactlyMatch;
}
// 根据策略判断是否放行
int strategy = rule.getStrategy();
// 黑名单:来源在名单中则拒绝
if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
return false;
}
// 白名单:来源不在名单中则拒绝
if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
return false;
}
return true;
}
这段代码虽然简单,但有几个值得注意的设计细节:
1. 前置条件检查
首先从 Context 获取调用来源名称。只有调用来源不为空,且规则配置了 limitApp 的情况下,才会走过滤逻辑。
这意味着:要使用黑白名单限流,前提是每个服务消费端在发起请求时必须携带自身服务名称。这一点由 Sentinel 的各主流框架适配器保证。
2. 性能优化:先快速过滤,再精确匹配
Sentinel 并没有直接 split 然后遍历匹配,而是先用 indexOf 做一次快速过滤,只有匹配上了才做精确匹配。
为什么要这么做?
indexOf的时间复杂度较低,能快速排除大多数不匹配的情况- 只有在可能匹配的情况下,才执行 split 和遍历的重操作
- 这是一种典型的”快速失败”性能优化思路
3. 策略判断逻辑
- 黑名单策略:来源在名单中 → 返回 false(拒绝)
- 白名单策略:来源不在名单中 → 返回 false(拒绝)
- 其他情况都返回 true(放行)
二、热点参数限流:基于参数的精细化限流
2.1 为什么需要热点参数限流
前面介绍的限流都是针对资源整体的,但实际业务中经常需要更细粒度的限流。
举个典型的电商场景:
- 都是调用下单接口,但购买的商品不同
- 主播带货的热门商品下单流量巨大
- 普通商品的下单量很小
- 同时,热门商品的库存有限,不可能每个下单请求都能成功
如果能实现根据商品 ID 单独限流:
- 将热门商品的流量控制在库存总量左右
- 普通商品不受影响
- 再配合 QPS 限流做兜底
这种有针对性的限流,能将接口的有效流量最大化。
这就是热点参数限流:根据方法调用传递的参数实现限流,针对访问频繁的参数值单独限流。
热点参数限流不在 Sentinel 的 core 模块中实现,而是在扩展模块
sentinel-parameter-flow-control中。另外,Sentinel 的 API Gateway 网关限流也是基于参数限流实现的。
2.2 基于滑动窗口的热点参数指标统计
热点参数限流使用的指标数据不是 core 模块中的统计数据,而是重新实现了一套指标统计功能,底层依旧是基于滑动窗口。
核心类:
| 类名 | 作用 |
| ParamMapBucket | 实现参数指标统计的 Bucket,统计每个参数不同取值的通过数、被限流数 |
| HotParameterLeapArray | 实现滑动窗口,持有 WindowWrap 数组,WindowWrap 包装 ParamMapBucket |
与 core 模块的区别:
core 模块的 MetricBucket 只统计每个指标的数值,更像是 Redis 中的 String 结构;而 ParamMapBucket 需要统计每个指标、参数的每种取值的数值,更像 Redis 中的 Hash 结构。
ParamMapBucket 的源码如下:
public class ParamMapBucket {
// 数组元素类型为 CacheMap<Object, AtomicInteger>
private final CacheMap<Object, AtomicInteger>[] data;
public ParamMapBucket() {
this(DEFAULT_MAX_CAPACITY);
}
public ParamMapBucket(int capacity) {
RollingParamEvent[] events = RollingParamEvent.values();
// 根据需要统计的指标创建数组
this.data = new CacheMap[events.length];
// RollingParamEvent 可取值为 REQUEST_PASSED、REQUEST_BLOCKED
for (RollingParamEvent event : events) {
data[event.ordinal()] = new ConcurrentLinkedHashMapWrapper<Object, AtomicInteger>(capacity);
}
}
}
字段说明:
- data:数组,下标 0 存储请求通过数,下标 1 存储请求被拒绝数
- CacheMap:key 为参数取值(如商品 ID),value 为指标数值
HotParameterLeapArray 继承自 LeapArray,实现了滑动窗口。ParamMapBucket 不存储窗口时间信息,窗口时间信息仍由 WindowWrap 存储。
从这里我们也能体会到 Sentinel 将滑动窗口抽象为
LeapArray的好处:为扩展实现自定义指标数据的滑动窗口提供了便利。
HotParameterLeapArray 提供的几个核心 API:
public class HotParameterLeapArray extends LeapArray<ParamMapBucket> {
public void addValue(RollingParamEvent event, int count, Object value) {
// 添加参数的指标数值
}
public Map<Object, Double> getTopValues(RollingParamEvent event, int number) {
// 获取热点参数的排名,如 QPS 排名前 10 的参数取值
}
public long getRollingSum(RollingParamEvent event, Object value) {
// 计算某个指标、某个参数取值的总请求数
}
public double getRollingAvg(RollingParamEvent event, Object value) {
// 获取某个指标、某个参数取值的平均 QPS
}
}
注意内存占用问题:如果是分钟级的滑动窗口,一分钟内参数的取值越多,占用的内存就越多。在使用热点参数限流时,一定要考虑参数取值的可能性数量。
2.3 参数限流中的 Node 体系
热点参数限流也有自己的 Node 体系,两个核心类:
| 类名 | 类比 core 模块 | 作用 |
| ParameterMetric | ClusterNode | 实现参数级别的统计功能 |
| ParameterMetricStorage | EntranceNode | 管理和存储每个资源对应的 ParameterMetric |
ParameterMetric 有三个关键字段:
public class ParameterMetric {
private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTimeCounters = new HashMap<>();
private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTokenCounter = new HashMap<>();
private final Map<Integer, CacheMap<Object, AtomicInteger>> threadCountMap = new HashMap<>();
}
各字段说明:
| 字段 | 作用 |
| ruleTimeCounters | 用于匀速流量控制效果,key 为参数限流规则,value 为参数不同取值对应的上次生产令牌时间 |
| ruleTokenCounter | 用于匀速流量控制效果,key 为参数限流规则,value 为参数不同取值对应的当前令牌桶令牌数 |
| threadCountMap | key 为参数索引,value 为参数不同取值对应的当前并行占用线程总数 |
ParameterMetricStorage 使用 ConcurrentHashMap 缓存每个资源对应的 ParameterMetric,只会为配置了参数限流规则的资源创建。
public final class ParameterMetricStorage {
private static final Map<String, ParameterMetric> metricsMap = new ConcurrentHashMap<>();
private static final Object LOCK = new Object();
public static void initParamMetricsFor(ResourceWrapper resourceWrapper, ParamFlowRule rule) {
if (resourceWrapper == null || resourceWrapper.getName() == null) {
return;
}
String resourceName = resourceWrapper.getName();
ParameterMetric metric;
// 双重检测,线程安全地创建 ParameterMetric
if ((metric = metricsMap.get(resourceName)) == null) {
synchronized (LOCK) {
if ((metric = metricsMap.get(resourceName)) == null) {
metric = new ParameterMetric();
metricsMap.put(resourceWrapper.getName(), metric);
}
}
}
// 初始化 ParameterMetric
metric.initialize(rule);
}
}
initParamMetricsFor 方法由 ParamFlowSlot 在资源被访问时调用,且只有配置了参数限流规则的资源才会被调用。
2.4 热点参数限流的入口:ParamFlowSlot
sentinel-parameter-flow-control 模块通过 Java SPI 注册自定义的 SlotChainBuilder,即 HotParamSlotChainBuilder,将 ParamFlowSlot 放置在 StatisticSlot 的后面。
ParamFlowSlot 就是实现热点参数限流功能的切入点:
public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
fireEntry(context, resourceWrapper, node, count, prioritized, args);
return;
}
checkFlow(resourceWrapper, count, args);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
参数是怎么传递过来的呢?答案是 ProcessorSlot#entry 方法的最后一个参数 args,通过 SphU#entry 方法一层层往下传递。
使用示例:
@GetMapping("/hello")
public String apiHello(String name) throws BlockException {
ContextUtil.enter("my_context");
Entry entry = null;
try {
// name 参数通过 entry 方法传递
entry = SphU.entry("GET:/hello", EntryType.IN, 1, name);
doBusiness();
return "Hello!";
} catch (Exception e) {
if (!(e instanceof BlockException)) {
Tracer.trace(e);
}
throw e;
} finally {
if (entry != null) {
entry.exit(1);
}
ContextUtil.exit();
}
}
当调用到 ParamFlowSlot#entry 时,ParamFlowSlot 调用 checkFlow 方法判断是否需要限流:
void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
// (1) 参数为空或无规则,直接返回
if (args == null) {
return;
}
if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
return;
}
List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());
// (2) 遍历规则逐一检查
for (ParamFlowRule rule : rules) {
applyRealParamIdx(rule, args.length);
// 初始化参数指标统计
ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {
String triggeredParam = "";
if (args.length > rule.getParamIdx()) {
Object value = args[rule.getParamIdx()];
triggeredParam = String.valueOf(value);
}
throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
}
}
}
流程很清晰:
- 检查参数和规则,无规则则放行
- 遍历规则,初始化
ParameterMetric - 调用
ParamFlowChecker#passCheck判断是否放行 - 拒绝时抛出
ParamFlowException,携带触发限流的参数值
2.5 参数限流规则配置
在深入判断逻辑之前,我们先了解 ParamFlowRule 的配置项:
public class ParamFlowRule extends AbstractRule {
private int grade = RuleConstant.FLOW_GRADE_QPS;
private double count;
private Integer paramIdx;
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
private int maxQueueingTimeMs = 0;
private long durationInSec = 1;
private int burstCount = 0;
}
各配置项详解:
| 配置项 | 说明 |
| grade | 阈值类型,同 FlowRule,支持 QPS 和线程数 |
| count | 限流阈值,同 FlowRule |
| paramIdx | 参数索引,从 0 开始,指定对哪个参数限流 |
| controlBehavior | 流控效果,同 FlowRule,但只支持快速失败和匀速排队 |
| maxQueueingTimeMs | 匀速排队的最大等待时间,同 FlowRule |
| durationInSec | 统计窗口时间大小,单位秒 |
| burstCount | 突发流量容许数,即令牌桶的额外容量 |
使用示例:
对资源”GET:/hello”的 name 参数限流,当 name 为”jackson”时,QPS 阈值为 5:
ParamFlowRule rule = new ParamFlowRule();
rule.setResource("GET:/hello");
rule.setParamIdx(0); // 索引 0 对应 name 参数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(5); // QPS 阈值 5
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
2.6 ParamFlowChecker 核心判断逻辑
ParamFlowChecker#passCheck 是参数限流判断的入口:
public static boolean passCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
Object... args) {
if (args == null) {
return true;
}
// 参数索引不合法,放行
int paramIdx = rule.getParamIdx();
if (args.length <= paramIdx) {
return true;
}
// 参数值为空,放行
Object value = args[paramIdx];
if (value == null) {
return true;
}
// 集群限流
if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
return passClusterCheck(resourceWrapper, rule, count, value);
}
// 单机限流
return passLocalCheck(resourceWrapper, rule, count, value);
}
三种直接放行的情况:
- 参数为空
- 参数索引超出范围
- 参数值为 null
然后分集群限流和单机限流两种情况处理。我们重点看单机限流。
passLocalCheck 方法处理不同类型的参数值:
private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
Object value) {
try {
// 集合类型:遍历每个元素
if (Collection.class.isAssignableFrom(value.getClass())) {
for (Object param : ((Collection)value)) {
if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
return false;
}
}
}
// 数组类型:遍历每个元素
else if (value.getClass().isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
Object param = Array.get(value, i);
if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
return false;
}
}
}
// 单值类型:直接检查
else {
return passSingleValueCheck(resourceWrapper, rule, count, value);
}
} catch (Throwable e) {
}
return true;
}
参数可能是集合、数组或单值,Sentinel 对三种情况都做了处理。核心逻辑都在 passSingleValueCheck 中。
passSingleValueCheck 根据阈值类型分流控效果:
static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule,
int acquireCount, Object value) {
// QPS 模式
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) {
return passThrottleLocalCheck(resourceWrapper, rule, acquireCount, value);
} else {
return passDefaultLocalCheck(resourceWrapper, rule, acquireCount, value);
}
}
// 线程数模式
else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
if (exclusionItems.contains(value)) {
int itemThreshold = rule.getParsedHotItems().get(value);
return ++threadCount <= itemThreshold;
}
long threshold = (long)rule.getCount();
return ++threadCount <= threshold;
}
return true;
}
线程数模式的逻辑很简单:获取当前参数值对应的并行线程数,+1 后如果超过阈值则限流。
你可能好奇:线程数是在哪里自增和自减的呢?
答案是:由
ParamFlowStatisticEntryCallback和ParamFlowStatisticExitCallback两个 Callback 实现,分别在StatisticSlot的 entry 和 exit 方法中被回调。
下面我们重点分析 QPS 模式下的两种流控效果。
2.7 快速失败:基于令牌桶算法
快速失败基于令牌桶算法实现。passDefaultLocalCheck 方法控制每个时间窗口只生产一次令牌,将令牌放入令牌桶,每个请求从桶中取令牌,令牌足够则放行,不足则直接拒绝。
ParameterMetric 的 tokenCounters 用作令牌桶,timeCounters 存储最近一次生产令牌的时间。
源码如下:
static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
Object value) {
// (1) 获取 ParameterMetric 和令牌桶、时间记录器
ParameterMetric metric = getParameterMetric(resourceWrapper);
CacheMap<Object, AtomicLong> tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule);
CacheMap<Object, AtomicLong> timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule);
if (tokenCounters == null || timeCounters == null) {
return true;
}
// (2) 计算限流阈值(支持热点参数单独配置)
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
long tokenCount = (long)rule.getCount();
if (exclusionItems.contains(value)) {
tokenCount = rule.getParsedHotItems().get(value);
}
if (tokenCount == 0) {
return false;
}
// (3) 计算最大令牌数(含突发容量)
long maxCount = tokenCount + rule.getBurstCount();
if (acquireCount > maxCount) {
return false;
}
while (true) {
// (4) 首次访问:初始化令牌桶
long currentTime = TimeUtil.currentTimeMillis();
AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime));
if (lastAddTokenTime == null) {
tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
return true;
}
// (5) 计算与上次生产令牌的时间间隔
long passTime = currentTime - lastAddTokenTime.get();
if (passTime > rule.getDurationInSec() * 1000) {
// (6) 超过一个窗口:重新计算令牌数
AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
if (oldQps == null) {
lastAddTokenTime.set(currentTime);
return true;
} else {
long restQps = oldQps.get();
// 计算需要新增的令牌数
long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000);
// 新令牌数 = 剩余 + 新增(不超过 maxCount),然后扣除本次请求
long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
: (restQps + toAddCount - acquireCount);
if (newQps < 0) {
return false;
}
if (oldQps.compareAndSet(restQps, newQps)) {
lastAddTokenTime.set(currentTime);
return true;
}
Thread.yield();
}
} else {
// (7) 同一窗口内:直接从令牌桶取令牌
AtomicLong oldQps = tokenCounters.get(value);
if (oldQps != null) {
long oldQpsValue = oldQps.get();
if (oldQpsValue - acquireCount >= 0) {
// 令牌足够,扣减并放行
if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
return true;
}
} else {
// 令牌不足,拒绝
return false;
}
}
Thread.yield();
}
}
}
整个流程可以总结为:
- 获取参数统计对象和令牌桶
- 计算限流阈值(支持热点参数单独配置不同阈值)
- 首次访问时初始化令牌桶
- 超过一个窗口时间,重新计算令牌数并补充
- 同一窗口内,直接从令牌桶取令牌,足够则放行,不足则拒绝
2.8 匀速排队:虚拟队列机制
匀速排队的实现原理与 RateLimiterController 一样:让请求在虚拟队列中排队,控制请求通过的时间间隔。如果当前请求的排队等待时间超过 maxQueueingTimeMs,则拒绝。
passThrottleLocalCheck 方法源码:
static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule,
int acquireCount, Object value) {
// (1) 获取 ParameterMetric 和时间记录器
ParameterMetric metric = getParameterMetric(resourceWrapper);
CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
if (timeRecorderMap == null) {
return true;
}
// (2) 计算限流阈值
Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
long tokenCount = (long)rule.getCount();
if (exclusionItems.contains(value)) {
tokenCount = rule.getParsedHotItems().get(value);
}
if (tokenCount == 0) {
return false;
}
// (3) 计算请求通过的时间间隔(costTime)
long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
while (true) {
long currentTime = TimeUtil.currentTimeMillis();
// (4) 首次访问:直接放行
AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
if (timeRecorder == null) {
return true;
}
long lastPassTime = timeRecorder.get();
// 计算当前请求的期望通过时间
long expectedTime = lastPassTime + costTime;
// (5) 判断是否可以放行或需要排队
if (expectedTime <= currentTime
|| expectedTime - currentTime < rule.getMaxQueueingTimeMs()) {
AtomicLong lastPastTimeRef = timeRecorderMap.get(value);
if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) {
long waitTime = expectedTime - currentTime;
if (waitTime > 0) {
lastPastTimeRef.set(expectedTime);
try {
// 排队等待
TimeUnit.MILLISECONDS.sleep(waitTime);
} catch (InterruptedException e) {
RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
}
}
return true;
} else {
Thread.yield();
}
} else {
// 等待时间超上限,拒绝
return false;
}
}
}
核心逻辑:
- 计算时间间隔:根据阈值和窗口时间,计算每个请求通过的时间间隔
- 虚拟队列:用
timeRecorder记录队列尾部请求的期望通过时间 - 排队等待:当前请求的期望通过时间 = 尾部时间 + 间隔;如果等待时间在允许范围内,则 sleep 等待后放行
- 超时拒绝:如果等待时间超过
maxQueueingTimeMs,直接拒绝
举个例子:阈值 200 QPS,窗口 1 秒,那么 costTime = 5ms,即每 5ms 只允许通过一个请求。
总结与思考
本文详细介绍了 Sentinel 的两大高级限流功能:黑白名单限流和热点参数限流。下面做一个全面总结。
核心要点回顾
1. 黑白名单限流
| 特性 | 说明 |
| 本质 | 基于调用来源的授权控制 |
| 策略 | 白名单(只允许名单内)、黑名单(拒绝名单内) |
| 配置 | limitApp 支持多个,用逗号分隔 |
| 性能优化 | 先用 indexOf 快速过滤,再精确匹配 |
| 执行时机 | 在 Slot 链路最前面,优先判断 |
2. 热点参数限流
| 特性 | 说明 |
| 本质 | 基于请求参数值的精细化限流 |
| 模块 | sentinel-parameter-flow-control(扩展模块) |
| 统计 | 独立实现滑动窗口,基于 ParamMapBucket |
| 流控效果 | 支持快速失败和匀速排队 |
| 阈值类型 | 支持 QPS 和线程数 |
3. 热点参数限流的两种流控效果
- 快速失败:基于令牌桶,支持突发流量(burstCount)
- 匀速排队:基于虚拟队列,控制请求通过间隔,支持最大等待时间
实践建议
关于黑白名单限流:
- 确保调用来源传递:使用框架适配器时,确保 origin 被正确设置
- 白名单优先:安全场景下优先使用白名单,默认拒绝更安全
- 结合网关使用:在网关层配置 IP 黑白名单效果更好
关于热点参数限流:
- 评估参数取值数量:参数取值越多,内存占用越大,务必提前评估
- 线程数模式更省内存:线程数模式下,线程数为 0 时会移除对应 key,内存占用可控
- 合理设置窗口时间:durationInSec 不宜过大,否则内存占用高
- 热点参数单独配置阈值:利用 exclusionItems 为热点参数配置单独的阈值
- 注意参数类型:参数可以是单值、集合或数组,都会被遍历检查
内存占用的注意事项
热点参数限流对内存的影响与参数取值的可能性数量成正比。参数取值越多,占用内存越大,对性能影响也越大。
举例说明:
- 根据商品 ID 限流,如果有十万个商品,CacheMap 中就会有十万个 key-value
- 这些数据不会被自动移除,会随着进程运行持续增长
- 线程数模式不存在这个问题,因为线程数为 0 时会移除对应的 key
因此,在使用热点参数限流时,一定要充分考虑参数的取值范围,避免内存溢出风险。
黑白名单限流和热点参数限流是 Sentinel 的两大高级功能,掌握它们能够帮助你实现更精细化的流量控制。下一篇文章我们将探讨如何自定义 ProcessorSlot 实现开关降级,敬请期待。












请登录后查看评论内容