- Java编程方法论:响应式RxJava与代码设计实战
- 知秋
- 2694字
- 2020-08-27 20:35:59
推荐序一
在Architecture and Design InfoQ Trends Report-January 2019(2019年1月的InfoQ架构和设计趋势报告)中,响应式编程(Reactive Programming)和函数式编程(Functional Programming)编列在第一季度(Q1)的Early Adopter(早期采纳者)中。尽管这仅是一家之言,但是不少开发人员逐渐意识到响应式之风已然吹起。也许你的生产系统尚未出现响应式的身影,不过你可能听说过Spring WebFlux或Netflix Hystrix等开源框架。笔者曾请教过Pivotal(Spring母公司)的布道师Josh Long:“Spring技术栈未来的重心是否要布局在响应式上?”对方的答复是:“没错,响应式是未来的趋势。”同时,越来越多的开源项目开始签署响应式宣言(The Reactive Manifesto)并喊出了“Web Are Reactive”的口号。
或许开源界的种种举动无法说服你向响应式的“港湾”中停靠,不过Java 9中Flow API的引入,又给业界注入了一剂“强心针”。不难看出,无论是Java API,还是Java框架,均走向了响应式编程模型的道路,这并非一种巧合。
通常,人们谈到的响应式可与响应式编程画等号,以“非阻塞(Non-Blocking)”和“异步(Asynchronous)”的特性并述,以数据结构与操作相辅相成。响应式编程涉及函数式和并发两种编程模型,前者关注语法特性,后者强调执行效率。简言之,响应式编程的意图在于“Less Code,More Efficient”。除此之外,笔者认为响应式更大的价值在于统一Java并发编程模型,使得同步和异步的实现代码无异,同时做到Java编程风格与其他编程语言更好地融合,或许你已经发现Java与JavaScript在响应式方面并不存在本质区别。纵观Java在响应式编程上的发展,其特性更新真可谓步步为营、如履薄冰。尽管Java线程API Thread与Runnable已具备异步以及非阻塞的能力,然而其同步和异步编程的模式并不统一,并且理解Thread API的细节和管理线程生命周期的成本均由开发人员承受。虽然在Java 5引入J.U.C框架(Java并发框架)之后,ExecutorService的实现减轻了以上负担,但是开发人员仍须关注ExecutorService的实现细节。比如,怎样合理地设置线程池空间及阻塞队列又成为新挑战。为此,Java 7引入了ForkJoinPool API,不过此时的J.U.C框架与响应式理念仍存在距离,即使是线程安全的数据结构也并不具备并行计算的能力(如集合并行排序),同时操作集合的手段也相当贫乏,缺少类似Map/Reduce等的操作。不过这些困难只是暂时的,终究会被Java 8“救赎”。Stream API的出现不仅具备数据操作在串行和并行间自由切换的能力(如sequential()及parallel()方法),而且淡化了并发的特性(如sorted()方法既可以进行传统排序,也可以进行并行排序)。相同的设计哲学也体现在Java响应式实现框架中,如本书中提及的RxJavaAPI io.reactivex.Observable。统一编程模型只是流的设计目标之一,它结合Lambda语法特性,虽然提供了数量可观的操作方法,如flatMap()方法等,但是无论对比RxJava,还是Reactor,流操作方法却又相形见绌。值得一提的是,这些操作方法在响应式的术语中被称为操作符(Operator)。当然框架内建操作符的多与少,并非判断其是否为响应式实现的依据。其中的决定性因素在于数据必须来自发布端(生产者)的“推送(Push)”,而非消费端的“拉取(Pull)”。显然,流属于消费端已就绪(Ready)的数据集合,并不存在其他数据推送源。不过JVM语言早期的响应式定义处于模糊地带,如RxJava API属于观察者模式(Observer Pattern)的扩展,而非迭代器模式(Iterator Pattern)的实现。而Reactor的实现则拥抱响应式流规范,该规范中消费端对于数据的操作是被动地处理,而非主动地索取。换言之,数据的到达存在着不确定性。当推送的数据无法得到消费端的及时响应时,响应式框架必须提供背压(Back Pressure)实现,确保消费端拥有“拒绝绝的权力(cancel)”。在此理论基础上,响应式流规范定义了一套抽象的API,作为Java 9中java.util.concurrent.Flow API的顶层设计。不过关于操作符的部分,该规范似乎不太关心,这也是RxJava和Reactor均称自身为响应式扩展框架的原因,同时两者在API级别提供了多种调度器(Scheduler)实现,可适配不同并发场景。尽管响应式定义在不同的阵营之间存在差异,但援引本人在《Reactive Programming:一种技术,各自表述》一文文中的总结:
Reactive Programming作为观察者(Observer)模式的延伸,不同于传统的命令编程(Imperative Programming)同步拉取数据的方式,如迭代器模式(Iterator),而是采用数据发布者同步或异步地推送到数据流(Data Stream)的方案。当该数据流(Data Stream)的订阅者监听到变化传播时,立即做出响应动作。在实现层面上,响应式编程可结合函数式编程简化面向对象语言语法的臃肿性,屏蔽并发实现的复杂细节,提供数据流的有序操作,从而达到提升代码的可读性以及减少Bug出现的目的。同时,响应式编程结合背压(Back Pressure)的技术解决了发布端生成数据的速度高于订阅端消费数据的速度的问题。
不难看出,响应式是一门综合的编程艺术,在实现框架的“加持”下,相同的代码逻辑可实现同步与异步非阻塞功能,从而达到提升应用整体性能的目的。不过现实的情况或许没有那么理想,Spring官方文档在“Web on Reactive Stack”章节中提到,“响应式和非阻塞通常并不会让应用运行得更快”:
Reactive and non-blocking generally do not make applications run faster.
为此,JHipster给出了一份Spring 5WebFlux性能测试报告,其中一条结论是“响应式应用并没有表现出速度的提升(甚至其速度变得更慢)”:
No improvement in speed was observed with our reactive apps(the Gatling results are even slightly worse).
数月后,看似相反的结论却在DZone的一篇名为Raw Performance Numbers - Spring Boot 2Webflux vs. Spring Boot 1的文章中出现,测试结果是Spring Boot 2WebFlux在高并发下下的响应时间更平稳。实际上,这个测试结果有些“关公战秦琼”的味道,毕竟Spring Boot 2下的WebFlux和Spring Boot 1下的Servlet容器所使用的线程模型是不同的,并且Servlet 3.0的异步以及非阻塞特性默认是关闭的。
不过以上两篇文章的结论并不矛盾,前者关注响应速度,后者则强调吞吐量,两者都是性能的核心指标。遗憾的是,这两篇文章均未对各自的测试用例做出调优,因此以上结论都存在一定的局限性,这也是笔者对响应式技术能否提升性能提出质疑的地方。
如果说笔者是国内提出响应式问题的第一人的话,那么知秋就是国内第一个解决响应应式问题的人。作为国内为数不多的响应式以及NIO方面的专家,在技术研究上,他追求格物致知,不轻易忽略技术细节;在知识分享上,他可谓知无不言,言无不尽,不仅在社交群中答疑解惑,而且录制免费视频,发布在B站以及YouTube频道上,并得到了Josh Long等“大牛”的推文(Twitter)。或许以上方式还不足以完整地讨论Java响应式技术,因此,知秋选择了漫长而又艰苦的著书之路。尽管他是笔者的朋友,然而“内举不避亲”,笔者郑重地向读者朋友推荐本书。这是中国大陆地区(不含港澳台地区)第一本全面解读Java响应式技术的书籍,作者的技术积累雄厚,书中的知识脉络循序渐进。同时,这也是一本引人深思的书,在进行源码导读的同时,也引导读者对代码设计进行思考。另外,这又是一本知识苦旅的书,因为它的涉及面较广,读者不仅需要具备一定的Java并发以及面向对象设计的基础,而且需要花费较多的时间去反复推敲书中的有关内容。正所谓“夫夷以近,则游者众;险以远,则至者少”,笔者希望读者在购买本书后,不轻言放弃,当你面临挑战时,那才是成长的开始。同时,笔者也期盼读者将响应式技术付诸实践,提早“触碰”未来。
小马哥(mercyblitz)
2019年3月5日