1. Reactor 对比

1.1 Reactor 线程模型

Reactor 线程模型就是通过 单个线程 使用 Java NIO 包中的 Selector 的 select()方法,进行监听。当获取到事件(如 accept、read 等)后,就会分配(dispatch)事件进行相应的事件处理(handle)。

如果要给 Reactor 线程模型 下一个更明确的定义,应该是:

Reactor线程模式 = Reactor(I/O多路复用)+ 线程池

Netty、Redis 使用了此模型,主要是解决 C10K 问题

C10K 问题:服务器如何支持 10K 个并发连接

1.2 Spring Reactor

Reactor 是 JVM 完全非阻塞的响应式编程基础,响应式编程是一种关注数据流和变化传播的异步编程范式。这意味着可以通过所采用的编程语言轻松地表达静态(例如数组)或动态(例如事件发射器)数据流。

Mono<List<String>> cartInfoMono = Mono._just_( **"songjiyang" **)
.map( UserService::_findUserByName _)
.map( UserService::_findUserShoppingCart _); String user = UserService._findUserByName_( **"songjiyang" **);
List<String> userShoppingCart = UserService._findUserShoppingCart_( user );

1.3 区别和联系

联系:

  • 两者都是使用异步的手段来提高系统的性能

区别:

  • Reactor 模型主要异步的处理新连接、连接和读写,而 Spring Reactor 在更高的代码级别提供了异步框架

或者反过来说,新连接、连接和读写等事件触发了 Netty Reactor 的某些管道处理器流程,某些事件触发了 Spring Reactor 的执行流程,这也是 Reactor(反应器)名称的由来

2. Java 中的异步

上面我们一直在讲异步,异步其实是针对调用者的,也就是调用者调用完方法之后就可以做的别的事情了,Java 中实现异步就两种方式:

  1. 回调
  2. 多线程

2.1 回调

回调其实就是把当前的事情完成之后,后面需要做的事当成函数传进行,等完成之后调用就行

**public static void **main( String[] args ){
_ doA_( ( next ) -> {
_log_.info( **"doB" **);
next.run();
}, () -> _log_.info( **"doC" **) ); }
**public static void **doA( Consumer<Runnable> next, Runnable nextNext ){ _log_.info( **"doA" **);
next.accept( nextNext );
} // output
15:06:52.818 [main] INFO concurrent.CompleteTest - doA
15:06:52.820 [main] INFO concurrent.CompleteTest - doB
15:06:52.820 [main] INFO concurrent.CompleteTest - doC

回调是在一个线程中来完成的,很容易理解,但问题是回调太多代码就变的很复杂,有回调地域的问题

回调只是一种异步的编程方式,本身实现异步其实还是需要多线程,例如单独起一个监听线程来执行回调函数,例如 EventListener

如果执行的任务不考虑线程安全问题的话,可以使用 CompletableFuture 来解决,会更加易于阅读

CompletableFuture
._runAsync_( ()-> _log_.info(**"doA"**) )
.thenRunAsync( ()-> _log_.info(**"doB"**) )
.thenRunAsync( ()->_log_.info(**"doC"**) )
.get(); // output
15:08:04.407 [ForkJoinPool.commonPool-worker-1] INFO concurrent.CompleteTest - doA
15:08:04.410 [ForkJoinPool.commonPool-worker-1] INFO concurrent.CompleteTest - doB
15:08:04.410 [ForkJoinPool.commonPool-worker-1] INFO concurrent.CompleteTest - doC

CompletableFuture 的 thenRunAsync 也是基于回调,每个任务 Class 会有一个 next, 多个任务组成一个回调链

Mono._just_(**""**)
.doOnNext( (x)-> _log_.info(**"doA"**) )
.doOnNext( (x)-> _log_.info(**"doB"**) )
.doOnNext( (x)-> _log_.info(**"doC"**) )
.block();
15:12:56.160 [main] INFO concurrent.CompleteTest - doA
15:12:56.160 [main] INFO concurrent.CompleteTest - doB
15:12:56.161 [main] INFO concurrent.CompleteTest - doC

2.2 多线程

