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

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

我一直在思考,究竟什么才算得上响应式架构?

按照响应式架构官方的定义,响应式架构(或称反应式架构),在2014年的时候,有一个自己的宣言,它宣称自己的架构的特点是Responsive(即时响应性),Resilient(回弹性),Elastic(弹性),Message Driven(消息驱动)。这似乎是一个很完美的架构。

在实际的响应式架构实现中,基本都会应用到消息以及异步编程,消息到还好,它是提升性能及服务间解耦的绝佳方式,但究竟什么时候开始,异步编程似乎也成为了响应式架构的本质特征,在谈论响应式架构时,现在似乎不太可能离得开RxJava,Spring WebFlux或Vert.x这一类的异步框架。

当然,基于Responsive的极高要求,使用异步编程是更佳的,它在性能上确实表现的更为出色。但我也在思考,难道不用异步编程的架构,就做不到上面这些特点了?

一个架构,服务内部使用同步式,服务间按需使用异步或消息,这样的微服务或分布式架构,难道不能实现Responsive,Resilient,Elastic以及Message Driven,算不算响应式架构?

一)

还是继续回到异步编程中来吧,在以异步编程为核心的响应式架构中,其中做为架构师,你最首先要询问的一个问题就是:

在你设计的架构中,性能与代码的简洁及软件的可维护性两者,究竟哪个是更重要的?


响应式架构有很多优点,其中最重要的一点是性能,因为使用异步编程实现,在性能上占有优势。

但这不是没有代价的,不存在没有代价的优势。任何一个优势背后,一定同时意味着它在另一方面不具备优势。

响应式架构及其背后的核心的异步编程,最重要的一个缺点就是:

一定程度上牺牲了代码的简洁性以及损害了软件的可维护性

来看几份代码。

同步式代码(摘自myddd-spring-boot)

代码语言:javascript复制
    public User createLocalUser(){
        if(Strings.isNullOrEmpty(name))throw new UserNameEmptyException();
        if(Strings.isNullOrEmpty(userId))throw new UserIdEmptyException();

        checkEmailAndPhone(UserType.LOCAL);

        if(Strings.isNullOrEmpty(password))this.password = DEFAULT_PASSWORD;

        this.userType = UserType.LOCAL;
        this.created = System.currentTimeMillis();
        this.encodePassword = getPasswordEncoder().encodePassword(password);
        return getUserRepository().save(this);
    }

这是一份同步式代码,就算没有任何注释,但几乎任何人都能很清晰简单的理解与阅读它。

这是同步式代码的优势,只要通过适当的原则,重构或方式,你可以让它非常简洁,优雅,易于理解与阅读。

而易于理解与阅读的代码,是软件可维护的必要前提条件之一。

异步式代码(hibernate reactive官方的示例

代码语言:javascript复制
            factory.withSession(
                    session -> session.find( Author.class, author2.getId() )
                            .chain( author -> fetch( author.getBooks() )
                                    .invoke( books -> {
                                        out.println( author.getName()   " wrote "   books.size()   " books" );
                                        books.forEach( book -> out.println( book.getTitle() ) );
                                    } )
                            )
            )
                    .await().indefinitely();

这个逻辑其实不复杂,但你阅读起来会觉得有些吃力了吧。如果更复杂的逻辑,有更多的流处理,再叠加错误处理,阅读与理解起来只会更困难。

这就是流式异步风格的缺点,它降低了可阅读性。

异步式代码(myddd-vertx)/ await风格

再看一份,await风格的响应式代码

代码语言:javascript复制
suspend fun resetPassword(id:String):Future<Unit>{
  val exists = repository.get(User::class.java,id).await()
  if(Objects.isNull(exists))throw UserNotFoundException()
  
  exists!!.password = DEFAULT_PASSWORD
  repository.save(exists).await()
  
  return Future.succeededFuture()
 }

是不是变得清晰很多?

为什么,因为它虽然是异步编程,但是它使用了同步式的风格,可阅读性立马变得清晰很多了。对吧?

二)

好,那接下来的一个问题是,能不能都用上await这种风格。鱼与熊掌兼得?

行,也不行!

因为要分开来说

JavaScript/TypeScript是可以轻松做到的

由于JavaScript这语言从诞生起,就是单线程异步的(现在有worker多线程机制,但它只是一种特别需求时的额外补充),这种代码风格问题是它一出生就不得不面临和需要改善的难点。

