Lettuce异步阻塞问题讲解&CompletableFuture源码讲解

195 阅读10分钟

问题现象

服务有调用下面的代码的行为,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");

问题根因

把整个流程简化后;

  1. key4Future会将命令thenAccept封装成一个UniAccept放到stack中;
  2. 当前命令都在同一个EventLoop中执行,当EventLoop将hget命令发送给server,server将结果返回后,会交给Lettuce的CommandHandler,CommandHandler会根据RESP格式进行解析redis返回结果;当解析完成,将命令的结果写入到AsyncCommand后,会调用AsyncCommand.complete方法,这个方法里面会调用CF.postComplete()
  3. 当执行postComplete的时候,会从stack中获取任务,开始执行;当执行到hset命令时,这个命令又路由到同一个EpollEventLoop中,然后又将这个任务放到EventLoop.taskQueue
  4. IO线程仍然在等待hset结果的返回,可是hset无法返回,应该他被放到taskQueue里面了,导致了死锁
  5. 最终只能等待hset命令超时,这个线程才会正常执行;

image.png

问题解决

在异步流程中,不要执行redis的同步执行命令;不然会导致redis命令执行超时的情况;

问题

如果在redis异步执行中,也有异步执行redis命令,这种会有问题吗?
解决这个问题,需要从CompletableFuture的源码中来回答了

CompletableFuture执行源码解析

功能

异步并行执行、多任务异步编排、回调函数等

一句话原理

CompletableFuture作为Publisher,若CF执行完成,回调通知Observer(Completion)列表,观察者继续往下执行;其中观察者(Observer)内部有一发布者(Publisher)(CompletableFuture),作为下一阶段通知的通知源;

image.png

整体思维导图

image.png

样例

以一个简单样例形成的异步流,来解析流程执行过程;

image.png

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("小炒黄牛肉失败");
});

整体流程

image.png

源码讲解

以这个样例作为入口,过一遍CF的组合、通知的整体源码解析流程;至于二元、多元操作流程、原理类似,就不再展开;

java.util.concurrent.CompletableFuture#asyncSupplyStage
  1. 将当前操作封装成一个CompletableFuture;
  2. 又创建一个AsyncSupply对象,将CF对象赋值给dep变量
  3. 将创建好的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
  1. 主线程继续往下执行,
  2. 如果当前的CF已经执行完成了,那么就会执行uniAcceptNow,获取action的结果;return
  3. 将当前的action封装成一个新的CompletableFuture,
  4. 将CompletableFuture封装成一个UniAccept;压入栈
  5. 将新建的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
  1. 主线程继续执行,
  2. 如果当前的CompletableFuture已经执行完成,那么会执行uniApplyNow,执行f; return
  3. 创建CompletableFuture,将其在封装成一个UniApply(Completion)放入等待栈中
  4. 返回创建的CompletableFuture
  5. 至此,主线程任务已经执行完成

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
  1. 接上面,将AsyncSupply放入线程池后,线程执行,会执行到run方法;
  2. 经过一系列非空判断后,调用f.get() 获取执行结果(对应去买菜那个函数)
  3. completeValue 给当前的CompletableFuture设置result
  4. 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
  1. 当前对象stack不为空,则说明还有未处理的消息监听者(Observer)
  2. 通过CAS开始操作,首先当前CF对象的stack指针移动到下一Completion
  3. 将当前Completion的next置为null,方便gc、分离
  4. 调用当前已分离的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
  1. 当执行到此时,买菜已经回来了,那么下一步就是去做饭、以及下发买菜成功的同志
  2. 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
  1. 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
  1. 将第二层的步骤从stack中拿出来,然后放到第一层的stack中;通俗点:将开始准备做饭后面的步骤(洗西红柿、搅拌鸡蛋、小炒黄牛肉)弹出栈,然后将其放入去买菜的stack中
  2. 详细解释参看下面的注释
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出栈,再入栈;顺序倒换;

image.png

小炒黄牛肉、买菜成功通知封装成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
  1. 看到一大堆的判空了吗? 玄妙就在这里;
  2. a.result 或者b.result其中一个为空,那么直接返回;
  3. 只有满足,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; // 搅拌鸡蛋

  1. 至此,我们把CompletableFuture的一元(UniApply、UniAccept)、二元(BiApply)源码通过做饭的流程讲解完成,相关的细节流程也梳理相对清楚;
  2. 其他的步骤比如(UniRun、BiAccept)都是相似流程;这里不做赘述了;
  3. 异步流程也是类似,只不过把一些通过通过调用claim方法,将其放到异步线程中进行执行了;

进阶

可作为进阶练习 通过此练习一元、二元、多元、异步编排等功能;熟悉相关流程 image.png

注意点

  1. CompletableFuture默认使用CommonPool(ForkJoinPool)作为共享线程池;但是该池子的线程池默认是CPU核心数-1 ;而且这个commonPool默认被JDK多个组件使用,比如:parallelStream,CompletableFuture等;所以如果大家使用时,需要格外注意;
  2. ForkJoinPool默认是work-stealing机制,针对一个能分而治之、结果可以广而聚之的任务是非常友好的;但即便如此,使用FJP还是会有死锁的情况,这个后续写个文章细聊;
  3. 看完源码,CF有好的一面,自然也有不好的一面;如果有CompletableFuture作为异步编程框架来使用, 需额外注意步骤依赖、相互等待导致死锁的情况;

从中学习感悟

学习源码,一方面是知其然知其所以然,另一方面是我们从中学习优秀的设计思想,为后续的设计工作提供优秀的参考价值;

  1. 无锁化设计思想,避免上锁、释放锁带来的性能损耗
  2. 发布-订阅模式;CF作为Publisher进行发布通知,Completion作为Observer进行订阅、并处理通知;同时每个Completion中也有CF作为新一轮的Publisher进行发布通知,这样CF相当于一个隔板的作用,起到相互隔离的作用;
  3. Treiber Stack 无锁栈在很多源码中都有设计,有空可以手动实现一遍,了解下其中的奥妙;
  4. 封装性做的很好,每一种Step都会对应一个相应的Completion,通过覆写tryFire等操作来完成特有逻辑