You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
13 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 11 | 反应式编程框架设计:如何使方法调用无阻塞等待?
你好,我是李智慧。
反应式编程本质上是一种异步编程方案在多线程协程、异步方法调用、异步I/O访问等技术基础之上提供了一整套与异步调用相匹配的编程模型从而实现程序调用非阻塞、即时响应等特性即开发出一个反应式的系统以应对编程领域越来越高的并发处理需求。
反应式系统应该具备如下的4个特质。
* **即时响应**:应用的调用者可以即时得到响应,无需等到整个应用程序执行完毕。也就是说应用调用是非阻塞的。
* **回弹性**:当应用程序部分功能失效的时候,应用系统本身能够进行自我修复,保证正常运行,保证响应,不会出现系统崩溃和宕机的情况。
* **弹性**:系统能够对应用负载压力做出响应,能够自动伸缩以适应应用负载压力,根据压力自动调整自身的处理能力,或者根据自身的处理能力,调整进入系统中的访问请求数量。
* **消息驱动**:功能模块之间、服务之间通过消息进行驱动,以完成服务的流程。
目前主流的反应式编程框架有RxJava、Reactor等它们的主要特点是基于**观察者设计模式**的异步编程方案,编程模型采用函数式编程。
观察者模式和函数式编程有自己的优势但是反应式编程并不是必须用观察者模式和函数式编程。我们准备开发一个纯消息驱动完全异步支持命令式编程的反应式编程框架框架名称为“Flower”。
## 需求分析
互联网及物联网场景下的应用系统开发,基本上都是高并发系统开发。也就是说,在同一个时刻,会有大量的用户或设备请求到达系统,进行计算处理。但是传统的编程模型都是阻塞式编程,阻塞式编程有什么特点,会产生什么问题呢?我们来看一段代码示例。
```plain
void a(){
....
int x = m();
int y = n();
return x + y;
}
```
在方法a中调用了方法m那么在方法m返回之前就不会调用方法n即方法a被方法m阻塞了。这种编程模型下方法m和方法n不能同时执行系统的运行速度就不会快并发处理能力就不会很高。
还有更严重的情况。服务器通常为每个用户请求创建一个线程而创建的总线程数是有限的每台服务器通常几百个。如果方法m是一个远程调用处理比较慢当方法a调用方法m时执行方法a的线程就会被长期挂起无法释放。如果所有线程都因为方法m而无法释放导致服务器线程耗尽就会使服务器陷入假死状态外部表现就是服务器宕机失去响应系统严重故障。
Flower框架应该满足如下典型Web应用的线程特性。
![图片](https://static001.geekbang.org/resource/image/e3/77/e3f2706fbf015cc81224d5c8cb3c9577.jpg?wh=1920x562)
当并发用户请求到达应用服务器时Web容器线程不需要执行应用程序代码它只是将用户的HTTP请求变为请求对象将请求对象异步交给Flower框架的Service去处理而Web容器线程自身立刻就返回。
如果是传统的阻塞式编程Web容器线程要完成全部的请求处理操作直到返回响应结果才能释放线程所以需要很多Web容器线程。但使用Flower框架只需要极少的容器线程就可以处理较多的并发用户请求而且容器线程不会阻塞。
同样在Flower框架中用户请求交给业务Service对象以后Service之间依然是使用异步消息通讯而非阻塞式的调用。一个Service完成业务逻辑处理计算以后会返回一个处理结果这个结果会以消息的方式异步发送给下一个Service。
## 概要设计
Flower框架实现异步无阻塞一方面是利用了Java Web容器的异步特性主要是Servlet3.0以后提供的AsyncContext快速释放容器线程另一方面则利用了异步的数据库驱动和异步的网络通信主要是HttpAsyncClient等异步通信组件。而Flower框架内核心应用代码之间的异步无阻塞调用则是利用了Akka 的Actor模型。
Akka Actor的异步消息驱动实现如下。
![图片](https://static001.geekbang.org/resource/image/f4/72/f4c3e72dyyf6876064d11yyba3168f72.png?wh=755x228)
一个Actor向另一个Actor发起通讯时当前Actor就是一个消息的发送者Sender它需要获得另一个Actor的ActorRef也就是一个引用通过引用进行消息通信。而ActorRef收到消息以后会将这个消息放到目标Actor的Mailbox里面然后就立即返回了。
也就是说一个Actor向另一个Actor发送消息时不需要等待对方真正地处理这个消息只需要将消息发送到目标Actor的Mailbox里面就可以了。Sender不会被阻塞可以继续执行自己的其他操作。而目标Actor检查自己的Mailbox中是否有消息如果有则从Mailbox里面获取消息并进行异步的处理。而所有的Actor会共享线程这些线程不会有任何的阻塞。
但是Actor编程模型无法满足人们日常的编程习惯以及Flower的命令式编程需求所以我们需要将Akka Actor封装到一个Flower的编程框架中并通过Flower提供一个新的编程模型。
Flower基于Akka的Actor进行开发将Service封装到Actor里面并且将Actor收到的消息作为参数传入Service进行调用。
Flower框架的主要元素包括Flower Service服务、Flower 流程和Flower容器。其中Service实现一个细粒度的服务功能Service之间会通过Message关联前一个Service的返回值Message必须是后一个Service的输入参数Message。而Flower容器就负责在Service间传递Massage从而使Service按照业务逻辑编辑成一个Flow流程
在Flower内部消息是一等公民基于Flower开发的应用系统是面向消息的应用系统。消息由Service产生是Service的返回值同时消息也是Service的输入。前一个Service的返回消息是下一个Service的输入消息**没有耦合**的Service正是通过消息关联起来组成一个Service流程并最终构建出一个拥有完整处理能力的应用系统。流程举例
```plain
// -> service1 -> service2 -> service5 -> service4
//      ^      |             ^              |
//      |       -> service3 -|              |
//      |___________________________________|
```
## 详细设计
Flower核心类图如下。
![](https://static001.geekbang.org/resource/image/7f/91/7fd7c49659117bb164bc616e24ee9891.jpg?wh=2000x1500)
Flower框架核心关键类及其职责如下
1. Service以及HttpService接口是框架的编程核心开发者开发的Service需要实现Service或者HttpService接口。HttpService与Service的不同在于HttpService在接口方法中传递Web参数开发者利用Web接口可以将计算结果直接print到HTTP客户端
2. ServiceFactory负责用户以及框架内置的service实例管理加载\*.services文件
3. ServiceFlow负责流程管理加载\*.flow文件
4. ServiceActor将Service封装到Actor。
Flower初始化及调用时序图如下。
![图片](https://static001.geekbang.org/resource/image/d0/f7/d0e3a161a016467242bb96aa685bc6f7.jpg?wh=1920x1532)
图中包含两个过程,第一个过程是**服务流程初始化**过程。首先开发者通过ServiceFacade调用已经定义好的服务流程。然后ServiceFacade根据传入的flow名和service名创建第一个ServiceActor。这个ServiceActor将通过ServiceFactory来装载Service实例并通过ServiceFlow获得当前Service在流程中所配置的后续Service可能有多个。依此递归创建后续Service的ServiceActor并记录其对应的ActorRef。
时序图中的第二个过程是**消息流处理**过程。调用者发送给ServiceFacade的消息会被flow流程中的第一个ServiceActor处理这个ServiceActor会调用对应的Service实例并将Service实例的返回值作为消息发送给流程定义的后续ServiceActor。
使用Flower框架开发应用程序就是开发各种Service开发服务Service类必须实现Flower框架的Service接口或者HTTP接口在process方法内完成服务业务逻辑处理。Service代码示例如下。
```plain
public class UserServiceA implements Service<User, User> {
  static final Logger logger = LoggerFactory.getLogger(UserServiceA.class);
  @Override
  public User process(User message, ServiceContext context) throws Throwable {
    message.setDesc(message.getDesc() + " --> " + getClass().getSimpleName());
    message.setAge(message.getAge() + 1);
    logger.info("结束处理消息, message : {}", message);
    return message;
  }
}
```
#### 服务注册
开发者开发的服务需要在Flower中注册才可以调用Flower提供两种服务注册方式配置文件方式和编程方式。
编程方式示例如下。
```plain
ServiceFactory serviceFactory = flowerFactory.getServiceFactory();
serviceFactory.registerService(UserServiceA.class.getSimpleName(), UserServiceA.class);
serviceFactory.registerService(UserServiceB.class.getSimpleName(), UserServiceB.class);
serviceFactory.registerService(UserServiceC1.class.getSimpleName(), UserServiceC1.class);
```
配置文件方式支持用配置文件进行注册,服务定义配置文件扩展名: .services放在classpath下Flower框架自动加载注册比如flower\_test.services。配置文件内容如下。
```plain
UserServiceA = com.ly.train.flower.base.service.user.UserServiceA
UserServiceB = com.ly.train.flower.base.service.user.UserServiceB
UserServiceC1 = com.ly.train.flower.base.service.user.UserServiceC1
```
#### 流程编排
在Flower中服务之间的依赖关系不能通过传统的服务之间依赖调用实现如开头的方法a调用方法m那样。而需要通过流程编排方式实现服务间依赖。服务编排方式也有两种配置文件方式和编程方式。
下面的例子演示的是以**编程方式**编排流程。
```plain
// UserServiceA -> UserServiceB -> UserServiceC1
final String flowName = "flower_test";
ServiceFlow serviceFlow = serviceFactory.getOrCreateServiceFlow(flowName);
serviceFlow.buildFlow(UserServiceA.class, UserServiceB.class);
serviceFlow.buildFlow(UserServiceB.class, UserServiceC1.class);
serviceFlow.build();
```
而流程**配置文件方式**则使用扩展名: .flow放在classpath下Flower框架会自动加载编排流程。 比如flower\_test.flow文件名flower\_test就是流程的名字流程执行时需要指定流程名。配置文件内容示例如下。
```plain
UserServiceA -> UserServiceB
UserServiceB -> UserServiceC1
```
我们将服务Service代码开发好注册到了Flower框架中并通过流程编排的方式编排了这几个Service的依赖关系后面就可以用流程名称进行调用了。调用代码示例如下其中flowName是流程的名字user是流程中的一个Service名是流程开始的Service。
```plain
final FlowRouter flowRouter = flowerFactory.buildFlowRouter(flowName, 16);
flowRouter.asyncCallService(user);
```
Flower框架源代码及更多资料可参考 [https://github.com/zhihuili/flower](https://github.com/zhihuili/flower)。
## 小结
架构师是一个技术权威他应该是团队中最有技术影响力的那个人。所以架构师需要具备卓越的代码能力否则就会沦为PPT架构师。PPT架构师可以一时成为团队的焦点但是无法长远让大家信服。
那么架构师应该写什么样的代码?架构师如果写的代码和其他开发工程师的代码一样,又何以保持自己的技术权威,实现技术领导?简单来说,代码可以分成两种,一种代码是给最终用户使用的,处理用户请求,产生用户需要的结果;另一种是给开发工程师使用的,各种编程语言、数据库、编译器、编程框架、技术工具等等。
编程语言、数据库这些是业界通用的,但是编程框架、技术工具,每个公司都可以依据自身的业务特点,开发自己的框架和工具。而架构师应该是开发框架的那个人,每个开发工程师都使用架构师的开发框架以及约定的编程规范开发代码。架构师通过这种方式落地自己的架构设计,保持自己的技术影响。
也许你的开发中不会用到反应式编程你可能也不需要深入学习Flower框架如何设计、如何使用。但是希望你能通过本文学习到如何设计一个编程框架结合你所在公司的业务场景将来开发一个你自己的编程框架。
## 思考题
Flower纯消息驱动、异步无阻塞的优良特点适合许多对并发处理要求高需要快速、及时响应的场景你能想到的现实应用场景有哪些呢
欢迎在评论区分享你的思考,我们共同进步。