![图片[1]-Sentinel责任链模式与工作流程:一次请求的完整旅行 - 速优课-速优课](http://www.suyouke.com/wp-content/uploads/2026/08/doubao_img_2304x1728_20260803_094414-1024x768.png)
Sentinel责任链模式与工作流程:一次请求的完整旅行
前言
前面我们了解了Sentinel的核心概念和各种Node,今天我们来看看Sentinel的整体工作流程。
Sentinel的骨架是责任链模式——所有的ProcessorSlot按顺序串成一条链,请求依次经过每个Slot的处理。理解了这条链,就理解了Sentinel的核心骨架。
这篇文章,我们就从责任链模式讲起,再一步步拆解一次请求在Sentinel中的完整旅行。
一、什么是责任链模式
先简单复习一下责任链模式。
责任链模式,就是把多个处理器串成一条链,请求从链头进入,依次经过每个处理器,直到被处理完。每个处理器只负责自己那部分工作,处理完就传给下一个。
这个模式在很多框架里都有应用:
- Shiro:用责任链实现权限过滤器链
- Netty:用责任链把ChannelHandler串起来处理请求
- Spring MVC:拦截器链也是类似的思想
Sentinel也用了责任链模式,不过是它自己的实现方式。
二、Sentinel的责任链结构
2.1 ProcessorSlot接口
先看ProcessorSlot接口的定义:
public interface ProcessorSlot<T> {
// 入口方法
void entry(Context context, ResourceWrapper resourceWrapper, T param, int count,
boolean prioritized, Object... args) throws Throwable;
// 调用下一个ProcessorSlot的entry方法
void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count,
boolean prioritized, Object... args) throws Throwable;
// 出口方法
void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
// 调用下一个ProcessorSlot的exit方法
void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
}
每个ProcessorSlot有两对方法:
- entry:请求进入时调用(往链的后面传)
- exit:请求退出时调用(往链的后面传)
注意:Sentinel的exit也是从前往后传的,不像Netty是从后往回传。这是两者的一个重要区别。
参数解析:
| 参数 | 含义 | 说明 |
|---|---|---|
| context | 当前调用链路上下文 | 贯穿整个调用链 |
| resourceWrapper | 资源ID | 当前保护的资源 |
| param | 泛型参数 | 一般传递DefaultNode |
| count | 申请数量 | 类似AQS的acquire参数,一般为1 |
| prioritized | 是否优先级排序 | 默认false |
| args | 调用参数 | 用于热点参数限流 |
举个例子:DegradeSlot(熔断降级Slot)的工作方式就是——在entry方法中检查是否达到熔断阈值,如果是就抛出DegradeException,exit方法什么也不做。
2.2 责任链的默认顺序
Sentinel的ProcessorSlot可以分为两大类:
- 辅助统计类:负责构建节点、统计数据
- 降级功能类:负责实现各种降级功能
默认的Slot顺序是这样的:
NodeSelectorSlot → ClusterBuilderSlot → LogSlot → StatisticSlot →
AuthoritySlot → SystemSlot → FlowSlot → DegradeSlot
为什么是这个顺序?
因为降级功能需要依赖统计数据,所以辅助统计的Slot必须放在前面。如果某个Slot不依赖统计数据,那位置就可以随意。
而且,同分类下的Slot也有严格顺序:
- 辅助统计类:NodeSelectorSlot → ClusterBuilderSlot → StatisticSlot,顺序乱了会抛异常
- 降级功能类:AuthoritySlot、SystemSlot、FlowSlot、DegradeSlot,顺序可以按需调整
2.3 默认的SlotChain构建器
DefaultSlotChainBuilder是默认的SlotChain构建器,代码很简单:
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new SystemSlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
就是按顺序一个个addLast到链上。
三、责任链是怎么串起来的
3.1 AbstractLinkedProcessorSlot
为什么能串成链?因为所有ProcessorSlot都继承了AbstractLinkedProcessorSlot,这个类里有一个指向下一个节点的指针:
public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
// 当前节点的下一个节点
private AbstractLinkedProcessorSlot<?> next = null;
public void setNext(AbstractLinkedProcessorSlot<?> next) {
this.next = next;
}
}
然后,fireEntry和fireExit方法就是调用下一个节点的对应方法:
@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj,
int count, boolean prioritized, Object... args) throws Throwable {
if (next != null) {
T t = (T) obj;
// 调用下一个ProcessorSlot的entry方法
next.entry(context, resourceWrapper, t, count, prioritized, args);
}
}
@Override
public void fireExit(Context context, ResourceWrapper resourceWrapper,
int count, Object... args) {
if (next != null) {
// 调用下一个ProcessorSlot的exit方法
next.exit(context, resourceWrapper, count, args);
}
}
每个Slot的entry方法里,处理完自己的事情后,调用fireEntry把请求传给下一个Slot。exit同理。
3.2 DefaultProcessorSlotChain
ProcessorSlotChain是链的抽象,默认实现是DefaultProcessorSlotChain。
public class DefaultProcessorSlotChain extends ProcessorSlotChain {
// 头节点(一个空实现的Slot,作为占位符)
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t,
int count, boolean prioritized, Object... args) throws Throwable {
super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper,
int count, Object... args) {
super.fireExit(context, resourceWrapper, count, args);
}
};
// 尾节点
AbstractLinkedProcessorSlot<?> end = first;
}
first是一个空实现的Slot,作为链表的头节点(哨兵节点)。end指向链表的尾节点,方便addLast操作。
addFirst和addLast的实现也很直观:
@Override
public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
protocolProcessor.setNext(first.getNext());
first.setNext(protocolProcessor);
if (end == first) {
end = protocolProcessor;
}
}
@Override
public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
end.setNext(protocolProcessor);
end = protocolProcessor;
}
调用链的entry和exit方法,就是从头节点开始:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj,
int count, boolean prioritized, Object... args) throws Throwable {
first.entry(context, resourceWrapper, obj, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper,
int count, Object... args) {
first.exit(context, resourceWrapper, count, args);
}
3.3 每个资源一条链
Sentinel会为每个资源创建且仅创建一个ProcessorSlotChain,资源名称相同就用同一条链。
这些链被缓存在CtSph的chainMap里:
public class CtSph implements Sph {
// 资源与ProcessorSlotChain的映射
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
}
在entryWithPriority方法中,会先查找对应的chain,找不到就创建:
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count,
boolean prioritized, Object... args) throws BlockException {
Context context = ContextUtil.getContext();
// 查找或创建ProcessorSlotChain
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
// 创建CtEntry
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 调用链的entry方法
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
}
return e;
}
为什么每个资源一条链?因为不同资源的规则配置不一样,统计数据也是分开的。每条链独立工作,互不干扰。
四、Sentinel的整体工作流程
说了这么多,一次请求在Sentinel中到底是怎么流动的?
我们先看一下不使用适配器的情况下,Sentinel的标准用法:
ContextUtil.enter("上下文名称,例如:sentinel_spring_web_context");
Entry entry = null;
try {
entry = SphU.entry("资源名称,例如:/rpc/openfein/demo", EntryType.IN);
// 执行业务方法
return doBusiness();
} catch (Exception e) {
if (!(e instanceof BlockException)) {
Tracer.trace(e);
}
throw e;
} finally {
if (entry != null) {
entry.exit(1);
}
ContextUtil.exit();
}
整个流程可以分为五步:
- ContextUtil.enter():创建调用上下文
- SphU.entry():进入资源保护(核心)
- Tracer.trace():记录业务异常(非BlockException)
- Entry.exit():退出资源保护
- ContextUtil.exit():销毁调用上下文
下面我们一步步拆解。
4.1 第一步:ContextUtil.enter()
ContextUtil.enter()负责为当前调用链路创建Context,以及创建EntranceNode。
核心逻辑:
protected static Context trueEnter(String name, String origin) {
// 先从ThreadLocal里拿
Context context = contextHolder.get();
if (context == null) {
// 从缓存中找EntranceNode
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 双重检查锁,创建EntranceNode
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 添加到ROOT的子节点
Constants.ROOT.addChild(node);
// 更新缓存
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
} finally {
LOCK.unlock();
}
}
// 创建Context
context = new Context(node, name);
context.setOrigin(origin);
// 放入ThreadLocal
contextHolder.set(context);
}
return context;
}
关键点:
- ThreadLocal存储Context:每个线程一个Context
- 每个Context名称对应一个EntranceNode:比如sentinel_spring_web_context这个名称,全局只有一个EntranceNode
- EntranceNode挂在ROOT下面:形成全局的调用树
打个比方:
- 每次请求都会创建一个新的Context(因为每个请求是一个线程)
- 但这些Context的名称都是sentinel_spring_web_context
- 所以它们共用同一个EntranceNode
- 所有接口的DefaultNode都挂在这个EntranceNode下面
4.2 第二步:SphU.entry()
这是Sentinel最核心的一步。
SphU.entry() → CtSph.entry() → entryWithPriority(),整个过程:
- 创建ResourceWrapper(资源ID)
- 查找或创建ProcessorSlotChain(每个资源一条链)
- 创建CtEntry,并设置为Context.curEntry
- 调用ProcessorSlotChain.entry(),让请求经过整个责任链
- 如果抛出BlockException,说明被限流/熔断了,调用exit清理后抛出
ProcessorSlotChain的entry调用过程大概是这样的:
![图片[2]-Sentinel责任链模式与工作流程:一次请求的完整旅行 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/image-14-1024x374.png)
请求依次经过:
- NodeSelectorSlot:创建DefaultNode,构建调用链
- ClusterBuilderSlot:创建ClusterNode
- LogSlot:日志
- StatisticSlot:统计数据(先调用后面的Slot,根据结果统计)
- AuthoritySlot:黑白名单检查
- SystemSlot:系统自适应限流
- FlowSlot:限流检查
- DegradeSlot:熔断检查
任何一个Slot抛出BlockException,流程就会中断。
4.3 第三步:Tracer.trace()
只有抛出非BlockException异常时,才会调用Tracer.trace()。
什么意思?BlockException是Sentinel自己抛出的(限流、熔断等),不算业务异常。只有业务代码抛出的异常,才需要记录到异常统计里。
private static void traceExceptionToNode(Throwable t, int count, Entry entry, DefaultNode curNode) {
if (curNode == null) {
return;
}
ClusterNode clusterNode = curNode.getClusterNode();
if (clusterNode == null) {
return;
}
// 记录异常
clusterNode.trace(t, count);
}
ClusterNode的trace方法:
public void trace(Throwable throwable, int count) {
if (count <= 0) {
return;
}
if (!BlockException.isBlockException(throwable)) {
// 非BlockException,才自增异常总数
this.increaseExceptionQps(count);
}
}
为什么是ClusterNode而不是DefaultNode?因为熔断降级、限流降级用的都是ClusterNode的全局统计数据。
4.4 第四步:Entry.exit()
请求处理完(不管成功还是失败),都要调用entry.exit()。
CtEntry的exit方法核心逻辑:
protected void exitForContext(Context context, int count, Object... args) {
if (context != null) {
// 1、调用ProcessorSlotChain的exit方法
if (chain != null) {
chain.exit(context, resourceWrapper, count, args);
}
// 2、还原Context.curEntry为父Entry
context.setCurEntry(parent);
if (parent != null) {
((CtEntry)parent).child = null;
}
}
}
两件事:
- 调用链的exit方法:让每个Slot做退出时的处理(比如StatisticSlot记录成功、减少线程数等)
- 还原Context.curEntry:把当前Entry从链表上摘下来,curEntry指回父Entry
还记得之前说的父子Entry双向链表吗?每次创建新的CtEntry,就把它设为Context.curEntry。exit的时候,再把curEntry还原为父Entry。就像栈一样,后进先出。
4.5 第五步:ContextUtil.exit()
最后一步,清理ThreadLocal。
public static void exit() {
Context context = contextHolder.get();
if (context != null && context.getCurEntry() == null) {
contextHolder.set(null);
}
}
只有当Context.curEntry为空时,才把Context从ThreadLocal中移除。
为什么要加这个判断?因为一次请求中可能多次调用SphU.entry(),每次entry对应一次exit。只有当所有entry都exit了(curEntry为空),才说明整个调用链结束了,才能清理Context。
五、整体流程总结
用一张流程图来总结一下:
请求到来
│
▼
ContextUtil.enter() ──→ 创建Context,创建/获取EntranceNode
│
▼
SphU.entry() ──→ 创建CtEntry,调用ProcessorSlotChain.entry()
│
├─→ NodeSelectorSlot:创建DefaultNode
├─→ ClusterBuilderSlot:创建ClusterNode
├─→ LogSlot:日志
├─→ StatisticSlot:前置处理(线程数+1等)
├─→ AuthoritySlot:黑白名单检查
├─→ SystemSlot:系统自适应检查
├─→ FlowSlot:限流检查 ──→ 不通过 → 抛FlowException
└─→ DegradeSlot:熔断检查 ──→ 不通过 → 抛DegradeException
│
▼ (通过所有检查)
执行业务代码
│
▼ (业务异常)
Tracer.trace() ──→ 记录异常(非BlockException)
│
▼
Entry.exit() ──→ 调用ProcessorSlotChain.exit(),还原curEntry
│
▼
ContextUtil.exit() ──→ 清理ThreadLocal
总结与思考
这篇文章我们从责任链模式讲起,拆解了Sentinel的整体工作流程。
几个关键要点再回顾一下:
- 责任链是Sentinel的骨架:所有功能都通过ProcessorSlot实现,串成一条链
- 每个资源一条链:不同资源的链是独立的,缓存在chainMap中
- Slot顺序有讲究:统计类在前,功能类在后;有些Slot顺序不能乱
- 一次请求五步走:enter → entry → 业务 → exit → exit
- Context通过ThreadLocal传递:每个请求一个Context,贯穿整个调用链
- 父子Entry形成栈结构:多次entry形成双向链表,exit时按顺序还原
思考一下:
- 为什么Sentinel选择责任链模式?(扩展性好,每个Slot职责单一,可以灵活插拔)
- 为什么每个资源一条链,而不是全局一条链?(不同资源规则不同,统计独立)
- exit方法为什么也是从前往后传,而不是从后往回传?(设计选择,Sentinel的exit主要做一些收尾统计,顺序影响不大)
理解了整体工作流程,后面我们再深入每个Slot的具体实现时,就知道它处于什么位置、起到什么作用了。
下一篇,我们来聊聊Java SPI机制,以及Sentinel是怎么利用SPI实现灵活扩展的。










请登录后查看评论内容