为了解决回调地狱的问题,JavaScript在语言级别就引进了async/await这种语法特性。无论是语言本身,还是第三方生态,都是支持这个风格的。也就是你如果用的是JavaScript或TypeScript,你完全可以使用这种同步风格。

代码语言:javascript复制
    public static async queryContact(userId: string): Promise<Contact | null> {
        let contact: Contact = await this.getRespository().queryContact(userId);
        if (!contact) {
            contact = await this.getNet().fetchContact(userId);
            if (contact) {
                this.getRespository().save(contact);
            }
        }
        return contact;
    }

没有理解上的困难吧。我在使用TypeScript时,全是这种风格,我感觉不出和以前编写同步的Java代码有多大的差别。

后端Java/Kotlin难以做到

但可惜的是,Java也好,Kotlin也罢,由于主要就是同步式编码,压根就没有这种难以阅读的风格问题。

所以,在这些语言背后,响应式架构,主要依赖的是RxJava或Mutiny这样的第三方类库来支持。

而这些不同类库表现出以下特点:

  • • 以流式风格为主,没有见到或很少有await这样的特性
  • • 并且各自流式风格API也不统一,各有各的API

我的myddd-vertx是借助vert.x Kotlin协程,才有await这种特性,能写出同步风格的代码。

但这很难完美,因为你会需要用第三方框架或类库,比如myddd-vertx中用上了hibernate reactive,而hibernate reactive则使用的是Mutiny,再假设下你再用上某个框架用的是RxJava。

于是,更尴尬的场景就出现了,你得在不同的风格中切换来切换去。本来就不好理解的流式异步,面临更加难以学习与阅读的处境了。

所以,对于后端的响应式框架来说,很难完全做到await这种风格。

当前Java的几个主流响应式类库,Spring WebFlux,RxJava或是Vert.x都做不到(Vert.x Kotlin除外,但它无法解决第三方类库的问题)

这就意味着,使用响应式框架时,你实质上是做了一个选择:

你看中了它的性能或其它优势,而选择牺牲掉了代码的可读性与软件的可维护性

三)

这并不是说,异步编程就一定可维护性差;同步编程就一定可维护性佳;所有编程都是人的活动,最终依赖的是人。

但是,从统计学上来说,异步编程风格的确更难以理解,阅读,大部分程序员学习不好,也掌握不了。当然会有足够优势的程序员能够驾驭它,但你要想下你是不是这样的,以及你能否组建一支这样的团队。

同样,这也并不是说异步编程就没有价值,不值得考虑,这也是大错特错的。

今天,微服务,分布式架构成为趋势,云原生日益火爆,而这些架构中,异步是非常重要的一个特性。没有异步,不同服务之间的交互很难高效。异步是当下架构的一种必须。

但怎么恰当的使用异步,考验的是架构师你的能力了。

因为异步高效,所以从前到后,从上到下全用异步,这并不是理智的行为,异步应该用在足够需要它的地方。

四)

回到最开始的问题,究竟性能与可维护性,你认为哪一个更重要?我当前的认知是:

性能是可以轻易弥补的,而可维护性可能是无法挽救的

性能不足,无非是添加更多的硬件,依靠集群或更多的服务节点来改善与缓解,这是花小钱能解决的(硬件越来越便宜)

但与之对应的是,那些可维护性差的代码,会面临一种无论如何加班,如何增加新的程序员,花多大的精力都解决不了问题的处境。

这种处境,应该很多程序员都遇到过,都深有体会。

我曾亲身经历一个产品,最终花了几个月时间,没有添加任何新的功能,仅仅是不停的修改BUG,每天统计Bug修复数量,但没能挽救产品,最终以重写来解决问题。

我也听说一个项目,开发人员日夜加班,办公桌下就有床,随时睡在公司以解决进度延迟的问题。这种项目的结果做为程序员你我都能猜到。

这种体验,想必很多程序员可能都经历过。

在编程的世界中,可维护性,是一种失去了,就可能难以再找回来的特性。

这只是响应式架构我认为其中最重的一个也最值得考量的一个缺点,但这并不是全部。

响应式架构究竟需要付出哪些成本,又能有哪些收益?我们可以来算一笔帐。

下一篇再聊。

0 人点赞