|
|
|
|
移动端

Reactive-MongoDB异步Java Driver解读

从3.0 版本开始,MongoDB 开始提供异步方式的驱动(Java Async Driver),这为应用提供了一种更高性能的选择。

作者:唐卓章 来源:Mongoing中文社区|2020-09-17 10:34

 一、关于 异步驱动

从3.0 版本开始,MongoDB 开始提供异步方式的驱动(Java Async Driver),这为应用提供了一种更高性能的选择。

但实质上,使用同步驱动(Java Sync Driver)的项目也不在少数,或许是因为先入为主的原因(同步Driver的文档说明更加的完善),又或者是为了兼容旧的 MongoDB 版本。

无论如何,由于 Reactive 的发展,未来使用异步驱动应该是一个趋势。

在使用 Async Driver 之前,需要对 Reactive 的概念有一些熟悉。

二、理解 Reactive (响应式)

响应式(Reactive)是一种异步的、面向数据流的开发方式,最早是来自于.NET 平台上的 Reactive Extensions 库,随后被扩展为各种编程语言的实现。

在著名的 Reactive Manifesto(响应式宣言) 中,对 Reactive 定义了四个特征:

  • 及时响应(Responsive):系统能及时的响应请求。
  •  
  • 有韧性(Resilient):系统在出现异常时仍然可以响应,即支持容错。
  • 有弹性(Elastic):在不同的负载下,系统可弹性伸缩来保证运行。
  • 消息驱动(Message Driven):不同组件之间使用异步消息传递来进行交互,并确保松耦合及相互隔离。

在响应式宣言的所定义的这些系统特征中,无一不与响应式的流有若干的关系,于是乎就有了 2013年发起的 响应式流规范(Reactive Stream Specification)。

https://www.reactive-streams.org/

其中,对于响应式流的处理环节又做了如下定义:

  • 具有处理无限数量的元素的能力,即允许流永不结束
  • 按序处理
  • 异步地传递元素
  • 实现非阻塞的负压(back-pressure)

Java 平台则是在 JDK 9 版本上发布了对 Reactive Streams 的支持。

下面介绍响应式流的几个关键接口:

  • Publisher

Publisher 是数据的发布者。Publisher 接口只有一个方法 subscribe,用于添加数据的订阅者,也就是 Subscriber。

  • Subscriber

Subscriber 是数据的订阅者。Subscriber 接口有4个方法,都是作为不同事件的处理器。在订阅者成功订阅到发布者之后,其 onSubscribe(Subscription s) 方法会被调用。

Subscription 表示的是当前的订阅关系。

当订阅成功后,可以使用 Subscription 的 request(long n) 方法来请求发布者发布 n 条数据。发布者可能产生3种不同的消息通知,分别对应 Subscriber 的另外3个回调方法。

数据通知:对应 onNext 方法,表示发布者产生的数据。

错误通知:对应 onError 方法,表示发布者产生了错误。

结束通知:对应 onComplete 方法,表示发布者已经完成了所有数据的发布。

在上述3种通知中,错误通知和结束通知都是终结通知,也就是在终结通知之后,不会再有其他通知产生。

  • Subscription

Subscription 表示的是一个订阅关系。除了之前提到的 request 方法之外,还有 cancel 方法用来取消订阅。需要注意的是,在 cancel 方法调用之后,发布者仍然有可能继续发布通知。但订阅最终会被取消。

这几个接口的关系如下图所示:

图片出处:http://wiki.jikexueyuan.com/index.php/project/reactor-2.0/05.html

MongoDB 的异步驱动为 mongo-java-driver-reactivestreams 组件,其实现了 Reactive Stream 的上述接口。

> 除了 reactivestream 之外,MongoDB 的异步驱动还包含 RxJava 等风格的版本,有兴趣的读者可以进一步了解

http://mongodb.github.io/mongo-java-driver-reactivestreams/1.11/getting-started/quick-tour-primer/

