一文学会webflux基本原理

286 阅读23分钟

目标

  1. 什么是webflux
  2. 使用webflux时,有什么需要注意的吗?
  3. 怎麽样 理解reactor响应式编程?

本文代码地址

github.com/CHYhave/web…

IO模型

C10k

同一个时间并发1万个连接的请求,服务器是否扛得住?

BIO

在BIO中调用socket的读写方法都是阻塞的,需要等待操作系统将数据准备就绪才返回。比如在发起网络读写时,需要等网络对端将数据传输到本机socket文件中或者等待操作系统将数据写入socket文件后才返回,cpu的事件被大量浪费在io等待上。当cpu运行一个BIO的程序时,大量的时间被消耗在io等待上,对于CPU来说等同于空转,因此一般运行BIO的进程cpu利用率都不会太高。

如下代码所示是典型的BIO server端代码,对于每一个连接都需要新建一个线程。

public static void main(String[] args) {
    try (ServerSocket serverSocket = new ServerSocket()) {
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 8080));
        while (true) {
            Socket clientSocket = serverSocket.accept();
            new Thread(() -> {
                try {
                    process(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
public static void process(Socket clientSocket) throws IOException {
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = clientSocket.getInputStream().read(buffer)) != -1) {
        String request = new String(buffer, 0, bytesRead);
        System.out.println("Received: " + request);
        // 业务处理逻辑
        String response = "Processed: " + request;
        clientSocket.getOutputStream().write(response.getBytes());
        clientSocket.getOutputStream().flush();
    }
}

BIO线程模型

于是在面对C10K问题的时候,传统的BIO模型网络程序便捉襟见肘

  • 传统的bio模型对于每一个连接需要创建一个线程来处理该连接的读写请求。在C10K场景下,则需要创建10k=1w个线程来处理链接,而一个线程的内存开销大概是1MB, 光是线程本身的内存开销就达到了惊人的10GB!
  • 此外,1w线程对操作系统调度也是个灾难,大量的线程调度带来大量的上下文切换的开销,1次上下文切换的开销大约是1ms-10ms,现代服务器cpu的主频基本都在2GHZ以上,即1ms能够执行400,000条指令以上,大量的上下文切换意味着cpu资源被大量浪费。
  • 结合以上两点,可以想象随着连接/线程数量变多,大量的cpu时间被浪费在上下文切换与线程生命周期管控上,系统的吞吐量、性能也会随着线程变多下降。更别提在没有合理分配堆内存时的OOM风险。

BIO线程模型

NIO

在nio中调用read、write方法都是非阻塞立即返回的,用户程序需要自己关注socket上面发生的读写时间。可以简单的理解为在调用socket.write后立即返回了。

channel.write(databuffer);
// 立即返回,buffer中的数据被写到缓冲区
// 不一定落盘,需要手动flush刷盘

nio有两个关键的抽象:1. event 2. selector。

  • event是对socket上操作的抽象,比如读写时间,操作系统会监听这些事件的发生。
  • 当事件发生时,会将对应的事件绑定到响应的selector上,我们在调用selector#select方法时,就会返回事件与事件关联的socket channel

nio主要解决的是bio阻塞造成的cpu资源浪费,传统的nio server代码如下,运行单个线程轮询select上发生的事件,由于所有的io操作都是非阻塞的,你可以理解为cpu的核心在运行该线程时不会发生浪费,即没有阻塞等待(除了select操作)。在这种模式下,bio c10k问题中的线程数量得到了限制。

但是这种模式的nio也有问题,由于所有的连接都是在一个线程上运行(等于排队处理),在并发量大的情况下,系统整体的延迟会上升。

此外,该程序为单线程运行,没有很好地利用现代cpu多核的特性,浪费硬件资源。

public static void nio(){
    try {
        Selector selector = Selector.open();
        ServerSocket serverSocket = new ServerSocket();
        ServerSocketChannel channel = serverSocket.getChannel();
        channel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            selector.select(1000);
            Set<SelectionKey> keys = selector.keys();
            if (keys.isEmpty()) {continue;}
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel1 = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = channel1.accept();
                    clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (key.isReadable()) {
                    // process read data
                    // maybe register SelectionKey.OP_WRITE
                }
                if (key.isWritable()) {
                    // process write
                }
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

nio线程模型

reactor/netty

netty或者说reactor模型是对nio线程模型的优化,在这种模型下线程被分成了两类,acceptor线程和worker线程,这些线程又被统称为eventLoop(事件循环)。

  • acceptor线程会持有一个只关注accept事件的selector。
  • worker线程也会持有一个selector,关注read和write事件。
  • selector在接受到连接请求后,会将对应的client socket channel分配给一个worker,即在该worker的selector上注册读写事件监听,并摘除自身selector上对该channel的accept事件关注。

一个eventLoop线程的运行逻辑大致如下所示

for (;;) {
    if (!hasTask()) {
        select.select(); // 任务优先,io延后
    }
    processSelectKey(); // 处理io任务
    runTask();  // 处理非io的任务
}

一般情况下acceptor线程数量为1,worker数量等于cpu核心数量。 为什么会这样设计呢?其实思想还是比较简单的,得益于非阻塞的特性,eventLoop可以以非阻塞的方式一直运行,理论上当cpu线程运行eventLoop线程时,利用率是100%。这时候过多的线程(大于cpu核心数)带来的上下文切换反而会造成性能下降,造成不必要的线程上下文切换的开销。(redis为什么是单线程的?)

回顾一下在使用java线程池时核心线程数量的设计:cpu密集型=1+core,io密集型=2*core,可以理解在非阻塞io下eventLoop执行的任务等同于cpu密集型的。

reactor线程模型

可以说reactor就是java在没有协程的情况下面对c10k问题的终极答案:

  • 解决bio线程数量随着请求增长造成的内存增长和上下文切换
  • 解决传统单线程nio对cpu资源利用效率不高(worker数量=cpu核心数量)

但是在使用reactor模型时也有一些注意事项:

  • 禁止在worker线程中执行阻塞操作
  • 不建议在worker线程中执行长耗时任务

怎么理解呢?

  1. 假设在eventloop线程中发起了阻塞操作,此时该eventlopp上关联的其他channel也被阻塞了,需要等待当前阻塞任务完成后才被执行(单线程)。由于worker线程的数量等于cpu核心数量,此时reactor性能还不如传统的bio线程模型。
  2. 第二个问题实际上与第一个问题相同,只不过这里执行的长耗时任务是非阻塞的,同样长耗时的任务也会阻塞eventloop,造成系统吞吐量下降,延迟上升。

综上所述,在使用reactor模型时,为了保证高吞吐量,我们需要确保每个请求在eventloop线程中占用的时间都尽量很短暂。比较好的解决方式是将阻塞操作以及长耗时任务投递到其他线程池中执行,避免阻塞eventloop(dubbo)。

private final static int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private final static Thread[] EVENT_LOOP = new Thread[CPU_COUNT];
private final static Selector[] SELECTORS = new Selector[CPU_COUNT];
static {
    try {
        for (int i = 0; i < CPU_COUNT; i++) {
            SELECTORS[i] = Selector.open();
            int finalI = i;
            EVENT_LOOP[i] = new Thread(() -> {
                while (true) {
                    try {
                        SELECTORS[finalI].select(1000);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    Set<SelectionKey> keys = SELECTORS[finalI].keys();
                    if (keys.isEmpty()) {continue;}
                    for (SelectionKey key : keys) {
                        if (key.isReadable()) {
                            // process read data
                            // maybe register SelectionKey.OP_WRITE
                        }
                        if (key.isWritable()) {
                            // process write
                        }
                    }
                }
            });
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
public static void reactor(){
    try (ServerSocket serverSocket = new ServerSocket()) {
        Selector selector = Selector.open();
        ServerSocketChannel channel = serverSocket.getChannel();
        channel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            selector.select(1000);
            Set<SelectionKey> keys = selector.keys();
            if (keys.isEmpty()) {continue;}
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel1 = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = channel1.accept();
                    int index = clientChannel.hashCode() % CPU_COUNT;
                    clientChannel.register(SELECTORS[index], SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Webflux-reactor编程范式

Netty虽然已经很好了,但是作为一名web开发直接使用netty是不是门槛有点过高了?不仅仅要了解netty的架构设计,还需要自己手动实现netty的encoder-decoder,解决拆包粘包问题,还需要管理堆外内存,心智负担也太高了把!

在java生态中,有没有一种框架能够降低我们的使用学习成本,并且完美地融入Spring地生态?答案就是webflux,接下来让我们看看webflux是如何把netty和reactor编程范式融合在一起地。

发布订阅模式

在了解webflux的基本设计前,有必要先了解下要发布订阅设计模型,传统的发布订阅设计模型可以被分为拉模型和推模型。

   --------------   event     --------------
   |  publisher |  ------->   | subscriber |
   --------------             --------------

推模型

   --------------   push      --------------
   |  publisher |  ------->   | subscriber |
   --------------             --------------

一个典型的推模型实现如下所示,这样的模型存在几个问题:

  1. onEvent方法是同步的,当前执行推送的线程需要等待所有的subscriber处理完事件后,才能够继续执行其他逻辑。
  2. 当然可以把onEvent方法,或者说订阅者消费逻辑异步化,那就会引出其他的问题:
  • 消费者线程池管理
  • 消费者线程池阻塞队列管理
  • 线程上下文切换带来的开销

当然推模型也具备优势:实时性好,生产者刚生产消息就推送给消费者了。

public class Publisher {
    private List<Subscriber> subscriberList;
    
    public void publishEvent(Event event) {
        for (Subscriber subscriber: subscriberList) {
            subscriber.onEvent(event);
        }
    }
}

拉模型

   --------------    pull     --------------
   |  publisher |  <-------   | subscriber |
   --------------             --------------

一个典型的拉模型实现如下所示,subscriber本身是个线程,通过一个阻塞队列和生产者产生关联,生产者不断投递事件到队列,消费者则不断轮询队列来消费。拉模型的优势:

  1. publisher和subscriber异步解耦
  2. 能够在一定程度上控制系统负载

拉模型也存在一定的问题:

  • 生产者和消费者速率不匹配,容易造成任务堆积。
  • 生产者慢时或者没有消息可生产时,容易造成消费者空轮询,浪费cpu资源
public class Subscriber implements Runnable{
    private BlockingQueue<Event> queue;
    
    public void onEvent(Event event) {
        queue.offer(event);
    }
    
    public void run() {
        while (true) {
            Event event = queue.take(1000);
            if (event == null) {
                continue;
            }
            // process event
        }
    }
}

推拉模型

通过上述对推模型和拉模型了解,可以看到不论是推模型还是拉模型都存在一定的问题。并且一般都需要更多复杂的设计来保证真个消费系统的可靠性,并通过多线程来实现异步提高吞吐量。

那么有没有一种设计能够保证结合推模型和拉模型的优势呢?答案是推拉结合模型。

整体的设计是引入了一个subscription对象作为中介桥梁,subscriber需要通过subscription完成pull操作,publisher通过subscription完成对subscriber的推送。

  • 当消费者有消费欲望时,通过subscription向publisher拉消息;
  • publisher在收到拉请求时,通过subscription推送消息。
   
   -------------      pull       ----------------     pull   --------------
   |           |   <----------   |              | <--------- |            |
   | publisher |                 | subscription |            | subscriber |
   |           |   ---------->   |              | ---------> |            |
   -------------      push       ----------------     push   --------------
   

上面的设计看起来可能有点抽象,我们通过一个demo快速的理解下这种设计的优势

数据流转

  1. 调用publisher#subscribe方法,创建subscription,并触发subscriber#onSubscribe方法
  2. onSubscribe方法中调用subscription#request (pull-拉)
  3. subscription#request中调用publisher#produceData方法 (pull-拉)
  4. publisher#produceData完成生产后调用subscription#deliverData方法 (push-推)
  5. subscription#deliverData调用subscriber#onNext方法 (push-推)
  6. subscriber#onNext完成消费后,继续回调subscription#request进入下一轮循环
public class PublisherAndSubscriber1 {
    public static void main(String[] args) throws InterruptedException {
        MyPublisher publisher = new MyPublisher();
        MySubscriber subscriber = new MySubscriber(1);
        publisher.subscribe(subscriber);
    }
    static class MyPublisher implements Publisher<Integer> {
        Iterator<Integer> iterator;
        {
            List<Integer> list = List.of(1, 2, 3, 4, 5);
            iterator = list.iterator();
        }
        @Override
        public void subscribe(Subscriber<? super Integer> subscriber) {
            MySubscription subscription = new MySubscription(subscriber, this);
            subscriber.onSubscribe(subscription);
        }
        public void produceData(MySubscription subscription, long n) {
            for (int i = 0; i < n; i++) {
                if (!iterator.hasNext()) {
                    subscription.cancel();
                    return;
                }
                subscription.deliverData(iterator.next());
            }
        }
    }
    static class MySubscription implements Subscription {
        private final Subscriber<? super Integer> subscriber;
        private final MyPublisher publisher;
        private volatile boolean cancelled = false;
        public MySubscription(Subscriber<? super Integer> subscriber,
                              MyPublisher publisher) {
            this.subscriber = subscriber;
            this.publisher = publisher;
        }
        @Override
        public void request(long n) {
            if (cancelled) return;
            publisher.produceData(this, n);
        }
        public void deliverData(Integer data) {
            if (!cancelled) {
                subscriber.onNext(data);
            }
        }
        @Override
        public void cancel() {
            cancelled = true;
        }
    }
    static class MySubscriber implements Subscriber<Integer> {
        private Subscription subscription;
        private int requested = 1;
        public MySubscriber() {}
        public MySubscriber(int requested) {
            this.requested = requested;
        }
        @Override
        public void onSubscribe(Subscription s) {
            this.subscription = s;
            s.request(this.requested);
        }
        @Override
        public void onNext(Integer item) {
            System.out.println(item);
            subscription.request(this.requested);
        }
        @Override
        public void onError(Throwable throwable) {
        }
        @Override
        public void onComplete() {
        }
    }
}

乍一看这种推拉模型似乎像是脱裤子放屁,相较于推拉模式没有特别明显的优势,只不过多了个subscription中介。

别急,先接着往下看。

假设有需求场景为:某接口在收到请求后从DB中捞出一堆数据,发布者将这些数据交给不同的消费者,数据的处理逻辑可以异步。

  • 推模式:消费者可以异步执行这些任务,但是发布者需要一次性捞出数据。假设某个时间段该接口请求量很大,大量的数据可能被载入到内存,可能会有oom的风险。
  • 拉模式:生产者将数据投递到消费队列就返回,消费者轮询队列消费。假设消费者消费的很慢,容易出现消息堆积的问题,同样容易引发oom的问题。如果限制了消息队列的大小,还需要额外实现拒绝策略。
  • 推拉模式:消费者可以自己决定没批次的数据量,生产者是惰性的,只有在消费者需要时才生产,杜绝oom的风险。

但是这里还有一个问题,我们的推拉结合订阅模型本质上还是一个同步模型,在生产者阻塞时仍然有推模式同样的问题。如果采用线程池等方式并发,最终的解决方案与原本的推模型和拉模型无异

为了解决上述的问题,我们需要进一步改造代码,来实现一种与线程数量无关的并发推拉结合订阅模式。

生产者阻塞

首先我们来模拟下生产者阻塞生产,代码如下

 public void produceData(MySubscription subscription, long n) {
     for (int i = 0; i < n; i++) {
         if (!iterator.hasNext()) {
             subscription.cancel();
             return;
         }
         try {
             Thread.sleep(1000);
         } catch (InterruptedException e) {}
         subscription.deliverData(iterator.next());
     }
 }

并且有多个生产者消费者(并发),为了方便后面的代码改造,这里把发布订阅放入一个单线程的线程池执行。

static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static void main(String[] args) throws InterruptedException {
    executorService.execute(() -> {
        MyPublisher publisher = new MyPublisher();
        MySubscriber subscriber = new MySubscriber(1);
        publisher.subscribe(subscriber);
    });
    executorService.execute(() -> {
        MyPublisher publisher2 = new MyPublisher();
        MySubscriber subscriber2 = new MySubscriber(1);
        publisher2.subscribe(subscriber2);
    });
}

执行代码,得到如下结果。结果符合预期,两个发布订阅是串行执行,目前的效果和普通的推模型是一致的。

1
2
3
4
5
1
2
3
4
5
10082

如果消费者也是阻塞的呢?

@Override
public void onNext(Integer item) {
    try {
        //模拟消费阻塞
        Thread.sleep(500);
    } catch (InterruptedException e) {}
    System.out.println(item);
    subscription.request(this.requested);
}

执行代码,得到如下结果。

1
2
3
4
5
1
2
3
4
5
15201
非阻塞式生产/消费者

通过上面的案例我们可以看到,推拉结合模型的问题是:

  • 如果生产者阻塞了,模型的性能就退化为推模型。
  • 如果消费者阻塞了,模型的性能就退化为拉模型。

ok,我们先来解决问题 1,如果我们的生产者是非阻塞的,那岂不是就没有问题1了!

那java中有什么办法能让因为iIO阻塞的接口变成非阻塞呢?

NIO!

// 新建一个线程池,用于模仿NIO无阻塞回调
static ExecutorService nioCallbackExecutorService = Executors.newCachedThreadPool();


// Publisher
public void produceData(MySubscription subscription, long n) {
    for (int i = 0; i < n; i++) {
        // 模拟nio非阻塞回调
        nioCallbackExecutorService.execute(() -> {
            if (!iterator.hasNext()) {
                subscription.cancel();
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            subscription.deliverData(iterator.next());
        });
    }
}


// Subscription
public void deliverData(Integer data) {
    // 消费的逻辑还是在原本的线程中
    executorService.execute(() -> {
        if (!cancelled) {
            subscriber.onNext(data);
        }
    });
}

生产者生产的慢不一定是因为IO阻塞,还可能因为本身的计算就有这么多耗时,比如执行一个o(n^k)(k >= 2, n是一个很大的数)的任务,这时候的解决方案,只能通过并行化生产来提高生产者的吞吐量。

static ExecutorService parallelTaskExecutorService = Executors.newCachedThreadPool();
public void produceData(MySubscription subscription, long n) {
    // 注意这里的逻辑和上一小节的不一样
    // 上一小节我们使用线程池模拟nio非阻塞回调
    // 而这里,后续生产消费的链路将被调度到parallelTaskExecutorService去执行
    parallelTaskExecutorService.execute(() -> {
        for (int i = 0; i < n; i++) {
            if (!iterator.hasNext()) {
                subscription.cancel();
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            subscription.deliverData(iterator.next());
        }
    });
        
}

假设我们在消费者中的代码逻辑如下:1. 调用了阻塞方法获取数据 2. 使用数据进行一些非阻塞计算

可以这样认为1中的阻塞方法 就是一个生产者,后续的计算行为2是消费逻辑

static class AnotherPublisher extends MyPublisher {
    Supplier<Integer> supplier;
    public AnotherPublisher(Supplier<Integer> supplier) {
        this.supplier = supplier;
    }
    @Override
    public void subscribe(Subscriber<? super Integer> subscriber) {
        MySubscription subscription = new MySubscription(subscriber, this);
        subscriber.onSubscribe(subscription);
    }
    public void produceData(MySubscription subscription, long n) {
        for (int i = 0; i < n; i++) {
            nioCallbackExecutorService.execute(() -> {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                Integer result = supplier.get();
                subscription.deliverData(result);
            });
        }
    }
}
static class AnotherSubscriber extends MySubscriber {
    Runnable runnable;
    public AnotherSubscriber(Runnable runnable) {
        this.runnable = runnable;
    }
    @Override
    public void onNext(Integer item) {
        executorService.execute(runnable);
    }
}

修改MySubscriber的onNext消费方法

@Override
public void onNext(Integer item) {
    AnotherPublisher anotherPublisher = new AnotherPublisher(new Supplier<Integer>() {
        @Override
        public Integer get() {
            return item * item;
        }
    });
    AnotherSubscriber anotherSubscriber = new AnotherSubscriber(() -> {
        System.out.println(name + ":" +item);
        countDownLatch.countDown();
        subscription.request(this.requested);
    });
    anotherPublisher.subscribe(anotherSubscriber);
}
编码优化

上面的编码存在问题:

  1. 代码重用性低,需要频繁实现publisher和subscriber
  2. 代码入侵严重,我们需要在subscriber和publisher的核心代码中实现生产和消费的细节
  3. 缺乏类型转换的能力,可能链路中数据类型发生变化
  4. 级联订阅时,subscriber每一次onNext都会新建新的subscriber和publisher,可能会触发频繁的ygc
  5. ....

为此我们要改进我们的代码,希望:

  1. publisher的代码是稳定的,不希望数据生产和publisher代码耦合
  2. subscriber的代码是稳定的,不希望消费逻辑和subscriber的代码耦合
  3. 链路中应该尽量少的new对象,并且在某批次的消费中,链路中的核心对想是固定的
  4. 支持链路中途数据类型的转换

重构后代码目录结构如下

名为Flux的Publisher

Flux是Publisher的实现

public class Flux<T> implements Publisher<T> {
    private final Iterable<T> source;
    public static <T> Flux<T> just(T... items) {
        List<T> list = Arrays.asList(items);
        return new Flux<>(list);
    }
    public static <T> Flux<T> just(List<T> list) {
        return new Flux<>(list);
    }
    @Override
    public void subscribe(Subscriber<? super T> subscriber) {
        Iterator<T> it = source.iterator();
        FluxSubscription<T> subscription = new FluxSubscription<>(it, subscriber);
        subscriber.onSubscribe(subscription);
    }
}

BaseSubscriber接受一个consumer对象作为消费动作的实现。

public class BaseSubscriber<T> implements Subscriber<T> {
    Subscription s;
    Consumer<T> consumer;
    int request = 1;
    @Override
    public void onSubscribe(Subscription s) {
        this.s = s;
        s.request(request); // 只请求一个
    }
    @Override
    public void onNext(T t) {
        consumer.accept(t);
        s.request(request); // 这里必须有
    }
}

FluxSubscription则是连接subscriber和publisher的桥梁,代理了Flux中的元素的迭代器。

public class FluxSubscription<T> implements Subscription {
    private final Iterator<T> iterator;
    private final Subscriber<? super T> subscriber;
    private volatile boolean cancelled = false;
    private volatile boolean completed = false;

    public FluxSubscription(Iterator<T> iterator, Subscriber<? super T> subscriber) {
        this.iterator = iterator;
        this.subscriber = subscriber;
    }

    @Override
    public void request(long n) {
        if (cancelled || completed) return;
        for (long i = 0; i < n; i++) {
            if (cancelled) return;
            if (iterator.hasNext()) {
                T t = iterator.next();
                subscriber.onNext(t);
            } else {
                completed = true;
                subscriber.onComplete();
                return;
            }
        }
    }
}
在链路中支持map操作

在Flux中增加map方法,该方法会新建一个MapFlux

public <R> Flux<R> map(Function<? super T, ? extends R> mapper) {
    return new MapFlux<>(this, mapper);
}

MapFlux的参数为:

  1. 原本的flux
  2. 一个mapper function用于参数转换映射

重点在subscribe方法中,新建了一个MapSubscriber对象,并将原本的subscriber作为参数传入

public class MapFlux<T, R> extends Flux<R> {
    private final Flux<T> source;
    private final Function<? super T, ? extends R> mapper;
    public MapFlux(Flux<T> source, Function<? super T, ? extends R> mapper) {
        super(null); // super的source参数可为null或保留
        this.source = source;
        this.mapper = mapper;
    }
    @Override
    public void subscribe(Subscriber<? super R> subscriber) {
        source.subscribe(new MapSubscriber<>(subscriber, mapper));
    }
}

MapSubscriber的核心在于onSubscribe和onNext方法

  1. 在onSubscribe方法中,通过调用原本的subscribe#OnSubscribe将原始的flux与subscriber连接
  2. onNext方法则代理了原本的订阅者的onNext方法
public class MapSubscriber<T, R> implements Subscriber<T> {
    private final Subscriber<? super R> actual;
    private final Function<? super T, ? extends R> mapper;
    private Subscription upstream;
    private volatile boolean cancelled = false;
    public MapSubscriber(Subscriber<? super R> actual, Function<? super T, ? extends R> mapper) {
        this.actual = actual;
        this.mapper = mapper;
    }
    @Override
    public void onSubscribe(Subscription s) {
        this.upstream = s;
        actual.onSubscribe(new Subscription() {
            @Override
            public void request(long n) {
                upstream.request(n);
            }
            @Override
            public void cancel() {
                cancelled = true;
                upstream.cancel();
            }
        });
    }
    @Override
    public void onNext(T t) {
        if (!cancelled) {
            try {
                R r = mapper.apply(t);
                actual.onNext(r);
            } catch (Throwable e) {
                actual.onError(e);
            }
        }
    }
}

结合代码案例来看一下

第一次map新建了一个MapFlux

调用subscribe,将subscribe 传递给MapFlux,MapFlux新建了一个MapSubscriber作为原本subscriber的装饰

最后执行代码逻辑如下

MapSubscriber#onNext -> BaseSubscriber#onNext -> 匿名Subscription#request -> FluxSubscription#request -> MapSubscriber#onNext

Flux.just(list)
    .map(i -> {
        try {
            Thread.sleep(500); // 模拟IO阻塞
        } catch (InterruptedException e) {
        }
        return i * 10;
    })
    .subscribe(
    new BaseSubscriber<>(t -> {
        System.out.println("onNext:" + t);
    }, 1)
);

可以理解为MapSubscriber和原本的Subscriber之间形成了一个栈,先进后出,当存在多个map操作重叠时候形成了如下的调用链路。其中FirstMapSubscriber是flux最后调用的map方法产生,以此往上递推。

FirstMapSubscriber#onNext -> SecondMapSubscriber#onNext -> ThirdMapSubscriber#onNext -> ..... ->BaseSubscriber#onNext -> 匿名Subscription#request -> FluxSubscription#request -> FirstMapSubscriber#onNext -> ...

Flux.just(list)
    ...
    .map(i -> {
        System.out.println("ThirdMap");
        return i;
    })
    .map(i -> {
        System.out.println("SecondMap");
        return i;
    })
    .map(i -> {
        System.out.println("FirstMap");
        return i;
    })
    .subscribe(
        new BaseSubscriber<>(t -> {
            System.out.println("onNext:" + t);
        }, 1)
    );
);
非阻塞IO

我们原本的阻塞方法是在map的操作中,只需要把mapper#apply放到线程池中,并回调原本的消费逻辑到主线程,就完成了类似nio非阻塞的模拟。

MapSubscriber#onNext

@Override
public void onNext(T t) {
    ioPool.execute(() -> {
        if (!cancelled) {
            try {
                R r = mapper.apply(t);
                eventLoop.execute(() -> actual.onNext(r));
            } catch (Throwable e) {
                eventLoop.execute(() -> actual.onError(e));
            }
        }
    });
}

我们上面实现的发布订阅模式仅支持单线程运行,当存在多个发布订阅关系时,可以利用现代cpu多核特性,将发布订阅分布在不同的cpu上并行计算,在java中实际就是多线程。

这里我们可以将线程数量设置为cpu的个数,理由可以参考netty。

public static final ExecutorService eventLoop = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), runnable -> new Thread(runnable, "eventloop-" + EventLoopCounter.getAndIncrement()));

webflux=reactor+netty

一段典型的reactor编程范式如下,是不是有点熟悉。reactor编程范式的核心就是上一章节中推拉结合的发布订阅模式。

public static void main(String[] args) {
    Flux.just(1, 2, 3, 4, 5)
        .filter(i -> i % 2 != 0)
        .map(value -> value * value)
        .flatMap(value -> Flux.just(value * value, value * value * value))
        .subscribe(System.out::printlan);
}

而webflux则是利用了reactor这种编程范式能够很好和nio结合的特性,利用netty开发的网络框架。

webflux api设计

publisher

在webflux中publisher只有两个Mono和Flux,Mono是一个发布单个元素的publisher,而Flux是发布非确定个数元素的publisher。

Mono<String> mono1 = Mono.just("Hello");
mono1.map(str -> str + " world!")
    .subscribe(System.out::println);
Flux.just(1, 2, 3)
    .flatMap(i -> Flux.just(i, i * i))
    .subscribe(System.out::println);

相关api

创建操作符
  1. 冷启动创建
public class CreationOperators {
    
    public void demonstrateCreation() {
        // 1. 静态数据创建
        Flux<String> staticData = Flux.just("A", "B", "C");
        Mono<String> singleValue = Mono.just("Hello");
        
        // 2. 从集合创建
        List<Integer> list = Arrays.asList(1, 2, 3);
        Flux<Integer> fromIterable = Flux.fromIterable(list);
        
        // 3. 范围创建
        Flux<Integer> range = Flux.range(1, 100);  // 1到100
        
        // 4. 时间序列创建
        Flux<Long> interval = Flux.interval(Duration.ofSeconds(1));
        
        // 5. 程序化创建
        Flux<String> generated = Flux.generate(
            () -> 0,  // 初始状态
            (state, sink) -> {
                sink.next("Item-" + state);
                if (state == 10) sink.complete();
                return state + 1;
            }
        );
        
        // 6. 异步创建
        Flux<String> created = Flux.create(sink -> {
            // 异步推送数据
            for (int i = 0; i < 5; i++) {
                sink.next("Data-" + i);
            }
            sink.complete();
        });
    }
}

2. 热启动创建

public class HotPublishers {
    
    public void demonstrateHotPublishers() {
        // 1. ConnectableFlux - 手动控制订阅时机
        ConnectableFlux<Integer> connectableFlux = Flux.range(1, 5)
            .delayElements(Duration.ofSeconds(1))
            .publish();  // 转换为热发布者
        
        // 多个订阅者
        connectableFlux.subscribe(i -> System.out.println("订阅者1: " + i));
        connectableFlux.subscribe(i -> System.out.println("订阅者2: " + i));
        
        connectableFlux.connect();  // 开始发布
        
        // 2. 主题发布者
        EmitterProcessor<String> processor = EmitterProcessor.create();
        FluxSink<String> sink = processor.sink();
        
        // 发布数据
        sink.next("消息1");
        sink.next("消息2");
    }
}
转换操作符
public class TransformationOperators {
    
    public void demonstrateTransformations() {
        Flux<String> source = Flux.just("hello", "world", "reactor");
        
        // 1. map - 1对1转换
        Flux<String> upperCase = source.map(String::toUpperCase);
        
        // 2. flatMap - 1对多异步转换
        Flux<String> characters = source
            .flatMap(word -> Flux.fromArray(word.split("")));
        
        // 3. concatMap - 有序的1对多转换
        Flux<String> orderedChars = source
            .concatMap(word -> Flux.fromArray(word.split("")));
        
        // 4. switchMap - 切换到最新的流
        Flux<String> switched = source
            .switchMap(word -> Flux.interval(Duration.ofMillis(100))
                .map(i -> word + "-" + i)
                .take(3));
        
        // 5. cast & ofType - 类型转换
        Flux<Object> objects = Flux.just("string", 1, 2.0);
        Flux<String> strings = objects.ofType(String.class);
        
        // 6. index - 添加索引
        Flux<Tuple2<Long, String>> indexed = source.index();
        
        // 7. timestamp - 添加时间戳
        Flux<Tuple2<Long, String>> timestamped = source.timestamp();
    }
}
过滤操作符
public class FilteringOperators {
    
    public void demonstrateFiltering() {
        Flux<Integer> numbers = Flux.range(1, 20);
        
        // 1. filter - 条件过滤
        Flux<Integer> evenNumbers = numbers.filter(n -> n % 2 == 0);
        
        // 2. take系列 - 取前N个
        Flux<Integer> firstFive = numbers.take(5);
        Flux<Integer> takeWhile = numbers.takeWhile(n -> n < 10);
        Flux<Integer> takeLast = numbers.takeLast(3);
        Flux<Integer> takeUntil = numbers.takeUntil(n -> n > 15);
        
        // 3. skip系列 - 跳过前N个
        Flux<Integer> skipFirst = numbers.skip(5);
        Flux<Integer> skipWhile = numbers.skipWhile(n -> n < 10);
        Flux<Integer> skipLast = numbers.skipLast(3);
        Flux<Integer> skipUntil = numbers.skipUntil(n -> n > 10);
        
        // 4. distinct - 去重
        Flux<Integer> duplicates = Flux.just(1, 2, 2, 3, 3, 3);
        Flux<Integer> unique = duplicates.distinct();
        Flux<Integer> distinctKey = duplicates.distinctUntilChanged();
        
        // 5. sample - 采样
        Flux<Long> sampled = Flux.interval(Duration.ofMillis(100))
            .sample(Duration.ofSeconds(1));  // 每秒采样一个
    }
}
聚合操作符
public class AggregationOperators {
    
    public void demonstrateAggregation() {
        Flux<Integer> numbers = Flux.range(1, 10);
        
        // 1. 基础聚合
        Mono<Integer> sum = numbers.reduce(0, Integer::sum);
        Mono<Integer> max = numbers.reduce(Integer::max);
        Mono<Long> count = numbers.count();
        Mono<Boolean> hasElements = numbers.hasElements();
        
        // 2. collect - 收集到集合
        Mono<List<Integer>> list = numbers.collectList();
        Mono<Map<Boolean, List<Integer>>> grouped = numbers
            .collectMap(n -> n % 2 == 0);  // 按奇偶分组
        
        // 3. buffer - 分批
        Flux<List<Integer>> batches = numbers.buffer(3);  // 每3个一批
        Flux<List<Integer>> timedBatches = numbers
            .delayElements(Duration.ofMillis(100))
            .buffer(Duration.ofSeconds(1));  // 按时间分批
        
        // 4. window - 分窗口
        Flux<Flux<Integer>> windows = numbers.window(3);
        
        // 5. groupBy - 分组
        Flux<GroupedFlux<Boolean, Integer>> grouped2 = numbers
            .groupBy(n -> n % 2 == 0);
        
        grouped2.subscribe(group -> {
            group.collectList().subscribe(items -> 
                System.out.println("Group " + group.key() + ": " + items));
        });
    }
}
合并操作符
public class CombinationOperators {
    
    public void demonstrateCombination() {
        Flux<String> flux1 = Flux.just("A", "B", "C");
        Flux<String> flux2 = Flux.just("1", "2", "3");
        Flux<String> flux3 = Flux.just("X", "Y", "Z");
        
        // 1. merge - 合并(无序)
        Flux<String> merged = Flux.merge(flux1, flux2, flux3);
        
        // 2. concat - 连接(有序)
        Flux<String> concatenated = Flux.concat(flux1, flux2, flux3);
        
        // 3. zip - 配对组合
        Flux<Tuple2<String, String>> zipped = Flux.zip(flux1, flux2);
        Flux<String> zipWith = flux1.zipWith(flux2, (a, b) -> a + b);
        
        // 4. combineLatest - 最新值组合
        Flux<String> combined = Flux.combineLatest(
            flux1.delayElements(Duration.ofMillis(100)),
            flux2.delayElements(Duration.ofMillis(150)),
            (a, b) -> a + "-" + b
        );
        
        // 5. startWith / concatWith
        Flux<String> withPrefix = flux1.startWith("PREFIX");
        Flux<String> withSuffix = flux1.concatWith(Flux.just("SUFFIX"));
        
        // 6. switchOnFirst - 根据首个元素切换策略
        Flux<String> switched = flux1.switchOnFirst((signal, flux) -> {
            if ("A".equals(signal.get())) {
                return flux.map(String::toLowerCase);
            }
            return flux.map(String::toUpperCase);
        });
    }
}
错误处理
public class ErrorHandlingOperators {
    
    public void demonstrateErrorHandling() {
        Flux<String> errorProneFlux = Flux.just("1", "2", "error", "4")
            .map(s -> {
                if ("error".equals(s)) {
                    throw new RuntimeException("处理错误");
                }
                return s.toUpperCase();
            });
        
        // 1. onErrorReturn - 错误时返回默认值
        Flux<String> withDefault = errorProneFlux
            .onErrorReturn("DEFAULT");
        
        // 2. onErrorResume - 错误时切换到另一个流
        Flux<String> withFallback = errorProneFlux
            .onErrorResume(error -> {
                System.err.println("发生错误: " + error.getMessage());
                return Flux.just("FALLBACK1", "FALLBACK2");
            });
        
        // 3. onErrorMap - 转换错误类型
        Flux<String> mappedError = errorProneFlux
            .onErrorMap(RuntimeException.class, 
                e -> new IllegalStateException("转换后的错误", e));
        
        // 4. onErrorContinue - 跳过错误元素继续处理
        Flux<String> skipErrors = errorProneFlux
            .onErrorContinue((error, item) -> {
                System.err.println("跳过错误元素: " + item + ", 错误: " + error.getMessage());
            });
        
        // 5. retry - 重试机制
        Flux<String> withRetry = errorProneFlux
            .retry(3)  // 重试3次
            .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));  // 退避重试
        
        // 6. doOnError - 错误时执行副作用
        Flux<String> withLogging = errorProneFlux
            .doOnError(error -> System.err.println("记录错误: " + error));
    }
}
时间操作符
public class TimingOperators {
    
    public void demonstrateTiming() {
        Flux<String> source = Flux.just("A", "B", "C", "D");
        
        // 1. delay - 延迟
        Flux<String> delayed = source.delayElements(Duration.ofSeconds(1));
        Flux<String> delayedStart = source.delaySubscription(Duration.ofSeconds(2));
        
        // 2. timeout - 超时控制
        Flux<String> withTimeout = source
            .delayElements(Duration.ofSeconds(2))
            .timeout(Duration.ofSeconds(1))  // 1秒超时
            .onErrorReturn("TIMEOUT");
        
        // 3. elapsed - 测量间隔时间
        Flux<Tuple2<Long, String>> withElapsed = source
            .delayElements(Duration.ofMillis(100))
            .elapsed();
        
        // 4. throttle - 限流
        Flux<String> throttled = source
            .delayElements(Duration.ofMillis(100))
            .throttleFirst(Duration.ofSeconds(1));  // 每秒最多1个
        
        Flux<String> debounced = source
            .delayElements(Duration.ofMillis(100))
            .throttleLast(Duration.ofSeconds(1));   // 防抖
    }
}
调度操作符
public class SchedulerOperators {
    
    public void demonstrateScheduling() {
        Flux<String> source = Flux.just("A", "B", "C");
        
        // 1. subscribeOn - 指定订阅的调度器
        Flux<String> subscribedOn = source
            .subscribeOn(Schedulers.boundedElastic())  // IO操作线程池
            .map(s -> {
                System.out.println("订阅线程: " + Thread.currentThread().getName());
                return s.toLowerCase();
            });
        
        // 2. publishOn - 指定后续操作的调度器
        Flux<String> publishedOn = source
            .publishOn(Schedulers.parallel())  // 并行计算线程池
            .map(s -> {
                System.out.println("处理线程: " + Thread.currentThread().getName());
                return s.toUpperCase();
            });
        
        // 3. 调度器类型
        Scheduler immediate = Schedulers.immediate();      // 当前线程
        Scheduler single = Schedulers.single();           // 单线程
        Scheduler parallel = Schedulers.parallel();       // CPU密集型
        Scheduler boundedElastic = Schedulers.boundedElastic(); // IO密集型
        Scheduler fromExecutor = Schedulers.fromExecutor(
            Executors.newFixedThreadPool(4));  // 自定义线程池
    }
}
背压操作符
public class BackpressureOperators {
    
    public void demonstrateBackpressure() {
        Flux<Integer> fastProducer = Flux.range(1, 1000);
        
        // 1. onBackpressureBuffer - 缓冲策略
        Flux<Integer> buffered = fastProducer
            .onBackpressureBuffer(100,  // 缓冲100个
                BufferOverflowStrategy.DROP_OLDEST);
        
        // 2. onBackpressureDrop - 丢弃策略
        Flux<Integer> dropped = fastProducer
            .onBackpressureDrop(item -> 
                System.out.println("丢弃: " + item));
        
        // 3. onBackpressureLatest - 保留最新策略
        Flux<Integer> latest = fastProducer.onBackpressureLatest();
        
        // 4. onBackpressureError - 错误策略
        Flux<Integer> error = fastProducer.onBackpressureError();
        
        // 5. limitRate - 限制请求速率
        Flux<Integer> limited = fastProducer
            .limitRate(10)      // 每次最多请求10个
            .limitRequest(100); // 总共最多100个
    }
}
订阅操作符
public class SubscriptionOperators {
    
    public void demonstrateSubscription() {
        Flux<String> source = Flux.just("A", "B", "C");
        
        // 1. 简单订阅
        source.subscribe();  // 仅订阅
        source.subscribe(System.out::println);  // 消费数据
        source.subscribe(
            System.out::println,           // onNext
            System.err::println,           // onError  
            () -> System.out.println("完成") // onComplete
        );
        
        // 2. 带订阅控制
        Disposable disposable = source.subscribe(
            System.out::println,
            System.err::println,
            () -> System.out.println("完成"),
            subscription -> subscription.request(2)  // 只要2个
        );
        
        // 3. 阻塞操作(仅用于测试)
        String first = source.blockFirst();           // 获取第一个
        String last = source.blockLast();             // 获取最后一个
        List<String> all = source.collectList().block(); // 获取所有
        
        // 4. 转换为其他类型
        CompletableFuture<String> future = source.last().toFuture();
        Stream<String> stream = source.toStream();
        Iterable<String> iterable = source.toIterable();
    }
}
副作用操作符

不会对后续的流产生影响的操作符

public class SideEffectOperators {
    
    public void demonstrateDoOnOperators() {
        Flux<String> source = Flux.just("A", "B", "C")
            .map(String::toLowerCase);
        
        Flux<String> withSideEffects = source
            // 1. 订阅生命周期
            .doOnSubscribe(subscription -> {
                System.out.println("🔔 订阅开始: " + subscription);
                System.out.println("📊 当前线程: " + Thread.currentThread().getName());
            })
            .doOnRequest(n -> {
                System.out.println("📞 请求 " + n + " 个元素");
            })
            
            // 2. 数据流生命周期  
            .doOnNext(item -> {
                System.out.println("📦 处理数据: " + item);
                // 常用于:数据验证、缓存更新、指标收集
                updateMetrics(item);
                logDataAccess(item);
            })
            .doOnComplete(() -> {
                System.out.println("✅ 流完成");
                // 常用于:资源清理、完成通知、统计上报
                notifyCompletion();
                cleanupResources();
            })
            .doOnError(error -> {
                System.err.println("❌ 流出错: " + error.getMessage());
                // 常用于:错误记录、告警、降级处理
                logError(error);
                sendAlert(error);
            })
            
            // 3. 取消和终止
            .doOnCancel(() -> {
                System.out.println("🚫 订阅被取消");
                // 常用于:资源清理、取消统计
                cleanupOnCancel();
            })
            .doOnTerminate(() -> {
                System.out.println("🔚 流终止 (完成或错误)");
                // 在onComplete或onError之前执行
                recordEndTime();
            })
            .doAfterTerminate(() -> {
                System.out.println("🏁 流终止后处理");
                // 在onComplete或onError之后执行
                finalCleanup();
            })
            .doFinally(signalType -> {
                System.out.println("🎯 最终处理, 信号类型: " + signalType);
                // 无论如何都会执行:完成、错误、取消
                finalStatistics();
            });
            
        withSideEffects.subscribe();
    }
    
}
public class AdvancedSideEffects {
    
    public void demonstrateAdvancedDoOn() {
        Flux<Integer> numbers = Flux.range(1, 10);
        
        numbers
            // doOnEach - 对每个信号执行副作用
            .doOnEach(signal -> {
                if (signal.isOnNext()) {
                    System.out.println("📨 接收数据信号: " + signal.get());
                } else if (signal.isOnError()) {
                    System.out.println("📨 接收错误信号: " + signal.getThrowable());
                } else if (signal.isOnComplete()) {
                    System.out.println("📨 接收完成信号");
                }
            })
            
            // doOnDiscard - 当元素被丢弃时执行
            .filter(n -> n % 2 == 0)  // 过滤后,奇数会被丢弃
            .doOnDiscard(Integer.class, discarded -> {
                System.out.println("🗑️ 丢弃元素: " + discarded);
                // 常用于:资源释放、统计丢弃数量
            })
            
            .subscribe();
    }
}