1. 背景

公司有一个推荐系统Rec,这个系统的主要功能是:

  1. 向外部系统提供推荐接口
  2. 根据请求获取推荐策略
  3. 根据推荐策略完成推荐的召回、过滤、打分、排序阶段

Rec作为微服务中的一环,本身不存储召回的物料信息,也不存储用户和物料的特征信息,它负责就是对各个服务的组合和流转

其流程如下:

2. 问题

在开发Rec的过程中,发现流程中存在可以优化的地方,例如:

流程 问题
“合并节点”需要等待所有的召回结果完成之后merge到一个List,然后去获取详情信息 获取详情信息是一个需要分批的IO操作,既然需要分批,为什么不是每个召回完成就去获取详情,而要等待合并。即使不需要分批,获取详情也会随着召回结果的数量变多而耗时变长
“过滤节点”需要先准备过滤的数据,等所有数据准备好,再进行过滤 获取数据是IO操作,过滤是CPU密集操作,是不是可以获取完一部分数据就可以立即进行过滤

上述的问题是纯粹从性能方面去考虑,目前的流程从逻辑是更容易理解的

2.1 优化流程图

  1. 合并节点
flowchart LR
画像召回 --> 合并
类目召回 --> 合并
ItemCF召回 --> 合并
合并 --> 获取详情
flowchart LR
画像召回 --> 获取详情1
类目召回 --> 获取详情2
ItemCF召回 --> 获取详情3
获取详情1 --> 合并
获取详情2 --> 合并
获取详情3 --> 合并
  1. 过滤节点
flowchart LR
获取质量分数据 --> 完成
获取重复展示数据 --> 完成
获取黑名单数据 --> 完成
完成 --> 过滤
flowchart LR
获取质量分数据 --> 过滤1 --> 完成
获取重复展示数据 --> 过滤2 --> 完成
获取黑名单数据 --> 过滤3 --> 完成

3. 分析

我们分析上述问题,并提出了的初步的优化方式

我们还要考虑下面几个问题:

  1. 这样的优化是不是有效的,收益如何,从性能和代码理解上
  2. 如何具体实现

上面提出的两个问题本质上是属于流程统筹方面的问题,在这个流程中,我们还是可以找到一些别的优化的地方:

例如“路由节点获取准备数据”是不是可以不用等待,提交完异步任务之后直接继续,在使用的时候去直接拿,如果发现还没有拿到数据就等着,有数据就直接使用,而不是原来的等待拿到数据再继续下一步

flowchart LR
画像召回 --> id{获取画像数据} -->|已获取到| 继续
id{获取画像数据} -->|没获取到| 等待
等待 --> id{获取画像数据}

当存在多个获取任务时,会缩短执行的时间,举个例子:

原先的流程:都使用线程池都来完成,合并前花费10ms,合并后花费10ms,总耗时20ms

flowchart LR
id["获取画像数据完成(10ms)"] -->合并 --> id2["画像召回(5ms)"]
id1["获取热门类目数据完成(5ms)"]-->合并 --> id3["热门类目召回(10ms)"]

新的流程:都使用线程池都来完成,总耗时15ms

flowchart LR
id["获取画像数据完成(10ms)"] --> id2["画像召回(5ms)"] -->合并
id1["获取热门类目数据完成(5ms)"] --> id3["热门类目召回(10ms)"]-->合并

上述的流程图展示出来和上面的提出的两个问题属于一类,但这个问题不能简单改变流程,因为:

  1. 从流程图中可以看出来它们离得太远,而移到一起这个逻辑会被打散,没有现在的直观
  2. 准备数据是在准备了很多数据,但把其中几个拿出来的效果不能确定

所以我们需要使用一些异步编程的手段,在流程不变的情况下,还能使其执行的更快,下面表示优化的时间是从哪来的:

4. 调研

在调研的过程中,发现了很多相关的技术:

  1. Reactor,Java响应式编程
  2. CompletableFuture,Java异步编程
  1. Quasar,java协程

4.1 Future

Java中提供Future可以满足我们异步的需要吗?

如果只是一个异步任务,例如画像召回需要等待画像数据,我们可以在画像召回中使用future.get()

但在画像召回完成之后,我们要进行过滤,需要提前准备一些数据,例如已展示推荐数据,使用future.get()让画像召回变成了同步任务,在获取结果之前无法继续

项目目前就是使用了线程池+Future的方案,不过future.get()都在线程池submit之后

4.2 CompletableFuture

针对上述问题, JDK8设计出CompletableFuture。CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方

