问题现象
服务有调用下面的代码的行为,QPS也不高,大概几十上百的样子,但是导致整个服务的CPU飙高,服务运行hang住;Redis命令有大量超时的现象
样例代码如下:
StatefulRedisConnection<String, String> redisConnection = redisClient.connect();
RedisAsyncCommands<String, String> asyncCommands = redisConnection.async();
// 异步调用
RedisFuture<String> key4Future = asyncCommands.hget("key4", "subkey4");
key4Future.thenAccept(s -> {
// 同步阻塞
if(s==null){
redisConnection.sync().hset("key4", "subkey4", "value4");
redisConnection.sync().expire("key4", 1000);
}
});
asyncCommands.setex("key1", 1000, "value1");
asyncCommands.setex("key2", 1000, "value2");
asyncCommands.setex("key3", 1000, "value3");
问题根因
把整个流程简化后;
- key4Future会将命令thenAccept封装成一个UniAccept放到stack中;
- 当前命令都在同一个EventLoop中执行,当EventLoop将hget命令发送给server,server将结果返回后,会交给Lettuce的CommandHandler,CommandHandler会根据RESP格式进行解析redis返回结果;当解析完成,将命令的结果写入到AsyncCommand后,会调用AsyncCommand.complete方法,这个方法里面会调用
CF.postComplete() - 当执行postComplete的时候,会从stack中获取任务,开始执行;当执行到hset命令时,这个命令又路由到同一个EpollEventLoop中,然后又将这个任务放到
EventLoop.taskQueue - IO线程仍然在等待hset结果的返回,可是hset无法返回,应该他被放到taskQueue里面了,导致了死锁;
- 最终只能等待hset命令超时,这个线程才会正常执行;
问题解决
在异步流程中,不要执行redis的同步执行命令;不然会导致redis命令执行超时的情况;
问题
如果在redis异步执行中,也有异步执行redis命令,这种会有问题吗?
解决这个问题,需要从CompletableFuture的源码中来回答了
CompletableFuture执行源码解析
功能
异步并行执行、多任务异步编排、回调函数等
一句话原理
CompletableFuture作为Publisher,若CF执行完成,回调通知Observer(Completion)列表,观察者继续往下执行;其中观察者(Observer)内部有一发布者(Publisher)(CompletableFuture),作为下一阶段通知的通知源;
整体思维导图
样例
以一个简单样例形成的异步流,来解析流程执行过程;
CompletableFuture<String> vegetable = CompletableFuture.supplyAsync(() -> {
System.out.println("去买菜" + Thread.currentThread());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "supplyAsync success";
});
vegetable.thenAccept(s -> {
if (s.contains("success")) {
System.out.println("买菜成功" + Thread.currentThread());
return;
}
System.out.println("买菜失败");
});
CompletableFuture<String> cook = vegetable.thenApply(s -> {
if (s.contains("success")) {
System.out.println("开始做饭" + Thread.currentThread());
return "thenApply success";
}
System.out.println("做饭失败");
return "thenApply fail";
});
cook.thenApply(s -> {
if (s.contains("success")) {
System.out.println("西红柿炒鸡蛋成功" + Thread.currentThread());
return "thenApply1 success";
}
System.out.println("西红柿炒鸡蛋失败");
return "thenApply1 fail";
});
cook.thenAccept((s) -> {
if (s.contains("success")) {
System.out.println("小炒黄牛肉成功" + Thread.currentThread());
return;
}
System.out.println("小炒黄牛肉失败");
});
整体流程
源码讲解
以这个样例作为入口,过一遍CF的组合、通知的整体源码解析流程;至于二元、多元操作流程、原理类似,就不再展开;
java.util.concurrent.CompletableFuture#asyncSupplyStage
- 将当前操作封装成一个CompletableFuture;
- 又创建一个AsyncSupply对象,将CF对象赋值给dep变量
- 将创建好的CF对象返回给主线程;
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(ASYNC_POOL, supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
e.execute(new AsyncSupply<U>(d, f));
return d;
}
java.util.concurrent.CompletableFuture#thenAccept
- 主线程继续往下执行,
- 如果当前的CF已经执行完成了,那么就会执行uniAcceptNow,获取action的结果;return
- 将当前的action封装成一个新的CompletableFuture,
- 将CompletableFuture封装成一个UniAccept;压入栈
- 将新建的CF返回
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
return uniAcceptStage(null, action);
}
private CompletableFuture<Void> uniAcceptStage(Executor e,
Consumer<? super T> f) {
if (f == null) throw new NullPointerException();
Object r;
if ((r = result) != null)
return uniAcceptNow(r, e, f);
CompletableFuture<Void> d = newIncompleteFuture();
unipush(new UniAccept<T>(e, d, this, f));
return d;
}
java.util.concurrent.CompletableFuture#thenApply
- 主线程继续执行,
- 如果当前的CompletableFuture已经执行完成,那么会执行uniApplyNow,执行f; return
- 创建CompletableFuture,将其在封装成一个UniApply(Completion)放入等待栈中
- 返回创建的CompletableFuture
- 至此,主线程任务已经执行完成
public <U> CompletableFuture<U> thenApply(
Function<? super T,? extends U> fn) {
return uniApplyStage(null, fn);
}
private <V> CompletableFuture<V> uniApplyStage(
Executor e, Function<? super T,? extends V> f) {
if (f == null) throw new NullPointerException();
Object r;
if ((r = result) != null)
return uniApplyNow(r, e, f);
CompletableFuture<V> d = newIncompleteFuture();
unipush(new UniApply<T,V>(e, d, this, f));
return d;
}
final void unipush(Completion c) {
if (c != null) {
while (!tryPushStack(c)) {
if (result != null) {
NEXT.set(c, null);
break;
}
}
if (result != null)
c.tryFire(SYNC);
}
}
去买菜:java.util.concurrent.CompletableFuture.AsyncSupply#run
- 接上面,将AsyncSupply放入线程池后,线程执行,会执行到run方法;
- 经过一系列非空判断后,调用f.get() 获取执行结果(对应
去买菜那个函数) - completeValue 给当前的CompletableFuture设置result
- postComplete开始做后置通知
public void run() {
CompletableFuture<T> d; Supplier<? extends T> f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null;
if (d.result == null) {
try {
d.completeValue(f.get());
} catch (Throwable ex) {
d.completeThrowable(ex);
}
}
d.postComplete();
}
}
java.util.concurrent.CompletableFuture#postComplete
- 当前对象stack不为空,则说明还有未处理的消息监听者(Observer)
- 通过CAS开始操作,首先当前CF对象的stack指针移动到下一Completion
- 将当前Completion的next置为null,方便gc、分离
- 调用当前已分离的Completion的tryFire方法
final void postComplete() {
CompletableFuture<?> f = this; Completion h;
while ((h = f.stack) != null ||
(f != this && (h = (f = this).stack) != null)) {
CompletableFuture<?> d; Completion t;
// cas 操作,利用varHandle来操作,等价于f.stack -->由h更新成h.next
if (STACK.compareAndSet(f, h, t = h.next)) {
if (t != null) {
// 说明在下面调用tryFire,返回的结果中,并不是this本身;而是d;
// 所以将d加入到stack里面,准备执行;
if (f != this) {
pushStack(h);
continue;
}
// 等价于h.next --> 由t修改成null;将h从stack中分离出来,开始执行
NEXT.compareAndSet(h, t, null); // try to detach
}
// 调用Completion.tryFire(-1)
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
开始做饭:java.util.concurrent.CompletableFuture.UniApply#tryFire
- 当执行到此时,买菜已经回来了,那么下一步就是去做饭、以及下发买菜成功的同志
- d表示准备做饭;claim表示如果有异步线程池,那么就异步执行操作即可,return null;如果没有,再开始同步执行操作 (对应
准备做饭)
final CompletableFuture<V> tryFire(int mode) {
CompletableFuture<V> d; CompletableFuture<T> a;
Object r; Throwable x; Function<? super T,? extends V> f;
if ((d = dep) == null || (f = fn) == null
|| (a = src) == null || (r = a.result) == null)
return null;
// d代表当前这个Completion中的CompletableFuture;这个时候,应该还没有执行
// 所以d一定是未执行状态,d.result =null 是成立的;
tryComplete: if (d.result == null) {
if (r instanceof AltResult) {
if ((x = ((AltResult)r).ex) != null) {
d.completeThrowable(x, r);
break tryComplete;
}
r = null;
}
try {
// claim 代表是否有异步线程池,如果有,则return;没有,则是同步任务
if (mode <= 0 && !claim())
return null;
else {
@SuppressWarnings("unchecked") T t = (T) r;
// 同步执行完成后,设置当前准备做饭对应的CF的result;标志当前这一步已经执行完成
d.completeValue(f.apply(t));
}
} catch (Throwable ex) {
d.completeThrowable(ex);
}
}
dep = null; src = null; fn = null;
// 然后开始执行 准备做饭的下一步了
return d.postFire(a, mode);
}
java.util.concurrent.CompletableFuture#postFire
- a表示src,首先先判断买菜回来没?mode = -1
final CompletableFuture<T> postFire(CompletableFuture<?> a, int mode) {
if (a != null && a.stack != null) {
Object r;
// 当前执行到,说明已经买菜回来了,所以a.result是有结果的;
if ((r = a.result) == null)
a.cleanStack();
// mode =-1
if (mode >= 0 && (r != null || a.result != null))
a.postComplete();
}
// 当前这一步,开始做饭 也完成了;开始做饭的Observer(洗西红柿、搅拌鸡蛋等)也不为空;
if (result != null && stack != null) {
// 但是 mode = -1 , 直接返回; 和postComplete里面的f!=this遥相呼应
if (mode < 0)
// this 表示的是开始做菜这个CF;
return this;
else
postComplete();
}
return null;
}
再次走到java.util.concurrent.CompletableFuture#postComplete
- 将第二层的步骤从stack中拿出来,然后放到第一层的stack中;通俗点:将开始准备做饭后面的步骤(洗西红柿、搅拌鸡蛋、小炒黄牛肉)弹出栈,然后将其放入去买菜的stack中
- 详细解释参看下面的注释
final void postComplete() {
CompletableFuture<?> f = this; Completion h;
// 2、此时h= f.stack(开始做饭的后面三个步骤:洗西红柿、搅拌鸡蛋、小炒黄牛肉)
while ((h = f.stack) != null ||
(f != this && (h = (f = this).stack) != null)) {
CompletableFuture<?> d; Completion t;
// 开始做饭的CF中的stack指向下一个步骤;把小炒黄牛肉这个Completion赋值给了h;
if (STACK.compareAndSet(f, h, t = h.next)) {
if (t != null) {
// 3、cf已经变了,所以这里一定可以进去;
if (f != this) {
// 4、把小炒黄牛肉的Completion放到买菜的stack里面;continue
pushStack(h);
continue;
}
NEXT.compareAndSet(h, t, null); // try to detach
}
// 1、返回到开始做饭这个CF;注意:此时f已经变了, 所以在下次循环的时候,f(开始做饭)!=this(去买菜)
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
形成的结构图如下;
note: 从上一个stack出栈,再入栈;顺序倒换;
小炒黄牛肉、买菜成功通知封装成UniAccept类型,其执行过程和UniApply类型,故不在此赘述
然后看下thenCompose是如何执行的?
代码还是回到java.util.concurrent.CompletableFuture#postComplete方法
重点在注释的位置;
final void postComplete() {
CompletableFuture<?> f = this; Completion h;
// 洗西红柿CF的stack里面有一个BiApply
while ((h = f.stack) != null ||
(f != this && (h = (f = this).stack) != null)) {
CompletableFuture<?> d; Completion t;
// 2、获取stack的下一元素
if (STACK.compareAndSet(f, h, t = h.next)) {
// 3、发现只有一个元素,直接跳过;
if (t != null) {
if (f != this) {
pushStack(h);
continue;
}
NEXT.compareAndSet(h, t, null); // try to detach
}
// (下一轮回)4、开始执行这个BiApply(西红柿炒鸡蛋Completion)
// 1、h代表洗西红柿这个Completion,当执行完,返回的是洗西红柿这个CF
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
java.util.concurrent.CompletableFuture.BiApply#tryFire
- 看到一大堆的判空了吗? 玄妙就在这里;
- a.result 或者b.result其中一个为空,那么直接返回;
- 只有满足,a、b两个依赖都有值,才会调用postFire,才有后置执行操作;
final CompletableFuture<V> tryFire(int mode) {
CompletableFuture<V> d;
CompletableFuture<T> a;
CompletableFuture<U> b;
Object r, s; BiFunction<? super T,? super U,? extends V> f;
if ((d = dep) == null || (f = fn) == null
|| (a = src) == null || (r = a.result) == null
|| (b = snd) == null || (s = b.result) == null
|| !d.biApply(r, s, f, mode > 0 ? null : this))
// 这里的返回null,和postComplete里面三元表达式遥相呼应;
return null;
dep = null; src = null; snd = null; fn = null;
return d.postFire(a, b, mode);
}
扩展一些,看下BiApply里面都有哪些变量
BiFunction<? super T,? super U,? extends V> fn; // 待执行函数;西红柿炒鸡蛋
Executor executor; // 异步线程池
CompletableFuture<V> dep; // 依赖此step的下一步,比如:准备吃饭;
CompletableFuture<T> src; // 洗西红柿
CompletableFuture<U> snd; // 搅拌鸡蛋
- 至此,我们把CompletableFuture的一元(UniApply、UniAccept)、二元(BiApply)源码通过做饭的流程讲解完成,相关的细节流程也梳理相对清楚;
- 其他的步骤比如(UniRun、BiAccept)都是相似流程;这里不做赘述了;
- 异步流程也是类似,只不过把一些通过通过调用
claim方法,将其放到异步线程中进行执行了;
进阶
可作为进阶练习
通过此练习一元、二元、多元、异步编排等功能;熟悉相关流程
注意点
- CompletableFuture默认使用CommonPool(ForkJoinPool)作为共享线程池;但是该池子的线程池默认是
CPU核心数-1;而且这个commonPool默认被JDK多个组件使用,比如:parallelStream,CompletableFuture等;所以如果大家使用时,需要格外注意; - ForkJoinPool默认是work-stealing机制,针对一个能分而治之、结果可以广而聚之的任务是非常友好的;但即便如此,使用FJP还是会有死锁的情况,这个后续写个文章细聊;
- 看完源码,CF有好的一面,自然也有不好的一面;如果有CompletableFuture作为异步编程框架来使用, 需额外注意步骤依赖、相互等待导致死锁的情况;
从中学习感悟
学习源码,一方面是知其然知其所以然,另一方面是我们从中学习优秀的设计思想,为后续的设计工作提供优秀的参考价值;
- 无锁化设计思想,避免上锁、释放锁带来的性能损耗
- 发布-订阅模式;CF作为Publisher进行发布通知,Completion作为Observer进行订阅、并处理通知;同时每个Completion中也有CF作为新一轮的Publisher进行发布通知,这样CF相当于一个隔板的作用,起到相互隔离的作用;
- Treiber Stack 无锁栈在很多源码中都有设计,有空可以手动实现一遍,了解下其中的奥妙;
- 封装性做的很好,每一种Step都会对应一个相应的Completion,通过覆写tryFire等操作来完成特有逻辑