使用WebFlux进行响应式编程(using webflux in reactive programming)

上一篇文章《理解响应式编程(reactive programming)》我们谈到响应式编程四个核心概念: 发布者(The Publisher): 发布者就是数据的生产者,这个是为系统生产数据的组件,这里的服务B就是一个发布者,他收到服务A的请求后就开始生产数据。 订阅者(The Subscriber): 订阅者订阅发布者生产的数据。这里服务A就是订阅者,他订阅来自服务B的数据。 订阅过程(The Subscription):订阅过程是一份服务之间的合约(contract),它被用于订阅者获取数据,或者取消订阅。 处理者(The Processor):处理者是一个响应式实体,他能够消费发布者的数据,并进行再加工,然后发布自己的数据,上面的例子并未体现这层逻辑。举个例子,排序处理者,他可以作为订阅者获取发布者的随机序数据,然后进行排序,然后作为发布者生产出排序后的数据。 一个典型的响应式交互如下: 订阅者和发布者签订订阅契约(Publisher.subscribe),一旦契约签订完成,订阅者向发布者请求数据(Subscription.request),发布者准备好数据后传输数据给订阅者(调用订阅者onNext),订阅者再次请求新数据(request),直到发布者告诉订阅者数据已经发送完成(onComplete),本次契约完成。 Reactive REST Appplication 现在我们使用WebFlux开始构建一个简单的响应式应用. 使用一个简单的领域模型-Employee 使用RestController返回发布者生产的数据 使用WebClient构建订阅者获取发布者数据 WebFlux Maven依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> 发布者(Publisher) WebFlux底层使用Project Reactor,所以我们这里使用Project Reactor的命名来介绍。 发布者有两个类型: Mono类型表示生产0个或者至多1个数据 Flux类型表示生产多个数据 示例代码分为三层分别是: controller-获取REST请求,并返回订阅数据 model-包含Employee领域模型 repository-构建内存数据库(采用HashMap) controller: 可以看到方法返回类型为Mono或者Flux,表示生产者会生产对应类型个数的数据 @RestController @RequestMapping("/employees") public class EmployeeController { private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class); private final EmployeeRepository employeeRepository; public EmployeeController(EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; } @GetMapping("/{id}") private Mono<Employee> getEmployeeById(@PathVariable String id) { logger.debug("getEmplyeeById controller get called"); return employeeRepository.findEmployeeById(id); } @GetMapping private Flux<Employee> getAllEmployees() { logger.debug("getAllEmployees controller get called"); return employeeRepository.findAllEmployees(); } @PostMapping("/update") private Mono<Employee> updateEmployee(@RequestBody Employee employee) { return employeeRepository.updateEmployee(employee); } } model: 很简单的领域模型Employee包含id和name两个字段 ...

March 21, 2022

理解响应式编程(reactive programming)