多线程的方式,大家应该都很熟悉

  1. Thread
  2. ExecutorService 线程池
  3. CompletionService 带结果队列的线程池
  4. CompletableFuture 用于任务编排
  5. Runable、Callable、Future、CompletableFuture

3. Spring Reactor

从上面可以看到一些使用 Reactor 的代码中,都可以在原生 JDK 中找到替换,那我们为什么还需要它呢?

  • 可组合和可读性
  • 丰富的操作
  • 订阅之前什么都不会发生
  • 背压

下面是 Java9 中 Flow 类的类图,SpringReactor 也是使用这四个类,在 Java9 中已经成了规范

3.1 Publisher

  • Mono,提供 0 到 1 个 Item

  • Flux,提供 0 到 N 个 Item

发布者提供 n 个 Item, 经过一些 operator(数据处理操作),完成或者异常中止

核心方法:

  • subscribe

3.1.1 创建

Mono<String> noData = Mono.empty();
Mono<String> data = Mono.just("foo");
Flux<Integer> numbersFromFiveToSeven = Flux.range(5, 3); Mono._fromSupplier_( ()->**1 **);
Mono._fromFuture_( CompletableFuture._runAsync_( ()-> {} ) );
Flux._create_((sink)->{
**for**( **int **i = **0**; i < **5**; i++ ){
sink.next( i ) ;
}
sink.complete();
});

3.1.2 处理

下面这些都称为 operator,可以很灵活处理其中的 Item

  • 转化 map、flatMap、
  • 消费 doOnNext、doNextError、doOnCancel
  • 过滤 filter、distinct、take
  • 错误处理 onErrorReturn、onErrorComplete、onErrorResume、doFinally
  • 时间相关 timeout、interval、delay
  • 分隔 window、buffer
  • 转同步 block、toStream

3.1.3 订阅

订阅然后消费发布者的内容

subscribe();
subscribe(Consumer<? super T> consumer);

订阅之后的返回值是Disposable****,可以使用这个对象来取消订阅,会告诉发布者停止生产对象,但不保证会立即终止

  • 当然可以给 subscribe 传递参数,自定义 complete 或者 error 时需要做的时
  • 同时可以使用 BaseSubscriber 类来实现订阅,可以控制消费的数量

3.2 Subscriber

消费者一般不用手动创建,通过 subscribe 传进 Consumer 函数后,会自动生成一个 LambdaSubscriber,核心方法:

  • onSubscribe
  • onNext
  • onError
  • onComplete

3.3 Processor

既是发布者,又是订阅者

3.4 Subscription

订阅,消费者调用 subscribe 方法之后可以在 onSubscribe 回调中获取,可以请求下一个 Item 或者取消订阅

  • request
  • cancel

3.5 Thread 和 Scheduler

没有指定的情况下:

  • 当前的 operator 使用上一个 operator 的线程,最先的 operator 使用调用 subscribe 的线程来执行

Reactor 中使用 Scheduler 来执行流程,类似 ExecutorService

  • subscribeOn 可以指定订阅时使用的线程,这样可以不阻塞的订阅
  • publishOn 指定发布时使用的线程

4. Spring Reactor 优化案例

流程中可以优化的点:

  1. 准备数据可以异步,等需要用的时候在去阻塞获取,相当于一个 Future
  2. 召回可以完成之后就去等正排数据,新的问题,如何去重?本来拿一次正排数据,现在拿 N 个召回次数据,请求量是不是会变大,耗时是不是也会增加
  3. 过滤的准备数据也可以异步,也就是说某个过滤策略的数据准备好了,就可以去执行过滤了,而且还存在很多不需要依赖数据的过滤策略也需要等
  4. 一般粗排只需要 1000 条数据,过滤时已经拿够了 1000 条就可以跳过了

我们上面所说的异步,其实就是说流程中某些节点是在同时执行的,不必等一个节点完成后再执行另外一个,这其实一个统筹学的问题

4.1 解决方法对比