三、使用示例

接下来,通过一个简单的例子来演示一下 Reactive 方式的代码风格:

A. 引入依赖

  1. org.mongodb 
  2.    mongodb-driver-reactivestreams 
  3.    1.11.0 

> 引入mongodb-driver-reactivestreams 将会自动添加 reactive-streams, bson, mongodb-driver-async组件

B. 连接数据库

  1. //服务器实例表List servers =newArrayList(); 
  2. servers.add(newServerAddress("localhost",27018));//配置构建器MongoClientSettings.Builder settingsBuilder =MongoClientSettings.builder();//传入服务器实例 
  3. settingsBuilder.applyToClusterSettings( 
  4.         builder -> builder.hosts(servers));//构建 Client 实例MongoClient mongoClient =MongoClients.create(settingsBuilder.build()); 

C. 实现文档查询

  1. //获得数据库对象MongoDatabase database = client.getDatabase(databaseName);//获得集合MongoCollection collection = database.getCollection(collectionName);//异步返回PublisherFindPublisher publisher = collection.find();//订阅实现 
  2. publisher.subscribe(newSubscriber(){ 
  3.     @Override 
  4.     publicvoid onSubscribe(Subscription s){ 
  5.         System.out.println("start..."); 
  6.         //执行请求 
  7.         s.request(Integer.MAX_VALUE); 
  8.  
  9.     } 
  10.     @Override 
  11.     publicvoid onNext(Document document){ 
  12.         //获得文档 
  13.         System.out.println("Document:"+ document.toJson()); 
  14.     } 
  15.  
  16.     @Override 
  17.     publicvoid onError(Throwable t){ 
  18.         System.out.println("error occurs."); 
  19.     } 
  20.  
  21.     @Override 
  22.     publicvoid onComplete(){ 
  23.         System.out.println("finished."); 
  24.     }}); 

注意到,与使用同步驱动不同的是,collection.find()方法返回的不是 Cursor,而是一个 FindPublisher对象,这是Publisher接口的一层扩展。

而且,在返回 Publisher 对象时,此时并没有产生真正的数据库IO请求。真正发起请求需要通过调用 Subscription.request()方法。

在上面的代码中,为了读取由 Publisher 产生的结果,通过自定义一个Subscriber,在onSubscribe 事件触发时就执行 数据库的请求,之后分别对 onNext、onError、onComplete进行处理。

尽管这种实现方式是纯异步的,但在使用上比较繁琐。试想如果对于每个数据库操作都要完成一个Subscriber 逻辑,那么开发的工作量是巨大的。

为了尽可能复用重复的逻辑,可以对Subscriber的逻辑做一层封装,包含如下功能:

  • 使用 List 容器对请求结果进行缓存
  • 实现阻塞等待结果的方法,可指定超时时间
  • 捕获异常,在等待结果时抛出

