在java web开发领域,区别于传统的的同步服务架构(底层实现基于同步阻塞IO模型),异步服务这个“新词”(bushi)在不断被提及和重视,不少公司的研发部门也开始在尝试对自己的业务系统进行异步化改造。为了实现和简化异步服务的编程难度,新的编程模式被创造出来,reactive promgraming(中文被翻译成 响应式编程模式),对应的开源实现也有不少,RxJava 、ProjectReactor。基于这些Reactive框架实现的异步服务被称作Reactive 服务。(Ps: reactive 框架内部 定义出来了许多新的概念producer、 consumer、 subscription,一开始学习,真的让人非常头大,难以理解)
所以Reactive 服务属于异步服务的一实现方式,也就是说也可以基于其他技术(比如 java8 CompletableFuture 、guava ListenableFutue)实现异步服务。那问题就主要变成两个了:
显然我们更需要理解第一个问题,把握住主要矛盾。下面内容主要分享自己对异步服务的学习和理解,对于第二问题本人还在体会当中。
相关技术
反应堆模式(Reactor)
同步非阻塞,多工模式,一个事情可以分为几个步骤,每个步骤相应去做,同步串行先做A,后做B
Proactor模式
异步非阻塞,多工模式,A,B,C同时去做,异步去做。
观察者模式(Observer)
事件通知和监听的模式,也是一种推模式,由服务端推送到客户端。
迭代器模式(Iterator)
拉模式,服务端准备好数据,由客户端通过循环去获取。
Java并发模型
WebFlux的底层核心技术是Reactive,Reactive就是关于同步、异步、多工、或者设计模式的综合体。
Reactive技术的核心概念是数据流。数据流是由多个事件构成的异步数据序列,每个事件包括一个值和时间戳。可以理解为一个事件队列,事件的产生和处理都是异步的。
数据流可以是热数据流或冷数据流。热数据流是指在创建后就开始产生事件,即使没有订阅者也会一直产生事件,而冷数据流则是在订阅后才开始产生事件。在实际应用中,数据流的类型取决于应用场景和需求。
Reactive技术通过变化传递,实现数据流的响应式处理。变化传递是指,当数据流中的某个事件发生变化时,整个应用程序会相应地自动更新UI和其他相关组件,无需手动维护和同步。这种自动更新的方式有利于提高应用程序的响应性和性能。
变化传递是通过Observable、Observer、Subject等对象实现的。Observable是数据流的生产者,Observer是数据流的消费者,Subject既是Observable也是Observer,可以同时作为数据流的生产者和消费者。
Reactive技术可以处理异步交互,即在进行网络请求、文件读写等操作时,不会阻塞应用程序的其他部分,从而提高整个应用程序的响应性和性能。异步处理是通过Schedulers、Schedulers.Worker等对象实现的。Schedulers用于指定数据流的线程调度策略,Schedulers.Worker用于在指定线程上执行操作。
在Reactive技术中,各个组件之间是通过事件流进行通信的,这使得组件之间的耦合度更低,能够更加容易地进行模块化和复用,从而提高应用程序的可扩展性和可重用性。
Reactive技术的实现细节与具体的Reactive框架有关。以下以RxJava为例,介绍其实现细节。
Observable的创建方式有很多种,包括just()、from()、create()等。just()可以创建一个发送特定数据序列的Observable,from()可以将一个Iterable、Array或Future转换为Observable,create()可以自定义Observable的行为。
Observable需要通过subscribe()方法订阅,subscribe()方法接收一个Observer作为参数,Observer用于处理Observable发送的事件。
RxJava通过Schedulers对象实现线程调度。Schedulers提供了多种线程调度策略,例如Schedulers.io()表示在I/O密集型操作中使用的线程池,Schedulers.computation()表示在计算密集型操作中使用的线程池。
RxJava提供了多种操作符用于转换、过滤、合并等数据流操作。常见的操作符有map()、filter()、flatMap()等。
以上是Reactive技术的核心概念和实现细节的简要介绍,需要根据具体的Reactive框架进行深入学习和实践。
异步这个词在太多场合出现了,异步服务、java nio api、linux 异步IO模型等等... 。在不同场景下实际含义还有点区别的,导致很多人对java异步服务有错误的理解。我的理解是:异步服务是事件驱动的、基于linux多路复用io模型的web服务。
其实目前基于低版本servlet 规范构建的服务端在部分场景的网络IO已经做到非阻塞了,比如组织请求报文,但是由于低版本servlet 只支持同步处理请求(从方法签名就可以看出,当方法执行完后需返回响应报文),导致线程方面,仍然是 一个线程负责一个请求。如果业务逻辑涉及调用外部接口(rpc)和访问DB时,该线程必须阻塞等待它们返回结果。虽然说线程阻塞时会释放对CPU时间的占用,但是它对内存的占用却没有释放(比如线程的函数调用栈),核心矛盾就在这里。当我们接口的请求并发量暴增后,为了能正常处理每个请求,我们需要使用大量线程,业务逻辑计算的整个过程中,每个线程都一直占用一部分内存,这导致我们需要使用更多内存(硬件资源)。另外线程的切换、创建、释放同样需要占用硬件资源。
一句话来说,就是 目前使用多线程的传统方式处理高并发场景。对线程的使用不够高效,对硬件资源(尤其内存)依赖很大。异步服务则尝试更高效使用线程,使用更少的硬件资源处理更多的用户请求。
// 同步的服务端契约
Response handle(Request request);
// 异步的服务端契约
CompletableFuture handleAsync(Request request);
Mono handleAsync(Request request);
2. 异步化改造
异步服务通过少使用线程、以及减少对线程的阻塞,来实现目标。实际方法是要求 服务端以及 所依赖的外部网络服务(rpc调用、访问db)都使用 linux 多路复用 io模型 。
(1)服务入口改造
首先要改的就是handle方法签名,当前线程执行该方法后,返回的是Future。也就是说执行handleAync方法后,相当于提交了一个异步任务,而不是立即获得响应报文。随后我们在Future上在添加一个onSuccess callback方法,该方法主要逻辑是将响应报文发送出去。至此,当前线程执行完所有代码,结束其生命周期,释放所有内存资源。那我们只要保证线程在执行handleAsync内部代码时,没有发生过阻塞,那就是可以说高效率使用了线程。那如何保证这一点,那则需要做到第二点,业务逻辑依赖的所有外部服务必须提供类似的异步接口。
(2)构建全异步调用链路
假设业务逻辑依赖对DB的一次查询,DbService。如果handleAsync调用的是同步的query(),则当前线程会因为网络IO发生阻塞,则又回到原来传统模式,无法“解放”线程。所以我们需要调用的是异步查询的queryAsync。把后续的计算逻辑放到CompletableFuture
public class DbService{
// 同步查询
Entity query(long id);
// 异步查询
CompletableFuture queryAsync(long id);
}
我们使用了外部接口的异步方法,将业务逻辑放到 callback方法中,是异步服务的外在形象,那这些代码什么时候被执行,被什么线程执行,这些线程是否存在阻塞,则是我们要真正掌握的精神内核。
(3)linux 多路复用IO模型
接口的服务端和客户端都使用该模型来解放线程。linux io有关中系统调用 select、epoll等提供了单线程监听多个tcp 连接状态的能力,向select提交tcp连接,包括感兴趣的事件(可读、可写、建立连接成功等),以及事件到达时需要执行的动作。单线程运行的select查询所有连接状态时会阻塞,但是如果有任意一个事件到达时,select便会返回。此时可以起新的线程去执行事件到达的callback方法。这个网络协议的交互使用这种事件驱动的方式进行工作。
这里不详细介绍这种IO模型机理,但是可以看到该模式下,区别同步阻塞IO模型,只有少量线程存在阻塞,执行callback方法的线程生命周期短、不阻塞,实现了更高效的利用线程,对硬件资源的利用率更高。较少的线程就可以提供很大IO吞吐量。