响应式架构,也许只是杯有毒的美酒(上)

2022-11-18 14:46:09 浏览数 (1)

做后端的架构师,应该对响应式架构这个概念不会陌生。

传统的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同步风格,我一度以为它能彻底解决异步的代码阅读性问题。

三)

如果又能做到同步风格,又实际是异步运行,那响应式架构确实不失为好的选择。

但一切并未如想像的那般美好,响应式架构仍然有着它内在的没能解决的问题。

下篇继续。

0 人点赞