代码如下:

  1. publicclassObservableSubscriberimplementsSubscriber{ 
  2.  
  3.     //响应数据 
  4.     privatefinalList received; 
  5.     //错误信息 
  6.     privatefinalList errors; 
  7.     //等待对象 
  8.     privatefinalCountDownLatch latch; 
  9.     //订阅器 
  10.     privatevolatileSubscription subscription; 
  11.     //是否完成 
  12.     privatevolatileboolean completed; 
  13.  
  14.     publicObservableSubscriber(){ 
  15.         this.received =newArrayList(); 
  16.         this.errors =newArrayList(); 
  17.         this.latch =newCountDownLatch(1); 
  18.     } 
  19.  
  20.     @Override 
  21.     publicvoid onSubscribe(finalSubscription s){ 
  22.         subscription = s; 
  23.     } 
  24.  
  25.     @Override 
  26.     publicvoid onNext(final T t){ 
  27.         received.add(t); 
  28.     } 
  29.  
  30.     @Override 
  31.     publicvoid onError(finalThrowable t){ 
  32.         errors.add(t); 
  33.         onComplete(); 
  34.     } 
  35.  
  36.     @Override 
  37.     publicvoid onComplete(){ 
  38.         completed =true
  39.         latch.countDown(); 
  40.     } 
  41.  
  42.     publicSubscription getSubscription(){ 
  43.         return subscription; 
  44.     } 
  45.  
  46.     publicList getReceived(){ 
  47.         return received; 
  48.     } 
  49.  
  50.     publicThrowable getError(){ 
  51.         if(errors.size()>0){ 
  52.             return errors.get(0); 
  53.         } 
  54.         returnnull; 
  55.     } 
  56.  
  57.     publicboolean isCompleted(){ 
  58.         return completed; 
  59.     } 
  60.  
  61.     /** 
  62.      * 阻塞一定时间等待结果 
  63.      * 
  64.      * @param timeout 
  65.      * @param unit 
  66.      * @return 
  67.      * @throws Throwable 
  68.      */ 
  69.     publicListget(finallong timeout,finalTimeUnit unit)throwsThrowable{ 
  70.         return await(timeout, unit).getReceived(); 
  71.     } 
  72.  
  73.     /** 
  74.      * 一直阻塞等待请求完成 
  75.      * 
  76.      * @return 
  77.      * @throws Throwable 
  78.      */ 
  79.     publicObservableSubscriber await()throwsThrowable{ 
  80.         return await(Long.MAX_VALUE,TimeUnit.MILLISECONDS); 
  81.     } 
  82.  
  83.     /** 
  84.      * 阻塞一定时间等待完成 
  85.      * 
  86.      * @param timeout 
  87.      * @param unit 
  88.      * @return 
  89.      * @throws Throwable 
  90.      */ 
  91.     publicObservableSubscriber await(finallong timeout,finalTimeUnit unit)throwsThrowable{ 
  92.         subscription.request(Integer.MAX_VALUE); 
  93.         if(!latch.await(timeout, unit)){ 
  94.             thrownewMongoTimeoutException("Publisher onComplete timed out"); 
  95.         } 
  96.         if(!errors.isEmpty()){ 
  97.             throw errors.get(0); 
  98.         } 
  99.         returnthis; 
  100.     }} 

借助这个基础的工具类,我们对于文档的异步操作就变得简单多了。

比如对于文档查询的操作可以改造如下:

  1. ObservableSubscriber subscriber =newObservableSubscriber(); 
  2. collection.find().subscribe(subscriber);//结果处理 
  3. subscriber.get(15,TimeUnit.SECONDS).forEach( d ->{ 
  4.     System.out.println("Document:"+ d.toJson());}); 

当然,这个例子还有可以继续完善,比如使用 List 作为缓存,则要考虑数据量的问题,避免将全部(或超量) 的文档一次性转入内存。

作者:唐卓章

华为技术专家,多年互联网研发/架设经验,关注NOSQL 中间件高可用及弹性扩展,在分布式系统架构性能优化方面有丰富的实践经验,目前从事物联网平台研发工作,致力于打造大容量高可用的物联网服务。

本文转载自微信公众号「 Mongoing中文社区」,可以通过以下二维码关注。转载本文请联系 Mongoing中文社区公众号。

【编辑推荐】

  1. 黑客入侵MongoDB数据库 被入侵数据占总数据库47%
  2. 如何用Spring WebFlux构建Reactive REST API
  3. 开源 5 款超好用的数据库 GUI 带你玩转 MongoDB、Redis、SQL 数据库
  4. 上亿数据怎么玩深度分页?兼容MySQL + ES + MongoDB
  5. MongoDB 事务,复制和分片的关系
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

5人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

172人订阅学习

搭建数据中心实验Lab

搭建数据中心实验Lab

实验平台Datacenter
共5章 | ITGO(老曾)

108人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微