响应式编程框架(Projecct reactor)在物联网项目中的应用

1,658 阅读7分钟

名词解释

  • 物联网项目

顾名思义,物理网项目就是针对物联网产品的特性拓展功能,发展业务的的项目。那么如今的物联网项目都有哪些特性呢,通过对物联网定义和架构的分析,得出 物联网的核心公功能就是信息数据的传输与处理,所以,要确保高效运作,物联网需要有全感知,可靠传输,智能处理三大特点。

1)全感知。“知觉是物联网的核心。IoT是由物品和人组成,具有很强的感知能力。为让物品有知觉,需要在物品上安装不同种类的识别装置,如电子标签.条码,二维码等,同时可通过温湿度传感器.红外线传感器.照相机等识别设备感知它的物理属性和个性化特征。通过这类设备,可以随时随地获得物品信息,实现全方位感知。

2)可靠传输。而数据传输的稳定可靠,是保证物物相联的关键。因为物联网是一种异构网络,不同实体之间的协议格式(规范)可能会有差异,所以需要通过相应的软硬件来实现协议格式转换,以确保项目间信息的实时、准确地传输。为实现不同传感器数据的统一处理,实现了物物间的信息交互,必须开发支持多种协议格式转换的通信网关。利用通信网关,将不同传感器的通信协议转化为事先约定的、统一的通信协议。

3)智能加工。其目标是实现对各种物品、人员的智能识别、定位、追踪、监测、管理等功能。因此,必须以智能信息处理平台为支撑,通过云(海)计算、人工智能等智能计算技术来存储、分析、处理海量数据,并根据不同的应用需求,对物品和人员进行智能控制。

  • 响应式编程

reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s)

这是响应式编程在维基百科的解释, 响应式编程是一个专注于数据流和变化传递的异步编程范式。 这意味着使用编程语言可以很容易地表示静态(例如数组)或动态(例如事件发射器)数据流。这里的数据流与变化传递可以理解为我们一个程序从入参到返回结果的整个链路中,各函数或代码块对数据的操作及传递过程,而响应 式编程的作用就是使这个过程编程异步及非阻塞的。

reactor 框架

Reactor 3框架是Pivotal(Spring 母公司)基于Reactive Programming思想实现的。它实现了Reactive Streams(该规范由 Netflix、TypeSafe、Pivotal等公司发起的响应式规范)。其他诸如RxJava 2, Akka Streams, Vert.x和Ratpack也都实现了该规范。

Reactor有一个很重要概念的就是backpressure。 由于生产者消费者处理数据的能力不对等,很容易产生下游消费能力过载的问题。这就需要一个backpressure处理,来告诉上游生产者避免过载。打个比方,一个人负责放水,一个人负责接水,如果放水的速度太快,水桶势必会溅出来,接水的人会根据情况来告诉放水的人什么速度最合适,并且在快满的时候告知放水人关闭开关。

Reactor还添加了运算符的概念,这些运算符被链接在一起以描述在每个阶段对数据应用的处理。应用运算符返回一个中间Publisher(实际上,它可以被认为是上游运算符的订阅者和下游的发布者)。数据的最终归纳点在最终Subscriber中(这里还定义了用户角度的业务逻辑)。 一个最小单元的Rrrreacctor流程,如下图

image.png

应用

通过上面对响应式编程与响应式编程框架的了解,不难得出响应式编程具有很多优点,如 非阻塞大大简化多线程异步编程,集成netty等跨家可以实现嘻嘻昂贵更高的问我网络并发处理能力,API丰富可以便捷的实现很多复杂的功能,只需要几行代码。比如

  1. 前端展示实时数据处理进度
  2. 请求从撤销,可获取刀链接断开事件
  3. 定时(internal),延迟(delay),超时(timeout),以及细粒度的流量控制(limitRate)
  4. 分组(goupBy),聚合(collecct,reduce)操作等

这些优点与物联网项目,可靠传输,实时响应的要求不谋而合,所以响应式编程在物联网项目中逐渐应用开来,像大型开物联网项目 JetLinks 就是响应式编程优秀的实践者。

JetLinks使用Project Reactor (opens new window)作为响应式编程框架,从网络层(webflux,vert.x)到持久层(r2dbc,elastic)全部 封装为非阻塞,响应式调用.

编程注意事项

选择合适的操作符

