Reactive(2) 响应式流与制奶厂业务
再谈响应式
在前一篇文章从Reactive编程到“好莱坞”中,谈到了响应式的一些概念,讲的有些发散。 但仅仅还是停留在概念的层面,对于实战性的东西并没有涉及。
所以大家看了后,或许还是有些不痛不痒。
响应式编程强调的是异步化、面向流的处理方式,这两者也并非凭空生出,而是从大量的技术实践中总结提炼出来的概念,就比如:
我们谈异步化,容易联想到 Java 异步IO(Asynchronized IO),而且习惯于将其和 BIO、NIO等概念来做对比。 殊不知,老早出现的 Swing 框架(Java UI)就已经将异步化思维玩的很溜了,不信的可以看看其内部 Observer模式(观察者)的实现。
我们谈流式处理,容易联想到 时下当红的 Flink框架。 但几乎所有的大数据分析、批处理应用都是基于流式进行处理的,比如 ETL,甚至是一个最简单的 Map Reduce 作业。
为什么Web后端开发的,对 Reactive 没有感觉

除了前端,Reactive 概念在大数据领域的应用其实非常的广泛了。 但是对于大多数做 Web 后端开发的人来说或许普及程度并不高,以笔者自身的感受是,码了这么些年头,除了做好代码分层之外,似乎也没有见到 Reactive可以发挥重大作用的地方。 原因就在于,在Web 后端开发领域基本是依托 HTTP协议机制实现的,这是一个相当简单的 请求 -> 应答 交互模式,客户端在发送请求后,会一直等待结果返回,也就是结果的通知是由客户端主动获取而非异步通知的,因此并不是 Reactive 的风格。 但这已经是符合用户一贯的使用方式了,绝大多数情况下并不需要做什么样的变化,此时我们对响应式的感知并不深刻。
更符合Reactive 的另外一个场景是 富客户端(Rich Application),假设在需要大量复杂的前端交互的场景下,我们可以选择将一些逻辑放在前端代码中实现。 此时的 Web 交互就不再是整个页面的刷新,而是演变为客户端与服务端的"实时"双向通讯,这类应用也比较普遍了,比如基于 WebSocket 实现的 聊天应用、小游戏等等。
浅显的从趋势上看, Reactive 的前景还是很明朗的,这里并不是说因为现在多数流行的编程语言中都有它的影子(比如提供了Rx风格的框架)。
而是未来的大数据处理、实时流计算会成为主流,这是环境决定的。 而这时 Reactive 这种"面向流"的编程模式无疑是很合适的。
Java 9 支持的 Reactive Stream
Java 平台直到 JDK 9 才提供了对于 Reactive 的完整支持,而在此之前的JDK版本中,也以及存在一些有关联性的API,比如:
- Future 和 CompletableFuture接口,用于实现异步计算。 后者较前者则是完善了异步结果通知、任务串行等特性。
- Stream 接口,可以将传统的集合转换为"流"的方式进行处理,比如迭代、映射转换。
这些关联性API 并不是完整的 Reactive,Java 9所支持的 Reactive Stream API 来自于2013年的响应式流规范(Reactive Stream Specification)。
https://www.reactive-streams.org/
基于这个规范中主要定义了下面几个接口:
Java的响应式流接口统一定义在 java.util.concurrent.Flow接口
Publisher
即数据的发布者。 Publisher 接口定义了一个subscribe方法,用于添加订阅者:Subscriber
指数据的订阅者。 Subscriber 接口定义了4个方法,用于针对不同的事件作出响应。
首先,在subscribe方法调用成功后,Subscriber的 onSubscribe(Subscription s) 方法会被触发(Subscription 表示当前的订阅关系)。
此后,正常可以继续调用 Subscription 的 request(long n) 方法来向发布者请求数据,n是指最大的数据条目数。
发布者会产生3种不同的消息,分别对应到 Subscriber 的3个回调方法:
数据消息:对应 onNext 方法,表示发布者产生的数据。
错误消息:对应 onError 方法,表示发布者产生了错误。
结束消息:对应 onComplete 方法,表示发布者已经完成了所有数据的发布。
在上面的3种通知中,错误、结束消息都表示当前的流已经到达了终点,后面不再会有消息产生。
Subscription
Subscription 表示的是一个订阅关系。 可以通过该对象请求数据(request方法),或者取消订阅(cancel方法)。Processor
Processor 表示的一种特殊的对象,既是生产者,又是订阅者。
负压的支持
负压是响应式流定义的一种重要的能力,在上述的接口中,实质上已经提供了负压的支持。
Publisher 只有在收到请求之后,才会产生数据。 这就保证了 Subscriber 可以根据自己的处理能力,确定要向 Publisher 请求的数据量,以此保证自身不会被冲垮。
范例
下面,以一个简单的代码示例来演示 Reactive Stream API 是如何使用的。

