前言
在第二章的时候,我们分析了Sph.entry和Entry.exit的执行流程。
除此以外,Sentinel还提供了一套Async API,本章来学习一下Async API的作用和源码。
一、案例
public class CustomAsyncDemo {
public static void main(String[] args) {
ContextUtil.enter("context-0", "appA");
Entry entry0 = null;
try {
entry0 = SphU.entry("entry0");
// 生成异步调用链路
AsyncEntry asyncEntry0 = SphU.asyncEntry("asyncEntry0");
Thread t1 = new Thread(() -> {
// 切换上下文
ContextUtil.runOnContext(/*上下文*/asyncEntry0.getAsyncContext(), /*Runnable*/ () -> {
try {
System.out.println(Thread.currentThread().getName() + " running, current_context = " + ContextUtil.getContext());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
asyncEntry0.exit();
}
});
});
t1.start();
System.out.println(Thread.currentThread().getName() + " running, current_context = " + ContextUtil.getContext());
t1.join();
} catch (BlockException | InterruptedException e) {
e.printStackTrace();
} finally {
if (entry0 != null) {
entry0.exit();
}
ContextUtil.exit();
}
}
}
根据上述demo,链路树长这样。
|_EntranceNode: context_0
|_ entry0
|_ asyncEntry0
控制台输出:
main running, current_context = Context{name='context-0', entranceNode=com.alibaba.csp.sentinel.node.EntranceNode@372f7a8d, curEntry=com.alibaba.csp.sentinel.CtEntry@7c3df479, origin='appA', async=false}
Thread-2 running, current_context = Context{name='context-0', entranceNode=com.alibaba.csp.sentinel.node.EntranceNode@372f7a8d, curEntry=com.alibaba.csp.sentinel.AsyncEntry@4c31aaef, origin='appA', async=true}
总结来说,async api做到了几点:
- 链路保持:SphU.asyncEntry方法,将async节点与当前节点平级,与父节点相连;
- 线程间上下文传递:ContextUtil.runOnContext方法,线程间传递Context。保证父线程Context中除了curEntry以外所有信息传递到子线程;
回顾一下Context的属性:
public class Context {
// 上下文名称
private final String name;
// EntranceNode 入口节点 --- 树根
private DefaultNode entranceNode;
// 当前Entry
private Entry curEntry;
// 来源
private String origin = "";
// 是否是异步上下文
private final boolean async;
}
二、SphU.asyncEntry
如果连续调用SphU.entry,树的层级会越来越深。
而SphU.asyncEntry能让新生成的节点与当前节点齐平。
核心代码在CtSph#asyncEntryWithPriorityInternal。
// CtSph.java
private AsyncEntry asyncEntryWithPriorityInternal(ResourceWrapper resourceWrapper, int count, boolean prioritized,
Object... args) throws BlockException {
// 1. 获取当前线程Context
Context context = ContextUtil.getContext();
// 当Context数量超过了阈值,不会做任何规则校验
if (context instanceof NullContext) {
return asyncEntryWithNoChain(resourceWrapper, context);
}
// 2. 如果用户没有主动创建Context,使用默认上下文sentinel_default_context
if (context == null) {
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
if (!Constants.ON) {
return asyncEntryWithNoChain(resourceWrapper, context);
}
// 3. 获取Slot链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
return asyncEntryWithNoChain(resourceWrapper, context);
}
// 4. 构造AsyncEntry,构造时将这个Entry接入Context中的Entry链表尾部
AsyncEntry asyncEntry = new AsyncEntry(resourceWrapper, chain, context);
try {
// 5. 执行所有规则校验
chain.entry(context, resourceWrapper, null, count, prioritized, args);
// 6. diff: 创建AsyncContext
asyncEntry.initAsyncContext();
// 7. diff: 对原有Context处理
asyncEntry.cleanCurrentEntryInLocal();
} catch (BlockException e1) {
asyncEntry.exitForContext(context, count, args);
throw e1;
} catch (Throwable e1) {
asyncEntry.cleanCurrentEntryInLocal();
}
return asyncEntry;
}
在entry方法的基础上,asyncEntry多了第6步和第7步。
首先关注AsyncEntry的构造方法(4) 。AsyncEntry在构造时,直接调用了父类的三个参数的构造方法。
public class AsyncEntry extends CtEntry {
AsyncEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper, chain, context);
}
}
// CtEntry.java
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;
setUpEntryFor(context);
}
private void setUpEntryFor(Context context) {
if (context instanceof NullContext) {
return;
}
this.parent = context.getCurEntry();
if (parent != null) {
((CtEntry) parent).child = this;
}
context.setCurEntry(this);
}
这意味着,在做规则校验时(5),当前上下文的curEntry是这个AsyncEntry,且当前AsyncEntry是接在上一个Entry的后面的,调用树如下:
|_EntranceNode: context_0
|_ entry0
|_ asyncEntry0
initAsyncContext方法(6) ,在AsyncEntry里创建了一个用于异步线程携带的上下文。在这里创建是为了让Entry被带入其他线程时,用户端代码能够拿到这个异步上下文。
// AsyncEntry.java
private Context asyncContext;
void initAsyncContext() {
if (asyncContext == null) {
if (context instanceof NullContext) {
asyncContext = context;
return;
}
// 拷贝上下文
this.asyncContext = Context.newAsyncContext(context.getEntranceNode(), context.getName())
.setOrigin(context.getOrigin())
.setCurEntry(this);
}
}
cleanCurrentEntryInLocal方法(7) ,将当前AsyncEntry从原来的Context的调用链中移除,将原有Context调用链和异步Context调用链分开。即AsyncEntry存在于原来的上下文中,仅仅是为了走一遍所有Slot。
public class AsyncEntry extends CtEntry {
private Context asyncContext;
void cleanCurrentEntryInLocal() {
if (context instanceof NullContext) {
return;
}
Context originalContext = context;
if (originalContext != null) {
Entry curEntry = originalContext.getCurEntry();
if (curEntry == this) {
Entry parent = this.parent;
originalContext.setCurEntry(parent);
if (parent != null) {
((CtEntry)parent).child = null;
}
} else {
// ...
throw new IllegalStateException(msg);
}
}
}
}
至此,异步Context调用树如下:
|_EntranceNode: context_0
|_ entry0
|_ asyncEntry0
同步Context调用树如下:
|_EntranceNode: context_0
|_ entry0
AsyncEntry的规则校验和数据统计都已经走完了,需要用户代码将异步Context传递到子线程中。
三、ContextUtil.runOnContext
用户代码调用ContextUtil.runOnContext,传入异步Context和回调方法(Runnable),可以实现上下文传递。
// 生成异步调用链路
AsyncEntry asyncEntry0 = SphU.asyncEntry("asyncEntry0");
Thread t1 = new Thread(() -> {
// 切换上下文
ContextUtil.runOnContext(/*上下文*/asyncEntry0.getAsyncContext(), /*Runnable*/ () -> {
try {
System.out.println(Thread.currentThread().getName() + " running, current_context = " + ContextUtil.getContext());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
asyncEntry0.exit();
}
});
});
t1.start();
runOnContext方法没有什么特别之处,就是将用户业务代码外面裹一层replaceContext方法,用于切换上下文。
// ContextUtil.java
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
public static void runOnContext(Context context, Runnable f) {
// 1. 用入参Context替换当前线程ThreadLocal持有的Context
Context curContext = replaceContext(context);
try {
// 2. 执行callback
f.run();
} finally {
// 3. 复原当前线程ThreadLocal持有的Context
replaceContext(curContext);
}
}
static Context replaceContext(Context newContext) {
Context backupContext = contextHolder.get();
if (newContext == null) {
contextHolder.remove();
} else {
contextHolder.set(newContext);
}
return backupContext;
}
总结
Sentinel的Async API支持在不同线程间传播Context上下文。
更好的支持了流控模式为链路的流控规则(因为链路流控规则涉及到当前上下文)。