过去十年互联网用户数呈指数级增长,各类网络服务访问数量也随之持续增长。为了应对持续增长的访问需求,各种技术被重新赋予了新的活力。微服务,DDD,响应式编程等技术被重新改进用于应对以上问题。本文着重讨论响应式编程背后的原理,帮助读者理解并应用于实际的开发中。 响应式宣言 说到响应式编程首先要引入一个概念响应式系统(Reactvie Systems)。回到2013年Jonas Boner领导的开发团队提出了响应式宣言,其中定义了响应式系统一系列核心原则.主要描述了该系统应具备灵活性,松耦合以及可扩展性.原则描述了响应式系统的基础特性: 可响应性:一个响应式系统应提供快速和一致的响应时间,以及一致的服务质量 可复原性:一个响应式系统在随机失败的情况下,通过复制和隔离能力保持响应 可伸缩性:一个响应式系统在不可预测的负载下,通过经济的可扩展性保持响应 消息驱动:系统组件之间应通过异步消息机制进行通信 响应式编程要解决什么问题 在非响应式同步调用的系统中两个服务是怎么调用的呢?假设我们有A,B,2个服务,当服务A调用服务B后(request),服务B开始处理收到的请求,这个时候服务A的线程会被阻塞住(idle),等待服务B处理完成后返回响应给服务A(respond),这时服务A线程被唤起继续处理接下来的逻辑。 这是我们常用的同步调用方式,他最大的问题是会使资源经常处于idle状态,没有充分利用我们的资源,对资源浪费很大。例如服务A调用服务B的线程在调用后就被阻塞住,不能做其他的事情,直到服务B响应为止。我们知道系统的线程要占用CPU周期,内存等硬件资源,并且极其有限。 那么响应式编程的目标就是解决资源浪费问题,最高效地使用资源。这里我们看到服务A线程调用服务B后,并未等待服务B处理完成,便开始处理其他逻辑,所以服务B的单个线程可以不断调用服务B。那么服务B也可以不间断的收到大量的请求进行处理,这里A,B两个服务都高效的利用了资源。 响应式编程背后的原理 这怎么实现呢?当服务A调用服务B的时候,AB服务之间建立了一个订阅通道,通道建立好以后,服务A线程就去处理其他逻辑,等服务B处理完请求准备好数据后,便会通知服务A数据已经准备好了,这个时候服务A会有一个独立的线程池去获取服务B的数据,这样就实现了同步调用/异步响应的调用方式。 这里有几个响应式编程重要的核心概念: 发布者(The Publisher): 发布者就是数据的生产者,这个是为系统生产数据的组件,这里的服务B就是一个发布者,他收到服务A的请求后就开始生产数据。 订阅者(The Subscriber): 订阅者订阅发布者生产的数据。这里服务A就是订阅者,他订阅来自服务B的数据。 订阅过程(The Subscription):订阅过程是一份服务之间的合约(contract),它被用于订阅者获取数据,或者取消订阅。 处理者(The Processor):处理者是一个响应式实体,他能够消费发布者的数据,并进行再加工,然后发布自己的数据,上面的例子并未体现这层逻辑。举个例子,排序处理者,他可以作为订阅者获取发布者的随机序数据,然后进行排序,然后作为发布者生产出排序后的数据。 背压机制(Backpressure Mechanism) 前面说到响应式编程包含发布者生产数据,订阅者订阅数据,很自然想到当发布者生产数据的速度和订阅者消费数据的速度不匹配的问题,特别是快于订阅者能消费数据的速度,这时系统就是出现问题,订阅者就会被过量的数据淹没。 响应式编程是为了高效的利用系统资源,总不能把系统服务打垮了吧。这时我们在设计系统的时候会引入背压机制来控制发布者和订阅者之间的平衡。 一般我们通过三种策略来调控速率: 控制发布者发送数据的速率(推荐) 订阅者使用缓存来存储暂时无法处理的数据 订阅者丢弃所有无法处理的数据 为了达到系统的高效运行,通过背压机制,使发布者和订阅者速率达到平衡,也就是根据消费能力来按需生产和发送数据,生产多少,就消费多少,用前面的A,B服务的例子来说明: 服务A给服务B发送一个request请求1个数据,服务B的生产完成1个数据的时候,就通知服务A(onNext)有个数据就绪了,服务A就以同样的速率处理数据,这样服务B生产速率就被服务A限制,A,B两个服务就工作在同样的速率,这样系统效率达到最佳。 在这里讲完了响应式编程的核心原理,下一篇《使用WebFlux进行响应式编程》会继续深入讨论响应式编程在代码中的实现。

March 20, 2022

代码抽象性与依赖性

我们常说代码除了满足功能需求以外,还应该满足以后的可读性,可测性,可扩展性,可维护性等。我们常常看到两种类型的团队,一类团队软件开发流程里面只有开发+测试,常常处于加班状态,不断的赶新功能上线。另一类团队有完善的软件开发过程,迭代开发,同行评审,单元测试,自动化测试等。主观觉得第二类团队的代码质量应该比较高,可我们对这两类团队的代码到底怎么样,除了一般常用的sonar,findbugs等静态扫描工具的数据,就没太多的了解。除非深入学习业务逻辑,并剖析源代码。 代码的抽象性,决定了代码以后是否容易扩展,抽象性高的代码易于通过继承的方式进行扩展,抽象性低的代码更容易出现复制-粘贴的扩展方式。代码被别的类依赖多,导致代码不容易变化,反之代码可变性就很高。如果从这两个方面考虑代码的设计,就提供了更多的维度了解团队代码的健康度。 我们以模块为基本单位,统计整个系统每个模块的抽象性和依赖性: 代码抽象性 Nc:模块内类的数量 Na:模块内抽象类和接口的数量 A:代码抽象性 A = Na / Nc 那么这个值的区间是[0,1] 代码依赖易变性 Fan-in:进入的依赖。模块外有多少个类依赖于该模块内部类。 Fan-out:出去的依赖。模块内有多少个类依赖于模块外部的类。 代码依赖易变性: I = Fan-out/(Fan-in + Fan-out) 值区间[0,1] 举个例子: 上图Fan-in=3, Fan-out=1 模块依赖易变性 I=1/(3+1) 如果我们把代码抽象性和依赖性放入到二维坐标轴,x坐标是代码依赖易变性,Y坐标是代码的抽象性。然后在图里面最高抽象性(左上角)到最高抽象易变性(右下角)划一条对角线,这条线叫主道线。 我们在上图可以看到三个区域1.痛苦区 2.无用区 3.主道线区 处在痛苦区的模块里面类的抽象性不足,同时极度的稳定,依赖易变性低。这种模块可扩展性不足因为抽象性低,同时被多个模块依赖,不能轻易改变。如果要扩展这个模块的功能,就会产生大量的复制-粘贴重复代码。有一个例外就是基础工具模块,这类模块的特征就是处于极度稳定,并不需要抽象性,所以这类工具模块处于这个区间是合理的。 处在无用区的模块里面类的抽象性很高,但没有类去使用所以依赖易变性也很高。这个区间的模块包含大量的抽象类和接口,但没有外部模块使用它,所以变成了无用的模块。 结论是我们期望一个系统大部分的模块都处于主道线区,不要偏离主道线太远。如果一个模块的二维坐标到主道线的距离过远比如达到0.5,那么这个模块值得打开深入分析里面的类的抽象性与依赖性是否合理。 这里我写了一个工具可以分析java代码的抽象性与依赖性供参考: GitHub