问题 Java 原生 Reactor
准备数据异步 Future,缺点:1. 需要调用方处理异常 2. 不能编排后续流程,eg: 拿完企业信息后继续拿企业治理信息,Future 需要 get 阻塞 Mono, 使用 onErrorResume 处理异常,使用 map 编排后续流程
召回完成拿正排 需要一个阻塞队列,召回把结果往里面 push,另外一个线程从队列里面拿同时去取正排数据,需要自己维护 map 来去重,需要循环等待到达批次后去取正排 Flux,召回使用 sink.next 把结果放进去合并节点订阅,使用 distinct 来去重,使用 buffer 来实现批次数据
过滤准备数据异步 需要阻塞队列 Flux, 在依赖任务中把准备好的过滤策略放进去,过滤节点订阅 Flux 并过滤
粗排取 1000 条 异步执行过滤,把过滤结果放到一个容器中,粗排节点不断查看这个容器的结果是否够 1000 条,够了就可以执行粗排了 Flux, 使用 take(1000)
**for **(StrategyConfig filterConfig : filterConfigList) {
_doStrategyFilter_(filterChainContext, recommendContext, recRequest, filterConfig, allFilters, partitionContext, partitionTrace);
} readyStrategyFlux.publishOn(ExecutorServiceHolder._scheduler_).doOnNext((readyStrategyName) -> {
**try **{
List<StrategyConfig> strategyConfigs = strategyNameToConfigs.get(readyStrategyName);
**for **(StrategyConfig strategyConfig : strategyConfigs) {
_doStrategyFilter_(filterChainContext, recommendContext, recRequest, strategyConfig, allFilters, partitionContext, partitionTrace);
}
} **catch **(Exception e) {
_LOGGER_.error(**"doOnNext filter error"**, e);
} }).blockLast();

这里的 blockLast 又回到了同步世界,可以很好的和已有的代码兼容

下面是 20240629 到 20240702 某个场景优化过滤阶段的耗时对比

pv qps tp99 avg
实验组 4051865 46.90 369.00 230.88
对照组 4054074 46.92 397.00 251.55

业务指标对比

无明显波动

5. 总结

Spring Reactor 是一个响应式编程框架,非常适合类似 MXN 这样的流程编排系统,也是 Java 中异步编程的一种补充,但也会有一些其他的问题,例如潜在的线程安全问题,已有框架的冲突 ThreadLocal 等

参考

【1】深入 Netty 逻辑架构,从 Reactor 线程模型开始(一)-阿里云开发者社区

【2】Reactor 3 Reference Guide

【3】C10k 问题简述-CSDN 博客

