前面的章节我们讲了REST。本节,继续微服务专题的内容分享,共计16小节,分别是:
本节内容重点为:
Reactive 原理:理解 Reactive 本质原理,解开其中的奥秘
WebFlux 使用场景:介绍 WebFlux 与 Spring Web MVC 的差异,WebFlux 真实的使用场景
WebFlux 整体架构:介绍 WebFlux、Netty 与 Reactor 之间的关系,对于 Spring Web MVC 架构深入理解
关于Spring WebFlux的基本情况这里不再赘述,与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞, 并通过Reactor项目实现Reactive Streams规范。 并且可以在诸如Netty,Undertow和Servlet 3.1+容器的服务器上运行。
其中笔者挑选了以下三种出镜率最高的讲法:
Q:Reactive 是异步非阻塞编程(错误)
A:Reactive 是同步/异步非阻塞编程
Q: Reactive 能够提升程序性能
A:大多数情况是没有的,少数可能能会,参考测试用例地址:https://blog.ippon.tech/spring-5-webflux-performance-tests/
Q: Reactive 解决传统编程模型遇到的困境
A: 也是错的,传统困境不需,也不能被 Reactive
http://projectreactor.io/docs/core/release/reference/#_blocking_can_be_wasteful
将以上 Reactor 观点归纳如下,它认为:
当前不阻塞,事后来执行
通用问题
来一个Spring-Event的 DEMO 的感受一下:
public static void main(String[] args) {
// 默认是同步非阻塞
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
// 构建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 切换成异步非阻塞
multicaster.setTaskExecutor(executor);
// 增加事件监听器
multicaster.addApplicationListener(event -> { // Lambda 表达
// 事件监听
System.out.printf("[线程 : %s] event : %s\n",
Thread.currentThread().getName(), // 当前执行线程名称
event);
});
// 广播事件
multicaster.multicastEvent(new PayloadApplicationEvent("Hello,World", "Hello,World"));
// 关闭线程池
executor.shutdown();
}
Reactor 认为异步不一定能够救赎
再次将以上观点归纳,它认为:
CompletableFuture
能够提升这方面的不足demo1、串行测试:
public class DataLoader {
public final void load() {
long startTime = System.currentTimeMillis(); // 开始时间
doLoad(); // 具体执行
long costTime = System.currentTimeMillis() - startTime; // 消耗时间
System.out.println("load() 总耗时:" + costTime + " 毫秒");
}
protected void doLoad() { // 串行计算
loadConfigurations(); // 耗时 1s
loadUsers(); // 耗时 2s
loadOrders(); // 耗时 3s
} // 总耗时 1s + 2s + 3s = 6s
protected final void loadConfigurations() {
loadMock("loadConfigurations()", 1);
}
protected final void loadUsers() {
loadMock("loadUsers()", 2);
}
protected final void loadOrders() {
loadMock("loadOrders()", 3);
}
private void loadMock(String source, int seconds) {
try {
long startTime = System.currentTimeMillis();
long milliseconds = TimeUnit.SECONDS.toMillis(seconds);
Thread.sleep(milliseconds);
long costTime = System.currentTimeMillis() - startTime;
System.out.printf("[线程 : %s] %s 耗时 : %d 毫秒\n",
Thread.currentThread().getName(), source, costTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
new DataLoader().load();
}
}
public class ParallelDataLoader extends DataLoader {
protected void doLoad() { // 并行计算
ExecutorService executorService = Executors.newFixedThreadPool(3); // 创建线程池
CompletionService completionService = new ExecutorCompletionService(executorService);
completionService.submit(super::loadConfigurations, null); // 耗时 >= 1s
completionService.submit(super::loadUsers, null); // 耗时 >= 2s
completionService.submit(super::loadOrders, null); // 耗时 >= 3s
int count = 0;
while (count < 3) { // 等待三个任务完成
if (completionService.poll() != null) {
count++;
}
}
executorService.shutdown();
} // 总耗时 max(1s, 2s, 3s) >= 3s
public static void main(String[] args) {
new ParallelDataLoader().load();
}
}
demo2运行结果:
elastic与parallel性能对比测试:
关于串行与并行的理解:
并行+join:我们知道,在并发编程里,join()方法可以等待线程销毁,说白了,可以上多线程顺序执行,常见的CountDownLatch则是通过AQS -> (状态位、队列 Integer)效果实现顺序执行。
线程测试:
public static void main(String[] args) throws InterruptedException {
println("Hello,World 1");
AtomicBoolean done = new AtomicBoolean(false);
final boolean isDone;
// volatile 易变,线程安全(可见性)
// final 不变,线程安全(一直不变)
// final + volatile = impossible
Thread thread = new Thread(() -> {
// 线程任务
println("Hello,World 2020");
// CAS
done.set(true); // 不通用
});
thread.setName("sub-thread");// 线程名字
thread.start(); // 启动线程
// 线程 join() 方法
thread.join(); // 等待线程销毁
println("Hello,World 2");
}
private static void println(String message) {
System.out.printf("[线程 : %s] %s\n",
Thread.currentThread().getName(), // 当前线程名称
message);
}
关于Java 8 Lambda 表达式里使用boolean类型变量问题:通过我们在一个事件里使用标记位采用boolean,但是如果这个事件被lambda所封装,就不能简单的使用boolean,即使使用final修饰,所以在上面的demo里采用AtomicBoolean(线程安全)作为标记位。
至于说final volatile同时修饰为什么不行?原因很简单,因为两个关键字本身就是对立的:
volatile 易变,线程安全(可见性)
final 不变,线程安全(一直不变)
CompletableFuture
Future
的局限性
CompletableFuture
的功能列举
Future
链式操作public class CompletableFutureDemo {
public static void main(String[] args) {
println("当前线程");
// Reactive programming
// Fluent 流畅的
// Streams 流式的
CompletableFuture.supplyAsync(() -> {
println("第一步返回 \"Hello\"");
return "Hello";
}).thenApplyAsync(result -> { // 异步?
println("第二步在第一步结果 +\",World\"");
return result + ",World";
}).thenAccept(CompletableFutureDemo::println) // 控制输出
.whenComplete((v, error) -> { // 返回值 void, 异常 -> 结束状态
println("执行结束!");
})
.join() // 等待执行结束
;
}
private static void println(String message) {
System.out.printf("[线程 : %s] %s\n",
Thread.currentThread().getName(), // 当前线程名称
message);
}
}
上述代码采用的是命令编程方式(Imperative programming)
命令编程方式最大的特点是流程编排,其优势在于:
传统的编程模式:三段式编程
try {
// 1、业务执行
// action
} catch (Exception e) {
// 2、异常处理
// error
} finally {
// 3、执行完成
// complete
}
CompletableFutureDemo 运行结果为:
以上流程图为:
Consumer
Supplier
Function
Predicate
map
/reduce
/flatMap
来一个steam流式处理操作demo:
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) // 0-9 集合
.filter(v -> v % 2 == 1) // 判断数值->获取奇数
.map(v -> v - 1) // 奇数变偶数
.reduce(Integer::sum) // 聚合操作
.ifPresent(System.out::println) // 输出 0 + 2 + 4 + 6 + 8
以上操作是不是非常直观? 就是一次性的将数据处理完成!
其实不论Java/C#/JS/Python/Scale/Koltin语言,都在使用这种操作(Reactive/Stream模式)
Stream 是迭代器(
Iterator
)模式,数据已完全准备,拉模式(Pull)
Reactive 是观察者(Observer
)模式,来一个算一个,推模式(Push),当有数据变化的时候,作出反应(Reactor)
Reactive Programming 作为观察者模式的延伸,不同于传统的命令编程方式同步拉取数据的方式,如迭代器模式。而是采用数据发布者同步或异步地推送到数据流(Data Streams)的方案。当该数据流(Data Streams)定于这坚挺到传播变化时,立即做出响应动作。在实现层面上,Reactive Programming 可结合函数式编程简化面向对象语言语法的臃肿性,屏蔽并发实现的复杂细节,提供数据流的有序操作,从而达到提升代码的可读性,以及减少Bugs出现的目的。同时,Reactive Programming结合背压(Backpressure)的技术解决发布端生成数据的速率高于订阅端消费的问题。
先上一个demo:
public static void main(String[] args) throws InterruptedException {
Flux.just(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) // 直接执行
.filter(v -> v % 2 == 1) // 判断数值->获取奇数
.map(v -> v - 1) // 奇数变偶数
.reduce(Integer::sum) // 聚合操作
.subscribeOn(Schedulers.elastic())
// .subscribeOn(Schedulers.parallel())
// .block());
.subscribe(ReactorDemo::println) // 订阅才执行
;
Thread.sleep(1000);
}
private static void println(Object message) {
System.out.printf("[线程 : %s] %s\n",
Thread.currentThread().getName(), // 当前线程名称
message);
}
那么是否适合 RPC 操作?
Flux 和 Mono 是 Reactor 中的两个基本概念。
Mono
:单数据Optional
0:1, RxJava :Single
Flux
: 多数据集合,Collection
0:N , RxJava :Observable
同样,再举一个栗子,看看WebFlux 与SpringMVC的性能比较:
@RestController
public class WebFluxController {
@RequestMapping("")
public Mono<String> index() {
// 执行计算
println("执行计算");
Mono<String> result = Mono.fromSupplier(() -> {
println("返回结果");
return "Hello,World";
});
return result;
}
private static void println(String message) {
System.out.printf("[线程 : %s] %s\n",
Thread.currentThread().getName(), // 当前线程名称
message);
}
}
测试结果(这里使用WebFluxApplication作为启动类,并在浏览器访问http://localhost:8080/):
相对SpringMVC来说,WebFlux执行效率不见得比SpringMVC要快,只是WebFlux在多线程异步处理方面比较友好,使得其具有更好伸缩性,其底层还是基于并发编程。
使用场景
函数式编程
非阻塞(同步/异步)
远离 Servlet API
Servlet
HttpServletRequest
不再强烈依赖 Servlet 容器(兼容)
Tomcat
Jetty
实际上很多技术基于Reactor做了实现,Reactor真的可以引领下一代么?
Spring Cloud Gateway -> Reactor
Spring WebFlux -> Reactor
Zuul2 -> Netty Reactive
本节示例代码:https://github.com/harrypottry/microservices-project/tree/master/spring-reactive
更多架构知识,欢迎关注本套Java系列文章:Java架构师成长之路