August 10, 2021

研发大猜想

经历了太多乱七八糟的产品和项目,听到了很多有趣的猜想和假设,我把他称之为"研发大猜想" 猜想1:这个系统架构和代码太烂了,我们重新做个2.0系统吧,前面的问题就都解决了 感觉好像新做的2.0代码就不会和1.0系统一样烂了,结果就是和以前一样前期赶需求,后期集中测试,什么设计,什么内建质量都是太浪费时间,最后搞了大半年搞出了跟1.0一模一样的代码烂泥。怎么办喃,还可以3.0嘛。 “如果不改变做事的方式,永远都只能做出一样的系统” 猜想2:因为业务要的太急,所以我们没有时间写单元测试,没有时间做代码评审 听着很有道理,就好像不那么忙的时候,他们就会写单元测试了。实际上他们从来没写过单元测试,也永远不会去写。因为从思想上就没有理解什么是合格的代码。一个合格的程序员交付给测试人员的代码应该是很难再测出问题,测试人员花大量的努力都无法验伪的程序。这个才是一个刚刚合格的程序,因为这方面仅仅是功能无问题,还要涉及设计的合理性,抽象性,耦合性,代码可测性等代码结构事宜。 “一个合格的程序员,应该有这样的品质:自己写的代码,应该在功能上很难验伪,在设计上保持代码健康。” 猜想3:业务两周就要一个版本,连回归测试的时间就不够,我们干脆把版本周期加长,然后搞几个前后并行的版本,这样人处于最忙状态,研发就最快了 在集成测试阶段开发人员有空闲,所以下个并行版本要进入到开发阶段。最好在第三个并行版本进入到需求分析阶段,这样大家就看起来都很忙了。下面的人保持最忙的状态,才显得上面的人员指挥恰当。 结果就是搞需求的时候,还要同时搞开发,搞缺陷,搞开发的时候还要同时搞上版本缺陷修复,每个事情都没做好。结果是需求没有搞清楚,开发设计一团乱,缺陷一大堆,就这样往复循环。但每个人都很忙,真的很忙。 “好好体验一下,做好每件事情,大脑处于专注流的状态” 猜想4:一个功能需求写了很多复杂逻辑,各种场景覆盖,产品经理的价值体现就在能把事情考虑周全 写产品需求是很枯燥的事情,不是某个领域熟悉的人,却想写出解决这个领域问题的产品需求,还要能读懂使用者最终怎么用这个产品,同时还想产品功能面面俱到。遇到这样的产品经理,研发团队是痛苦的,平白的做了大量的复杂业务逻辑。最后用户认为功能难用,不能解决问题,推翻重做,产品经理美其名曰“我在试错”。这样的产品经理从来考虑不清楚,这些功能的价值是什么,能解决什么问题。拍脑袋写大量的文档是他们最喜欢的事情,为什么呢?因为不用思考。 殊不知就算你熟悉这个领域,也不该一开始设计复杂的功能逻辑。简单即是美,往往是简单的功能才能解决用户问题。 “优秀的产品经理真的很稀有” 猜想5:项目会有紧急需求,遇到紧急需求的项目要加人,我的项目总是缺人,要多补充人,就能把事情完成 人能解决所有的问题,人多我就什么都能搞。不了解沟通的耗费,不了解优秀程序员和劣质能写hello world 人的区别,不了解往进行中项目加人的影响。只想通过加人解决问题。问题真的是人不够吗? 你见过谁说我的项目人太多了,需求太少的事情吗? “问题的本质真的很重要” 猜想6:项目大部分是倒排期,不合理 按照估算排期就是正确的?怎么证明按照估算排期就是正确的?我倒是觉得估算排期证伪比较容易。看看历史数据估算和实际偏差有多大就知道了。 “所以动态规划就很重要了”,实时的适应变化,这个迭代需求多了,减少点, 需求少了,增加点。要满足某个倒排期的市场需求,去找寻mvp。 猜想7: 我们团队没有产品经理,团队无法明确需求,需求经常做偏 程序员要做好两件事情,搞明白要做什么,正确的做出来。有了产品经理,就指望这个人把做什么搞明白,再告诉你。做的东西用户不满意就是产品经理的问题。你再不用了解用户遇到什么问题,也不用思考怎样能解决用户的问题。没有产品经理,你就什么都不会了。 你都不想理解问题,就开始写代码. “难道真的是不要把爱好变成工作?”