Spring Reactor基本介绍和案例的更多相关文章

  1. Spring Data JPA介绍与简单案例

    一.Spring Data JPA介绍 可以理解为JPA规范的再次封装抽象,底层还是使用了Hibernate的JPA技术实现,引用JPQL(Java Persistence Query Languag ...

  2. springboot:spring data jpa介绍

    转载自:https://www.cnblogs.com/ityouknow/p/5891443.html 在上篇文章springboot(二):web综合开发中简单介绍了一下spring data j ...

  3. spring boot(五)Spring data jpa介绍

    在上篇文章springboot(二):web综合开发中简单介绍了一下spring data jpa的基础性使用,这篇文章将更加全面的介绍spring data jpa 常见用法以及注意事项 使用spr ...

  4. (2)java Spring Cloud+Spring boot+mybatis企业快速开发架构之SpringCloud-Spring Cloud是什么?Spring Cloud版本介绍

    ​ Spring Cloud 是一系列框架的有序集合.它利用 Spring Boot 的开发便利性,巧妙地简化了分布式系统基础设施的开发,如服务注册.服务发现.配置中心.消息总线.负载均衡.断路器.数 ...

  5. 1、Spring简介及IOC入门案例

    一.Spring框架介绍 1.介绍 Spring框架是由于软件开发的复杂性而创建的.Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情.然而,Spring的用途不仅仅限于服务 ...

  6. Spring学习笔记 - 第二章 - 注解开发、配置管理第三方Bean、注解管理第三方Bean、Spring 整合 MyBatis 和 Junit 案例

    Spring 学习笔记全系列传送门: Spring学习笔记 - 第一章 - IoC(控制反转).IoC容器.Bean的实例化与生命周期.DI(依赖注入) [本章]Spring学习笔记 - 第二章 - ...

  7. Spring MVC学习(一) Spring MVC基本介绍以及配置

    Spring MVC学习(一) Spring MVC基本介绍以及配置   摘要:这篇笔记是对于Spring MVC的第一部分的学习中的整理,主要记录了Spring MVC的基础知识以及配置一个简单的S ...

  8. Spring MVC 使用介绍(十五)数据验证 (二)依赖注入与方法级别验证

    一.概述 JSR-349 (Bean Validation 1.1)对数据验证进一步进行的规范,主要内容如下: 1.依赖注入验证 2.方法级别验证 二.依赖注入验证 spring提供BeanValid ...

  9. Spring MVC 使用介绍(十四)文件上传下载

    一.概述 文件上传时,http请求头Content-Type须为multipart/form-data,有两种实现方式: 1.基于FormData对象,该方式简单灵活 2.基于<form> ...

  10. Spring MVC 使用介绍(十三)数据验证 (一)基本介绍

    一.消息处理功能 Spring提供MessageSource接口用于提供消息处理功能: public interface MessageSource { String getMessage(Strin ...

随机推荐

  1. 用 Certbot-auto 在 letsencrypt.org申请免费 SSL 证书实现 HTTPS

    参考帖子 https://www.cnblogs.com/lzpong/p/6433189.html https://www.cnblogs.com/756623607-zhang/p/1163850 ...

  2. SATA与PCI-E速度对比

    SATA SATA接口已经发展到了第三代,理论上的最大速度达到600MB/s.平时大家见到的SATA SSD使用的都是SATA三代,实际测试速度在550MB/s左右,这比普通的机械硬盘的速度100MB ...

  3. python教程6.5-excel处理模块

    第三方开源模块安装  创建文件 打开已有文件 写数据 选择表 保存表 遍历表 按行遍历 按列遍历 遍历指定行列 遍历指定第几列数据 删除表 设置单元格样式 字体 对齐 设置行高列宽

  4. DP-Modeler三维修模软件简介

    图像快速建模系统DP-Modeler是天际航自主研发的一款集精细化单体建模及Mesh网格模型修饰于一体的新型软件.通过特有的摄影测量算法,支持航测摄影.无人机影像.地面影像.车载影像.激光点云等多数据 ...

  5. 2024-05-22:用go语言,你有一个包含 n 个整数的数组 nums。 每个数组的代价是指该数组中的第一个元素的值。 你的目标是将这个数组划分为三个连续且互不重叠的子数组。 然后,计算这三个子数

    2024-05-22:用go语言,你有一个包含 n 个整数的数组 nums. 每个数组的代价是指该数组中的第一个元素的值. 你的目标是将这个数组划分为三个连续且互不重叠的子数组. 然后,计算这三个子数 ...

  6. es 排序突然很慢的原因

    今天突然之间发现一个访问es的查询很慢.由刚上线之前测试的100ms直接到了5s左右.瞬间懵逼. 这个用户索引大概200w的数据. 查询语句如下 GET /user/_search{"fro ...

  7. Vue3.0极速入门(一) - 环境安装&新建项目

    Vue介绍 Vue.js 是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架.与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用.Vue 的核心库只 ...

  8. C# WinForm控件及其子控件转成图片(支持带滚动条的长截图)

    概述(Overview) 参考了网上的分析,感觉都不太理想:1.一个控件内如果包含多个子控件时没有考虑顺序问题:2.超出控件可显示区域时不能长截图,有滚动条会多余截取了滚动条.这个随笔旨在解决这个问题 ...

  9. TDSQL数据库考试实操题

    第一题: 演练二 物理备份(5分) 答: 第二题:2. 演练一 请根据给定的演练方案,进行相关演练,并按如下要求提交截图 主备切换(5分) 答: 第三题:3. 演练一 请根据给定的演练方案,进行相关演 ...

  10. java检测当前CPU负载状态的方法

    1.java检测当前CPU负载状态 在Java中,直接检测CPU负载状态并不像在操作系统命令行中那样简单,因为Java标准库并没有直接提供这样的功能.但是,我们可以通过几种方法间接地获取CPU负载信息 ...