以某一个制奶厂为例,为了提高营收,工厂推出了一个厂家直销的业务。 顾客可以直接向厂方订购一定天数的奶制品,每天则是由工厂的服务人员送奶上门。
为了模拟这个场景,我们实现的代码如下:
- 制奶厂,一个Publisher实现:
public class MilkFactory extends SubmissionPublisher<String> {
private final ScheduledFuture<?> periodicTask;
private final ScheduledExecutorService scheduler;
private static final List<String> milks = Arrays.asList("益力多", "酸牛奶", "原味奶", "低脂蛋奶", "羊奶", "甜牛奶");
public MilkFactory() {
super();
//初始化定时器
scheduler = new ScheduledThreadPoolExecutor(1);
//每一天生产完牛奶并推送给消费者
periodicTask = scheduler.scheduleAtFixedRate(
() -> submit(produceMilk()), 0, 1, TimeUnit.SECONDS);
}
//随机生产牛奶
private String produceMilk() {
return milks.get((int) (Math.random() * milks.size()));
}
//关闭流
public void close() {
periodicTask.cancel(false);
scheduler.shutdown();
super.close();
}
}
MilkFactory 集成自SubmissionPublisher(一个提供缓冲的Publisher实现),其内部会启动一个定时器,用于模拟每天给用户发放生产的牛奶。
通过submit()方法可以将数据推送给用户。
- 顾客,一个Subscriber实现:
public class MilkCustomer implements Flow.Subscriber<String> {
private Flow.Subscription subscription;
private AtomicInteger available = new AtomicInteger(0);
private int dayCount;
public MilkCustomer(int dayCount) {
this.dayCount = dayCount;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
//设置总量
available.set(dayCount);
//第一天
subscription.request(1);
}
@Override
public void onNext(String milk) {
System.out.println("今天的牛奶到了: " + milk);
//如果还有存量,继续请求
if(available.decrementAndGet() > 0){
subscription.request(1);
}else{
System.out.println("牛奶套餐已经派完,欢迎继续订购");
this.subscription.cancel();
}
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("closed.");
}
}
MilkCustomer 接受一个dayCount入参,即表示订购的数量,在首次订阅时会请求第一天的奶品,此后则每次收到到奶品后再请求下一天的,直到将总量消费完。
- 测试程序
执行下面的代码:
MilkFactory factory = new MilkFactory();
//订阅1周
MilkCustomer customer = new MilkCustomer(7);
factory.subscribe(customer);
输出:
今天的牛奶到了: 酸牛奶
今天的牛奶到了: 羊奶
今天的牛奶到了: 原味奶
牛奶套餐已经派完,欢迎继续订购
小结
在上例中,我们使用 Java 提供的 Reactive Stream API 实现了一个"送奶上门" 的业务流。
整个过程相对是比较简单的,最关键的地方就在于对流式处理以及订阅关系的理解。 然而目前的 Reactive 实现还没有完全的统一,比如 Spring WebFlux(SpringBoot 2支持) 仍然是基于 Reactor 私有API而不是 Reactive Stream API 来构建的,后面有机会再做下介绍。
扩展阅读
关于Future和CompletableFuture的区别
https://juejin.im/post/5adbf8226fb9a07aac240a67
Reactive(2) 响应式流与制奶厂业务的更多相关文章
- JVM平台上的响应式流(Reactive Streams)规范
// Reactive Streams // 响应式流是一个倡议,用来为具有非阻塞后压的异步流处理提供一个标准.大家努力的目标集中在运行时环境(JVM和JavaScript)和网络协议上. 注:响应式 ...
- JVM上的响应式流 — Reactor简介
强烈建议先阅读下JVM平台上的响应式流(Reactive Streams)规范,如果没读过的话. 官方文档:https://projectreactor.io/. 响应式编程 作为响应式编程方向上的第 ...
- 响应式流API的构建基础
下面三个重要的概念是响应式流API的构建基础: 发布者是事件的发送方,可以向它订阅. 订阅者是事件订阅方. 订阅将发布者和订阅者联系起来,使订阅者可以向发布者发送信号. http://www.info ...
- reactive stream: 响应式编程
既然 Reactive Stream 和 Java 8 引入的 Stream 都叫做流,它们之间有什么关系呢?有一点关系,Java 8 的 Stream 主要关注在流的过滤,映射,合并,而 Reac ...
- Reactive(3)5分钟理解 SpringBoot 响应式的核心-Reactor
目录 一.前言 二. Mono 与 Flux 构造器 三. 流计算 1. 缓冲 2. 过滤/提取 3. 转换 4. 合并 5. 合流 6. 累积 四.异常处理 五.线程调度 小结 参考阅读 一.前言 ...
- Reactive 理解 SpringBoot 响应式的核心-Reactor
Reactive 理解 SpringBoot 响应式的核心-Reactor bestcoding 2020-02-23 17:26:43 一.前言 关于 响应式 Reactive,前面的两篇文章谈了不 ...
- 5分钟理解 SpringBoot 响应式的核心-Reactor
目录 一.前言 二. Mono 与 Flux 构造器 三. 流计算 1. 缓冲 2. 过滤/提取 3. 转换 4. 合并 5. 合流 6. 累积 四.异常处理 五.线程调度 小结 参考阅读 一.前言 ...
- Reactive(1) 从响应式编程到"好莱坞"
目录 概念 面向流设计 异步化 响应式宣言 参考文档 概念 Reactive Programming(响应式编程)已经不是一个新东西了. 关于 Reactive 其实是一个泛化的概念,由于很抽象,一些 ...
- 函数式响应式编程 - Functional Reactive Programming
我们略过概念,直接看函数式响应式编程解决了什么问题. 从下面这个例子展开: 两个密码输入框,一个提交按钮. 密码.确认密码都填写并一致,允许提交:不一致提示错误. HTML 如下: <input ...
随机推荐
- jsp的简介(1)
一.什么是Java Server Pages? JSP全称Java Server Pages,是一种动态网页开发技术.它使用JSP标签在HTML网页中插入Java代码.标签通常以<%开头以%&g ...
- 一文掌握 Spring Boot Profiles
Spring Boot Profiles 简介 Profile 的概念其实很早在 Spring Framework 就有了,在 Spring Framework 3.1 版本引入了注解 @Profil ...
- 洛谷 P3811 题解
题面 利用暴力快速幂O(nlogn)会TLE掉: 所以对于求1~n的所有逆元要用递推公式: #include <bits/stdc++.h> using namespace std; ]; ...
- linux字符设备驱动中内核如何调用驱动入口函数 一点记录
/* 内核如何调用驱动入口函数 ? *//* 答: 使用module_init()函数,module_init()函数定义一个结构体,这个结构体里面有一个函数指针,指向first_drv_init() ...
- 在表格中添加text便加框
private void createTableText(Table table) { TableEditor editor = new TableEditor(table); for (int i ...
- AppBoxFuture: 123挨个站-数据按序存储
最近几天在优化存储的编码规则,顺带把之前设计了但未实现的倒排序一并实现了.由于所有数据(元数据.实体.索引等)都映射至RocksDB的Key-Value存储,所以必须扩展RocksDB的自定义比较 ...
- kube-scheduler源码分析
kubernetes集群三步安装 kube-scheduler源码分析 关于源码编译 我嫌弃官方提供的编译脚本太麻烦,所以用了更简单粗暴的方式编译k8s代码,当然官方脚本在编译所有项目或者夸平台编译以 ...
- Linux 根分区扩容
扩容分区之前,首先要保证当前有闲置空间 1. 查看当前现有分区情况 df -lah 可以看出当前根分区只剩 6.4 G 可用 2. 查看当前磁盘情况 fdisk -l 可以看出有 30G的未分配空间 ...
- 单元测试python unittest
记录自己学习单元测试框架的一篇博客 菜鸟的学习之路比较艰辛到处找资料一把辛酸泪啊 1.首先是创建一个类里面设计一些简单的函数方便写用例: 原谅我蹩脚的英文直接用拼音命名了 : 2.接着就是创建用例文件 ...
- 数据结构之二叉树的构建C++版
二叉树的构建要注意与链式表的区别,二叉树这里的构建十分低级,每个树只是构建了一个单一的二叉树节点,总体来看是有下向上构建的.用户需要手动去构建自己需要的树,而不是直接去插入数据就到二叉树中了,因为不是 ...