利用CompletableFuture,我们可以这样写:

    CompletableFuture recallFuture = CompletableFuture
.supplyAsync(获取画像数据任务)
.thenApply(画像召回)
.thenAccept(获取召回信息过滤数据) CompletableFuture filterDataFuture = CompletableFuture
.supplyAsync(获取过滤数据任务) recallFuture.thenCombineAsync(filterDataFuture, (s, w) -> {过滤数据 });

4.3 Reactor

看起来CompletableFuture已经可以满足我们的需求了,为什么需要再了解Reactor呢?它们有什么差别?

官方参考手册通过对比Callback、CompletableFuture和Reactor,它们都可以实现异步功能,但CompletableFuture/Future有下面的缺点

Future objects are a bit better than callbacks, but they still do not do well at composition, despite the improvements brought in Java 8 by CompletableFuture. Orchestrating multiple Future objects together is doable but not easy. Also, Future has other problems:

It is easy to end up with another blocking situation with Future objects by calling the get() method.

They do not support lazy computation.

They lack support for multiple values and advanced error handling.

  1. 调用future.get()就进入到了阻塞,这种情况很容易出现
  2. 不支持惰性计算,参考StackOverFlow中Oleh Dokuka的回答
  3. 不支持多个值处理和高级错误处理,查看官方参考手册中的例子可以看出

除此之外,官方参考手册介绍了一些其他特点,参考3.3

4.3.1 实现

如何使用Reactor实现上述功能呢?

         Mono<String> recallMono = Mono
.fromCallable(() -> "获取画像数据")
.flatMap((portraitData) -> Mono.fromCallable(() -> "画像召回"))
.flatMap((recItemData) -> Mono.fromCallable(() -> "获取召回信息过滤数据")); Mono<String> filterDataMono = Mono
.fromCallable(() -> "获取过滤数据任务"); Mono.zip(recallMono, filterDataMono).filter((t)->true);

Java大部分的library都是同步的(HttpClient,JDBC),Mono可以和Future组合使用线程来实现异步任务,Java也存在一些异步库例如Netty,Redis Luttuce.

4.4 Quasar

至于为什么会提到Quasar,贝壳技术 | 响应式编程和协程在 Java 语言的应用中介绍了响应式编程和协程一起使用的场景,给出了原因:

  1. 响应式编程必须使用异步才能发挥其作用
  2. Java中异步的唯一解决方案就是线程
  3. 过多的线程会造成OOM,所以需要使用协程

我个人觉得响应式编程本质是从统筹学来优化程序的,最著名的例子就是烧水泡茶流程,我们只不过是通过合理编排让硬件资源最大化利用。

具体实现是将原本同步逻辑中的片段打散到不同的线程中去异步执行,原本同步阻塞的线程这时候可以给别的任务使用,应该会减少更多线程的使用

5. 实现

  1. 使用线程池来处理Reactor中的异步任务
  2. 使用flatMap、map编排后续任务
  3. 使用Flux表示推荐结果流,通过不同召回不断把召回结果sink到流中
  4. 使用buffer来处理分批任务
  5. 使用zip或者flatMap来处理并发任务
  6. 使用distinct去重
  7. 使用block异步转同步获取推荐结果

6. 效果

待更新

参考

[1] 贝壳技术 | 响应式编程和协程在 Java 语言的应用

[2] 异步编程利器:CompletableFuture详解 |Java 开发实战

[3] Reactor Java文档

[4] 并发模型之Actor和CSP

[5] RxJava VS Reactor

[6] CompletableFuture原理与实践-外卖商家端API的异步化