系统中大量使用到了reactor,其核心类只有2个Flux(0-n个数据的流),Mono(0-1个数据的流). 摒弃传统编程的思想,熟悉Flux,Mono操作符(API),就可以很好的使用响应式编程了.

常用操作符:

  1. map: 转换流中的元素: flux.map(UserEntity::getId)
  2. mapNotNull: 转换流中的元素,并忽略null值.(reactor 3.4提供)
  3. flatMap: 转换流中的元素为新的流: flux.flatMap(this::findById)
  4. flatMapMany: 转换Mono中的元素为Flux(1个转多个): mono.flatMapMany(this::findChildren)
  5. concat: 将多个流连接在一起组成一个流(按顺序订阅) : Flux.concat(header,body)
  6. merge: 将多个流合并在一起,同时订阅流: Flux.merge(save(info),saveDetail(detail))
  7. zip: 压缩多个流中的元素: Mono.zip(getData(id),getDetail(id),UserInfo::of)
  8. then: 上游流完成后执行其他的操作.
  9. doOnNext: 流中产生数据时执行.
  10. doOnError: 发送错误时执行.
  11. doOnCancel: 流被取消时执行.如: http未响应前,客户端断开了连接.
  12. onErrorContinue: 流发生错误时,继续处理数据而不是终止整个流.
  13. defaultIfEmpty: 当流为空时,使用默认值.
  14. switchIfEmpty: 当流为空时,切换为另外一个流.
  15. as: 将流作为参数,转为另外一个结果:flux.as(this::save)

完整文档请查看官方文档(opens new window)

代码格式化

使用reactor时,应该注意代码尽量以.换行并做好相应到缩进.例如:


//错误
return paramMono.map(param->param.getString("id")).flatMap(this::findById);

//建议
return paramMono
            .map(param->param.getString("id")) 
            .flatMap(this::findById);

lamdba

避免在一个lambda中编写大量的逻辑代码,推荐参考领域模型,将具体当逻辑放到对应到实体或者领域对象中.例如:


//错误
return devicePropertyMono
        .map(prop->{
            Map<String,Object> map = new HashMap<>();
            map.put("property",prop.getProperty());
            ....
            return map;
        })
        .flatMap(this::doSomeThing)

//建议
//在DeviceProperty中编写toMap方法实现上面lambda中到逻辑.
return devicePropertyMono
        .map(DeviceProperty::toMap)
        .flatMap(this::doSomeThing)

null处理

数据流中到元素不允许为null,因此在进行数据转换到时候要注意null处理.例如:


//存在缺陷
return this.findById(id)
           .map(UserEntity::getDescription); //getDescription可能返回null,为null时会抛出空指针,

reactor 3.4后可以使用以下方式来处理可能存在null的map操作

return this.findById(id)
           .mapNotNull(UserEntity::getDescription); 

非阻塞与阻塞

默认情况下,reactor的调度器由数据的生产者(Publisher)决定,在WebFlux中则是netty的工作线程, 为了防止工作线程被阻塞导致服务崩溃,在一个请求的流中,禁止执行存在阻塞(如执行JDBC)可能的操作的,如果无法避免阻塞操作,应该指定调度器如:

paramMono
  .publishOn(Schedulers.elastic()) //指定调度器去执行下面的操作
  .map(param-> jdbcService.select(param))

上下文

在响应式中,大部分情况是禁止使用ThreadLocal的(可能造成内存泄漏).因此基于ThreadLocal的功能都无法使用,reactor中引入了上下文,在一个流中,可共享此上下文 ,通过上下文进行变量共享以例如:事务,权限等功能.例如:


//从上下文中获取
@GetMapping
public Mono<UserInfo> getCurrentUser(){
    return Mono.subscriberContext()
            .map(ctx->userService.findById(ctx.getOrEmpty("userId").orElseThrow(IllegalArgumentException::new));
}

//定义过滤器设置数据到上下文中
class MyFilter implements WebFilter{
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain){
        return chain.filter(exchange)
            .subscriberContext(Context.of("userId",...))
    }
}

注意

在开发中应该将多个流组合为一个流,而不是分别处理.例如:

//错误
return flux.doOnNext(data->this.save(data).subscribe());

//正确
return flux.flatMap(this::save);

//错误,没有将流组合在一起
request.flatMap(this::save);
Mono<Void> result = this.notifySaveSuccess();
return result;

//正确
return request
    .flatMap(this::save)
    .then(this.notifySaveSuccess());