July 27, 2021

明明每个人能力感觉还好,为啥整个团队交付的代码很糟糕?

组建团队的时候从算法到数据结构,从OO到template,从并发到协程,每个人都各有擅长。可整个团队交付的代码总是一团糟,总觉得还不如自己一个写全部代码好。本文尝试从软件流程和开发实践的角度分析内在原因。 “软件工程”,我们先来看工程学wiki定义:“工程学、工程科学或工学,是通过研究与实践应用数学、自然科学、 社会学等基础学科的知识,以达到改良各行业中现有材料、土木建筑、机械、电机电子、仪器、系统、 化学和加工步骤的设计和应用方式一门学科,而实践与研究工程学的人称为工程师。在高等学府中, 将自然科学原理应用至服务业、工业、农业等各个生产部门所形成的诸多工程学科也称为工科和工学。” 个人不是很喜欢这个词,因为早期软件工程和土木工程建筑设计流程很相识。无法确定瀑布开发方式是否参照了土木工程。 如果我们要造一座桥大致会有如下关键流程: 如果用瀑布流程开发软件: 流程是不是很相似,建筑行业几千年的经验总结,沉淀,产生了无数的建筑奇迹。这套建筑工程流程经过了无数的实践,会有很大问题吗?我认为不会。那软件行业参照这套流程设计可行吗?看隔壁日本这套流程玩的溜起来,但这套流程需满足建筑行业的几个基本条件。 条件1:施工人员严格按照流程的要求施工 条件2:一开始问题想的很清楚,到了施工阶段,需求变更很少,甚至几乎没有 条件3:足够的时间完成项目 瀑布流程里面每个流程都是环环相扣,a.每个流程都是由具备相应能力的人员完成,b.缺少任何一个环节都会导致后续动作直接变形。 建筑行业中“初步设计,技术设计,施工设计”都是由相应具备能力的建设设计师,结构力学工程师等协作完成。对应于软件行业里面“架构设计,概要设计,详细设计”,现实中架构设计由架构师参与完成,但概要设计,详细设计架构师很少参与,大多是由负责相应开发功能的开发人员完成。如果要类比建筑行业就是技术设计和施工设计由施工人员完成?!所以团队成员是否具备概要设计和详细设计能力会直接影响你后续的编码活动。 裁剪,瀑布流程里面我听到最多的就是这个词语。“详细设计浪费时间裁剪掉”,“单元测试没能力做,裁剪掉”,“代码检视没时间做,裁剪掉”,一个好端端的流程被你裁剪成只剩开发和测试,你说团队做出的东西不是一坨翔,你信吗?软件开发是你想的那么简单吗。 但软件毕竟不是建筑。软件的要解决的问题千奇百怪,很难一次想明白,软件变更成本相比建筑行业低太多。同时很多时候产品需要快速上线,无法接受瀑布流程的一个月甚至几个月的交付周期。所以瀑布流程需要满足的几个条件,在现在看来很难满足。在这种环境下用这套流程开发,自己是有多想不开。 敏捷开发流程就是在这种环境下应运而生。解决方案一开始想不清楚,先做个MVP来看看。需求变化多,采用迭代的方式缓解。用户量随时间增长,架构根据需要调整。 但敏捷开发在缩短迭代周期到2周以后,相应的开发实践都需要做相应的调整来适应开发流程的变化。 极限编程基本实践: 越是缩短交付周期,这些基础实践越重要,可以说如果不进行这些基础实践,根本不可能高质量1~2周交付。可经历过所谓瀑布开发流程的裁剪者们又来实践敏捷了,“UT太难写了,不写了”,“重构,不存在的”,“代码检视,太浪费时间了”,最后敏捷开发又变成了,两周只有开发和测试的活动。 这些裁剪者们往往从来没有写过UT,没有感受过UT对调试代码的益处,对重构的帮助。也没从来了解过优秀团队怎么做代码检视(https://google.github.io/eng-practices/review/), 然后他们拍脑袋本能的觉得除了本能开发一切都不重要。不去借鉴业界优秀的实践。 任何开发流程我认为有两个核心作用:帮助团队高效协作,帮助不同能力的团队成员都输出较高质量的代码。 如果你有幸和一群技术高手共事,那你可以看到他们用更多的开发实践去高效协作。 如果你所在的团队成员能力高低不平,做扎实基础实践和流程去帮助团队成员。 如果你所在的团队能力一般,整个交付过程,只有开发和测试,自求多福吧。

April 8, 2021