做后端的架构师,应该对响应式架构这个概念不会陌生。
传统的Java结合Spring Boot,是主流的架构选择,这种属于同步式架构,同步式架构的最大特点就是,使用线程来处理并发。
同时,这也是同步式架构的一大弱点,因为这种模式有一些天然的缺点:
- • 服务器的线程是有限的,超过一定了限度的并发就会导致性能问题
- • 线程的创建与维护是有代价的,也就是需要占用CPU及内存,而资源是有限的。并发越高,线程越多,对资源的占用也就越高
由于这些缺点,于是衍生了与之不同的架构模式,那就是:响应式架构
一)
响应式架构,严格说来有很多特点与优点,但无疑其最大的一个特点就是异步,这也是它能区别于同步式架构的最大不同。同步式架构依靠的线程,执行程序都是同步的。
而响应式架构则所有执行都是异步的,非同步的。由于是异步的,因此不需要每个执行占用一个线程。这就使得它具有一个最大的优势,极高的性能。
如果以性能这个维度来对比,显然响应式架构是优胜者。
近些年来,从Spring WebFlux,Node.js,Vert.x等,许多响应式框架先后涌现,获得了不少的关注度。
我的myddd-vertx就是基于Kotlin与Vert.x构建的一个响应式基础框架,我这一二年也不断的对响应式架构进行研究与实践。
与最开始的热情相比,在一些实际的编码与调研之后,我认为我对响应式架构的热情已经大幅度冷却。
就我现在的认知来看,它远远没有预期的那么好。对一个公司或团队来说,我认为它甚至可能是一杯有毒的美酒。
所以我在这篇文章就聊一聊我现在认知。
二)
我先从异步的编程风格说起吧。
无论哪一种响应式框架或语言,其异步编码风格都离不开以下三种:
- • 回调式异步编码
- • 流式异步编码
- • await同步式异步编码
回调式异步编码
JavaScript或Node.js早些年就是这种风格,通过不断的回调来实现异步编码。Swift 5.5之前也是这种风格;
大致代码如下:
代码语言:javascript复制func makeSandwich(completionBlock: (result: Sandwich) -> Void) {
cutBread { buns in
cutCheese { cheeseSlice in
cutHam { hamSlice in
cutTomato { tomatoSlice in
let sandwich = Sandwich([buns, cheeseSlice, hamSlice, tomatoSlice]
completionBlock(sandwich))
}
}
}
}
}
没什么好说的,这种代码不可能谈得上简洁,优雅,完全不沾边。
因为这种代码过于难以阅读与维护,它获得了一个专有称呼:回调地狱。
流式异步编码
回调这种搞法,显然不太靠谱,不管你说它的性能多好都白搭。
所幸,异步编码并不只有回调一种风格;
流式异步风格是另一种异步编码风格,早些年非常火爆,比如RXJava等,就是这其中的杰出代表。
它的风格是这样的:
代码语言:javascript复制Observable.from(numbers)
.groupBy(i -> 0 == (i % 2) ? "EVEN" : "ODD")
.subscribe(group ->
group.subscribe((number) -> {
if (group.getKey().toString().equals("EVEN")) {
EVEN[0] = number;
} else {
ODD[0] = number;
}
})
);
assertTrue(EVEN[0].equals("0246810"));
assertTrue(ODD[0].equals("13579"));
如果你使用过Java 8的流,那它们大致一样。只不过异步流式风格比Java 8那个流强大的多,概念也多的多;
从概念上来说,流与异步非常契合,它们的搭配非常天然。
但就上面这个代码来说,可读性应该不会有人认为很好吧。流式风格可能没有回调那么差劲,但仍然属于学习曲线高,并且难以阅读。
对一部分数据进行流式处理在代码上是一种美,所有逻辑都是流式就谈不上美了,极大的提高了学习曲线并且不易阅读。
流式风格早些年非常火爆,这些年流行程度大幅度下降。类似RxJava一些框架的流行程度与前几年已无法相比。
StackOverflow指数 (RxJava)
Google搜索指数(RxJava)
await同步式风格
不管是回调风格,或是流式风格,代码的可读性都是个大问题。
异步来异步去,大家发现还是同步式的代码更简洁,易于阅读与维护;那怎么办呢,能不能代码又像同步式风格,又做到异步?
可以,于是出现了await同步式风格。
代码语言:javascript复制async func cutBread() -> Bread
async func cutCheese() -> Cheese
async func cutHam() -> Ham
async func cutTomato() -> Vegetable
async func makeSandwich() -> Sandwich {
let bread = await cutBread()
let cheese = await cutCheese()
let ham = await cutHam()
let tomato = await cutTomato()
return Sandwich([bread, cheese, ham, tomato])
}
这是最开始上面那个回调地狱代码的await同步风格。简洁性天差地别吧。
很多响应式框架,都有await这样的支持。比如Swift 5.5引进了await,JavaScript也引进了async/await,Vert.x结合Kotlin协程,也能做到await同步风格。
就代码易于阅读性和学习曲线来说,await同步式风格是响应式架构的救星。我的myddd-vertx
就是使用的await同步风格,我一度以为它能彻底解决异步的代码阅读性问题。
三)
如果又能做到同步风格,又实际是异步运行,那响应式架构确实不失为好的选择。
但一切并未如想像的那般美好,响应式架构仍然有着它内在的没能解决的问题。
下篇继续。