@Author:zxw
@Email:502513206@qq.com
目录
-
1.前言
上篇文章已经讲了资源类
CtEntry
中的具体属性,那接下来通过一段代码看下Sentinel在初始化Entry
的过程中做了哪些操作。public static void main(String[] args) {
initFlowRules();
while (true) {
Entry entry = null;
try {
// 自定义资源名,需要和exit()方法承兑出现
entry = SphU.entry("HelloWorld");
// 被保护的业务逻辑
/*您的业务逻辑 - 开始*/
System.out.println("hello world");
/*您的业务逻辑 - 结束*/
} catch (BlockException e1) {
// 资源访问组织,被限流或被降级
/*限流控逻辑处理 - 开始*/
System.out.println("block!");
/*限流控逻辑处理 - 结束*/
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
2.源码分析
2.1 Sph
通过上面代码可以看到创建Entry是通过如下方式,SphU可以理解为一个帮我们创建Entry资源的工具类,只是一个调用的入口
SphU.entry("HelloWorld");
。但是真正创建Entry资源的其实是我们的CtSph
类,Sph接口提供了同步和异步创建Entry的方式以及之前讲解ResourceWrapper
时有Method和String两种。 ```java public interface Sph extends SphResourceTypeSupport {Entry entry(String name) throws BlockException;
AsyncEntry asyncEntry(String name, EntryType trafficType, int batchCount, Object… args) throws BlockException;
Entry entry(Method method) throws BlockException; }
对于上面代码的创建部分
```java
entry = SphU.entry("HelloWorld");
我们知道Entry内部还有一个ResrouceWrapper
资源类,这里我们只传了一个name字符串进去,该name肯定就是资源名称并且代码内部会帮我们创建一个ResourceWrapper
资源类
StringResourceWrapper resource = new StringResourceWrapper(name, type);
2.2 Context
之前分析CtEntry
元数据时,其内部还有一个上下文对象Context
,所以首先我们需要获取当前线程的上下文对象
Context context = ContextUtil.getContext();
这个Context因为是线程独有的,内部通过ThreadLocal
进行存储,Sentinel对其有数量大小的限制,其大小一般为2000
// ContextUtil
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
// Constants:最大上下文数量
public final static int MAX_CONTEXT_NAME_SIZE = 2000;
如果当前Context的数量超过了最大限制数量,那么Sentinel默认会返回空的上下文对象,对于该对象Sentinel就不会进行任何规则校验
// ContextUtil
private static final Context NULL_CONTEXT = new NullContext();
// CtSph:忽略规则链,直接创建对象
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,
// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper, null, context);
}
对于第一次启动程序,那上下文肯定是不存在的,这时会先生成一个context。可以看到生成Context时传了一个固定值sentinel_default_context
,使用过sentinel控制台的应该都了解,我们的请求资源一般都会挂在一个默认的父节点下,这就是Sentinel会默认为我们生成父节点。
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// Constants
public final static String CONTEXT_DEFAULT_NAME = "sentinel_default_context";
2.3 ProcessorSlot
有了上下文,回顾一下CtEntry
资源类中还有个调用链路ProcessorSlot
的字段,接下来则是构造该链路
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
在来回顾一下下图,在这里就是构造这样一条调用链
Sentinel是通过Spi机制首先构建出SlotChainBuilder
的builder对象,该配置文件存在com.alibaba.csp:sentinel-core
包下的META-INF/services
目录中。如果找不到的话就会默认使用DefaultSlotChainBuilder
构造类。SlotChainProvider.class
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// Resolve the slot chain builder SPI.
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
if (slotChainBuilder == null) {
// Should not go through here.
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
slotChainBuilder.getClass().getCanonicalName());
}
return slotChainBuilder.build();
}
创建构造类后通过build方法得到链路,这个链路Sentinel已经事先准备好在配置文件中了,也是通过Spi进行获取的,如果我们想扩展自己的插槽则可以在com.alibaba.csp.sentinel.slotchain.ProcessorSlot
文件中加入我们的构造类
# Sentinel default ProcessorSlots
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
com.alibaba.csp.sentinel.slots.logger.LogSlot
com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
com.alibaba.csp.sentinel.slots.system.SystemSlot
com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
加载类之后还需要做一件事就是对他们进行排序,在这里Sentinel是自定义了一个@Spi
注解,通过注解上的order字段链路的排序得到一个排序后的列表
// SpiLoader.class
Collections.sort(sortedClassList, new Comparator<Class<? extends S>>() {
@Override
public int compare(Class<? extends S> o1, Class<? extends S> o2) {
Spi spi1 = o1.getAnnotation(Spi.class);
int order1 = spi1 == null ? 0 : spi1.order();
Spi spi2 = o2.getAnnotation(Spi.class);
int order2 = spi2 == null ? 0 : spi2.order();
return Integer.compare(order1, order2);
}
});
之前分析过ProcessorSlotChain
既然是个链路,那么肯定存在着前后节点的引用,这时我们拿到排序后的链表,只需要循环调用加入到下一个节点即可
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
简单画下ProcessorSlot
得到的流程
可以看到构造链路如此繁琐,所以得到链路后首先要缓存到本地中,在CtSph
类中通过一个Map映射缓存该链路
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
当然这里链路的数量并不时无限的,Sentinel设置默认的大小为6000,如果超过了则直接返回空链路
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
如果返回空的链路,那就不存在什么规则校验这类东西,因为Sentinel对于流量控制和规则校验都是在链路中的节点做的,所以链路为空则直接返回对象。
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
2.4 Entry
在得到了链路ProcessorSlot
、上下文Context
、资源ResourceWrapper
后这下就能构造我们的资源类Entry
对象了。
Entry e = new CtEntry(resourceWrapper, chain, context);
3.总结
可以发现Entry
的构造流程其实并不复杂,有三个核心东西值得我们关注
- ResourceWrapper:一般都是用StringResourceWrapper
- Context:线程独有的上下文对象
- ProcessorSlot:通过Spi机制加载调用的链路
对于本次流程中出现的一些类进行解释说明
- SphU:构造Entry的工具类
- CtSph:提供了构造Entry的接口,SphU内部本质上是调用的Sph提供的接口
- ContextUtil:管理Context上下文
- InternalContextUtil:构造Context上下文
- SlotChainProvider:创建
SlotChainBuilder
对象 - SlotChainBuilder:创建ProcessorSlotChain调用链