使用Spring Reactor优化推荐流程的更多相关文章

  1. SSH(Struts2+Spring+Hibernate)框架搭建流程<注解的方式创建Bean>

    此篇讲的是MyEclipse9工具提供的支持搭建自加包有代码也是相同:用户登录与注册的例子,表字段只有name,password. SSH,xml方式搭建文章链接地址:http://www.cnblo ...

  2. 最简单易懂的Spring Security 身份认证流程讲解

    最简单易懂的Spring Security 身份认证流程讲解 导言 相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,其实这是一个误区,Spring Security确 ...

  3. Spring Web MVC处理流程

    Spring Web MVC 处理流程: 1.浏览器向Spring发出请求,请求交给前端控制器 DispatcherServlet处理 2.控制器通过HandlerMapping找到相应的Contro ...

  4. Spring Bean的生命周期、Spring MVC的工作流程、IOC,AOP

    1.Spring Bean的生命周期? (1)构造方法实例化bean. (2)构造方法设置对象属性. (3)是否实现aware接口,三种接口(BeanNameAware,BeanFactoryAwar ...

  5. Spring Boot 自动装配流程

    Spring Boot 自动装配流程 本文以 mybatis-spring-boot-starter 为例简单分析 Spring Boot 的自动装配流程. Spring Boot 发现自动配置类 这 ...

  6. Spring 获取单例流程(三)

    读完这篇文章你将会收获到 Spring 何时将 bean 加入到第三级缓存和第一级缓存中 Spring 何时回调各种 Aware 接口.BeanPostProcessor .InitializingB ...

  7. Spring 获取单例流程(二)

    读完这篇文章你将会收获到 Spring 中 prototype 类型的 bean 如何做循环依赖检测 Spring 中 singleton 类型的 bean 如何做循环依赖检测 前言 继上一篇文章 S ...

  8. Spring详细基本开发流程

    LOGO 文章已托管到GitHub,大家可以去GitHub查看阅读,欢迎老板们前来Star! 搜索关注微信公众号 码出Offer 领取各种学习资料! 一.Spring概述 1.1 Web开发中的一些问 ...

  9. Spring官方都推荐使用的@Transactional事务,为啥我不建议使用!

    GitHub 17k Star 的Java工程师成神之路,不来了解一下吗! GitHub 17k Star 的Java工程师成神之路,真的不来了解一下吗! GitHub 17k Star 的Java工 ...

  10. .NET性能优化-推荐使用Collections.Pooled(补充)

    简介 在上一篇.NET性能优化-推荐使用Collections.Pooled一文中,提到了使用Pooled类型的各种好处,但是在群里也有小伙伴讨论了很多,提出了很多使用上的疑问. 所以特此写了这篇文章 ...

随机推荐

  1. Prometheus使用nginx 设置二级路径反向代理

    1.nginx 设置 location /promethues/ { proxy_pass http://10.xx.xxx.55:9090/prometheus/; } 2.设置prometheus ...

  2. aardio + PHP 可视化快速开发独立 EXE 桌面程序

    aardio 支持与很多编程语言混合开发.网络上大家分享的 aardio + Python 混合开发的文章很多,aardio + PHP 的文章却很少. 其实 aardio 与 PHP 混合开发是真的 ...

  3. 在PE文件中简单注入代码,实现在启动前弹窗

    获得的新知识: 1.kernel32.dll,user32.dll,ntdll.dll等一些dll在同一个PC环境下的映射到虚拟内存基址是一样的. 2.在win8以上系统上,更改PE文件的入口点要大于 ...

  4. 自定义ListView下拉刷新上拉加载更多

    自定义ListView下拉刷新上拉加载更多 自定义RecyclerView下拉刷新上拉加载更多 Listview现在用的很少了,基本都是使用Recycleview,但是不得不说Listview具有划时 ...

  5. SpringMvc(五) - 支付宝沙箱和关键字过滤,md5加密,SSM项目重要知识点

    1.支付宝沙箱 1.1 jar包 alipay-sdk <!-- alipay-sdk --> <dependency> <groupId>com.alipay.s ...

  6. .Net Framework中的AppDomain.AssemblyResolve事件的常见用法、问题,以及解决办法

    一.简述 本文简要的介绍.NET Framework中System.AppDomain.AssemblyResolve事件的用法.使用注意事项,以及复杂场景下AssemblyResolve事件的污染问 ...

  7. esp32把玩记-④ 星星点灯 (点亮led)

    注意 全程使用Micropython,不会安装看我第一篇文章感谢 正式开始 用Thonny烧录(运行)以下代码 import time from machine import Pin led=Pin( ...

  8. C语言经典编程100题

    程序1] 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月 后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分析: 兔子的规律为数列1,1 ...

  9. 解决springboot+vue+mybatis中,将后台数据分页显示在前台,并且根据页码自动跳转对应页码信息

    文章目录 先看效果 1.要考虑的问题,对数据进行分页查询 2.前端和后台的交互 先看效果 1.要考虑的问题,对数据进行分页查询 mapper文件这样写 从每次开始查询的位置,到每页展示的条数, < ...

  10. 配置文件yaml和ini

    前言 本文主要介绍配置文件yaml和ini的读取. 一.yaml文件 YAML是一个可读性高,易于理解,用来表达数据序列化的格式.语法与python的语法类似.文件后缀  .yaml 下面是yaml文 ...