vert.x
异步非阻塞
1.Verticle对象和处理器(Handler)是什么关系?Vert.x如何保证Verticle内部线程安全?
一个应用程序通常是由在同一个 Vert.x 实例中同时运行的许多 Verticle 实例组合而成。不同的 Verticle 实例通过向 Event Bus 上发送消息来相互通信。一个JVM里可含多个Vert.x的实例
Verticle对象往往包含有一个或者多个处理器(Handler),在Java代码中,后者经常是以Lambda也就是匿名函数的形式出现
vertx.createHttpServer().requestHandler(req->{
//hander1
}).listen(8080);
vertx.createHttpServer().requestHandler(req->{
//hander2
}).listen(8080);
在Vert.x中,完成Verticle的部署之后,真正调用处理逻辑的入口往往是处理器(Handler),Vert.x保证同一个普通Verticle(也就是EventLoop Verticle,非Worker Verticle)内部的所有处理器(Handler)都只会由同一个EventLoop线程调用,由此保证Verticle内部的线程安全。所以我们可以放心地在Verticle内部声明各种线程不安全的属性变量,并在handler之间分享他们
public class MyVerticle extends AbstractVerticle {
int i = 0;//属性变量
public void start() throws Exception {
vertx.createHttpServer().requestHandler(req->{
i++;
req.response().end();//要关闭请求,否则连接很快会被占满
}).listen(8080);
vertx.createHttpServer().requestHandler(req->{
System.out.println(i);
req.response().end(""+i);
}).listen(8081);
}
}
Handler内部是atomic/原子操作,Verticle内部是thread safe/线程安全的,Verticle之间传递的数据是immutable/不可改变的。
2.Verticle类型
这里有3种不同类型的verticle:
Standard Verticles
这是最常见和通用的类型-他们总是使用一个 event loop thread(事件循环线程) 执行。
Worker Verticles
使用来自 worker pool(工作者线程池) 中的线程运行。一个实例永远不会被多个线程同时执行。
若您想要将 Verticle 部署成一个 Worker Verticle,您可以通过
setWorker
方法来设置:DeploymentOptions options = new DeploymentOptions().setWorker(true); vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
Worker Verticle 实例绝对不会在 Vert.x 中被多个线程同时执行,但它可以在不同时间由不同线程执行。
Multi-threaded worker verticles
使用来自 worker pool(工作者线程池) 中的线程运行。一个实例可以被多个线程同时执行。
警告:Multi-threaded Worker Verticle 是一个高级功能,大部分应用程序不会需要它。由于这些 Verticle 是并发的,您必须小心地使用标准的Java多线程技术来保持 Verticle 的状态一致性。
3.EventLoop
EventLoop 是一个在Vert.x生命周期内,不停轮询事件是否发生,并将发生的事件交给Handler予以处理的无限调度循环线程
如果Eventloop在完成一个调度循环的时间过长,就有可能导致新发生的事件得不到及时的处理,进而导致单次事件响应时间过长,影响客户体验。
为了在短时间内完成调度循环,就需要用户正确估算出,哪些程序代码会相对长时间地占用Eventloop线程的执行时间,然后将该部分代码的执行交由其它线程去处理。
一个简单粗暴的判断标准:任何涉及到IO操作的代码,都可以认为是可能造成阻塞的代码,纯粹内存操作的代码,只要执行时间没有明显超长(例如执行循环几万次的处理便可认为是执行时间超长),都可以认为是非阻塞代码
Vert.x提供了除了Eventloop线程池以外的线程池,名曰Worker线程池。此时就需要用户自行将该部分代码包装成Worker线程执行的代码,并交给Worker线程予以执行,执行完成之后再由Eventloop线程执行回调函数处理其结果。注意:Vert.x中将代码交给Worker线程执行的方式有两种,一种是通过executeBlocking函数包装,另外一种是写入Worker Verticle中。
4.Vert.x中各种Client该如何正确使用,用完是否需要关闭?
答:Vert.x中提供了各种预设客户端,例如HttpClient,JDBCClient,WebClient,MongoClient等,一般情况下,建议将客户端与Verticle对象绑定,一个Verticle对象内保留一个特定客户端的引用,并在start方法中将其实例化,这样Vert.x会在执行deployVerticle的时候执行start方法,实例化并保存该对象,在Verticle生命周期内,不需要频繁创建和关闭同类型的客户端,建议在Verticle的生命周期内对于特定领域,只创建一个客户端并复用该客户端,
public class TheVerticle extends AbstractVerticle{
//将客户端对象与Verticle对象绑定,这里选取了三种不同的客户端作为示范
HttpClient httpClient;
WebClient webClient;
JDBCClient jdbcClient;
public void start(){
//
}
}
使用客户端完之后,无需调用client.close();方法关闭客户端,频繁创建销毁客户端会在一定程度上消耗系统资源,降低性能,同时增加开发人员的负担,Vert.x提供客户端的目的就在于复用连接以减少资源消耗,提升性能,同时简化代码,减轻开发人员的负担。如您关闭客户端,在下一次使用该客户端的时候,需要重新创建客户端。
5.Vert.x中Future该如何正确使用,怎样规避回调地狱?
Future是Handler的子接口,以提供链式调用,但无法封装异步调用的结果,借助Handler的另一子接口Promise来封装异步调用的结果,并生成Future
compose(mapper)
:当前Future
完成时,执行相关代码,并返回Future
。当返回的Future
完成时,组合完成。compose(handler, next)
:当前Future
完成时,执行相关代码,并完成下一个Future
的处理。对于第二个例子,处理器需要完成
next
future,以此来汇报处理成功或者失败。您可以使用
completer
方法来串起一个带操作结果的或失败的Future
,它可使您避免用传统方式编写代码:如果成功则完成Future
,否则就标记为失败
- Future对象提供了一种异步结果的包装,用户可使用Future类中的setHandler方法来保存回调函数,然后在原先使用该回调函数的地方用completer方法予以填充,这样便可将回调函数从原参数中取出,以减少回调缩进,从而规避回调地狱。简单点说就是包装异步代码,使代码简洁。只适用于返回数据为 AsyncResult 的函数
future.compose(message ->
Future.<Message<String>>future(f ->
vertx.eventBus().send("address", message.body(), f)
)
);
//以上和以下两种写法是等效的,上述写法3.4.0+版本支持
future.compose(message ->{
Future<Message<String>> f = Future.<Message<String>>future();//可简写为Future<Message<String>> f = Future.future();
vertx.eventBus().send("address",message.body(),f.completer());//可简写为vertx.eventBus().send("address",message.body(),f);
return f;
});
- Future 接口提供了
compose
方法来链式地组合多个异步操作。
每一个 compose
方法需要传入一个 Function
,Function
的输入是前一个异步过程的返回值,需要返回一个新的需要链接的 Future
。 该 Function
方法当且仅当前一个异步流程执行成功时才会被调用。
首先通过 eventbus 发送消息
message
到address1
如果第一步成功,则发送第一步的消息的返回值到
address2
如果以上任何一步失败,则不会继续执行下一个异步流程,直接执行最终的 Handler ,并且
res.succeeded()
为false
,可以通过res.cause()
来获得异常对象.如果以上三步全都成功,则同样执行 Handler,
res.succeeded()
为true
,可以通过res.result()
获取最后一步的结果。
Future.<Message<String>>future(f ->
vertx.eventBus().send("address1", "message", f)
).compose((msg) ->
Future.<Message<String>>future(f ->
vertx.eventBus().send("address2", msg.body(), f)
)
).setHandler((res) -> {
if (res.failed()) {
//deal with exception
return;
}
//deal with the result
});;
此处的setHander方法相当于java中的finally
6.Reactor 模式和 Multi-Reactor 模式
Vert.x 的 API 都是事件驱动的,当有事件时 Vert.x 会将事件传给处理器来处理。
事件:
- 触发一个计时器
- Socket 收到了一些数据
- 从磁盘中读取了一些数据
- 发生了一个异常
- HTTP 服务器收到了一个请求
在多数情况下,Vert.x使用被称为 Event Loop 的线程来调用您的处理器。
由于Vert.x或应用程序的代码块中没有阻塞,Event Loop 可以在事件到达时快速地分发到不同的处理器中。
由于没有阻塞,Event Loop 可在短时间内分发大量的事件。例如,一个单独的 Event Loop 可以非常迅速地处理数千个 HTTP 请求。
我们称之为 Reactor 模式
每个 Vertx
实例维护的是 多个Event Loop 线程。默认情况下,我们会根据机器上可用的核数量来设置 Event Loop 的数量,您亦可自行设置。
这意味着 Vertx 进程能够在您的服务器上扩展,与 Node.js 不同。我们将这种模式称为 Multi-Reactor 模式(多反应器模式),区别于单线程的 Reactor 模式(反应器模式)。
Node.js 实现了这种模式。
在一个标准的反应器实现中,有 一个独立的 Event Loop 会循环执行,处理所有到达的事件并传递给处理器处理。
单一线程的问题在于它在任意时刻只能运行在一个核上。如果您希望单线程反应器应用(如您的 Node.js 应用
)扩展到多核服务器上,则需要启动并且管理多个不同的进程。
请注意:即使一个
Vertx
实例维护了多个 Event Loop,任何一个特定的处理器永远不会被并发执行。大部分情况下(除了 Worker Verticle 以外)它们总是在同一个 Event Loop 线程中被调用
7.运行阻塞式代码
- 可以通过调用
executeBlocking
方法(将代码交给Worker线程执行的其中一种方法)来指定阻塞式代码的执行以及阻塞式代码执行后处理结果的异步回调。
vertx.executeBlocking(future -> {
// 调用一些需要耗费显著执行时间返回结果的阻塞式API
String result = someAPI.blockingMethod("hello");
future.complete(result);
}, res -> {
System.out.println("The result is: " + res.result());
});
默认情况下,如果 executeBlocking
在同一个上下文环境中(如:同一个 Verticle 实例)被调用了多次,那么这些不同的 executeBlocking
代码块会 顺序执行(一个接一个)。
- 另外一种运行阻塞式代码的方法是使用 Worker Verticle。(将代码交给Worker线程执行的其中一种方法,具体见2.Verticle类型)
一个 Worker Verticle 始终会使用 Worker Pool 中的某个线程来执行。
默认的阻塞式代码会在 Vert.x 的 Worker Pool 中执行,通过 setWorkerPoolSize
配置。
可以为不同的用途创建不同的池:
WorkerExecutor executor = vertx.createSharedWorkerExecutor("my-worker-pool");
executor.executeBlocking(future -> {
// 调用一些需要耗费显著执行时间返回结果的阻塞式API
String result = someAPI.blockingMethod("hello");
future.complete(result);
}, res -> {
System.out.println("The result is: " + res.result());
});
Worker Executor 在不需要的时候必须被关闭:
executor.close();
当使用同一个名字创建了许多 worker 时,它们将共享同一个 pool。当所有的 worker executor 调用了 close
方法被关闭过后,对应的 worker pool 会被销毁。
8 .创建Vertx
Vertx vertx = Vertx.vertx();
Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));
VertxOptions
对象有很多配置,包括集群、高可用、池大小等。在Javadoc中描述了所有配置的细节。
集群模式
// 注意要添加对应的集群管理器依赖,详情见集群管理器章节
VertxOptions options = new VertxOptions();
Vertx.clusteredVertx(options, res -> {
if (res.succeeded()) {
Vertx vertx = res.result(); // 获取到了集群模式下的 Vertx 对象
// 做一些其他的事情
} else {
// 获取失败,可能是集群管理器出现了问题
}
});
9.创建Verticle
- Verticle 是由 Vert.x 部署和运行的代码块。默认情况一个 Vert.x 实例维护了N(默认情况下N = CPU核数 x 2)个 Event Loop 线程。Verticle 实例可使用任意 Vert.x 支持的编程语言编写,而且一个简单的应用程序也可以包含多种语言编写的 Verticle。
public class MyVerticle extends AbstractVerticle {
// Called when verticle is deployed
// Verticle部署时调用,方法完成后变成启动状态
public void start() {
}
// Optional - called when verticle is undeployed
// 可选 - Verticle撤销时调用,方法完成后变成停止状态
public void stop() {
}
}
- 您可以实现 异步版本 的
start
方法来做这个事。这个版本的方法会以一个Future
作参数被调用。方法执行完时,Verticle 实例并没有部署好(状态不是 deployed)。稍后,您完成了所有您需要做的事(如:启动其他Verticle),您就可以调用Future
的complete
(或fail
)方法来标记启动完成或失败了。
这儿有一个例子:
public class MyVerticle extends AbstractVerticle {
public void start(Future<Void> startFuture) {
// 现在部署其他的一些verticle
vertx.deployVerticle("com.foo.OtherVerticle", res -> {
if (res.succeeded()) {
startFuture.complete();
} else {
startFuture.fail(res.cause());
}
});
}
}
同样的,这儿也有一个异步版本的 stop
方法,如果您想做一些耗时的 Verticle 清理工作,您可以使用它。
public class MyVerticle extends AbstractVerticle {
public void start() {
// 做一些事
}
public void stop(Future<Void> stopFuture) {
obj.doSomethingThatTakesTime(res -> {
if (res.succeeded()) {
stopFuture.complete();
} else {
stopFuture.fail();
}
});
}
}
请注意:您不需要在一个 Verticle 的
stop
方法中手工去撤销启动时部署的子 Verticle,当父 Verticle 在撤销时 Vert.x 会自动撤销任何子 Verticle。
10.部署Verticle
- 部署方式
Verticle myVerticle = new MyVerticle();
vertx.deployVerticle(myVerticle);
也可以使用名称
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle");
// 部署JavaScript的Verticle
vertx.deployVerticle("verticles/myverticle.js");
// 部署Ruby的Verticle
vertx.deployVerticle("verticles/my_verticle.rb");
DeploymentOptions options = new DeploymentOptions().setInstances(16);//指定实例数量,跨多核扩展时很有用
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
可在部署时传给 Verticle 一个 JSON 格式的配置
JsonObject config = new JsonObject().put("name", "tim").put("directory", "/blah");
DeploymentOptions options = new DeploymentOptions().setConfig(config);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
Verticle 名称可以有一个前缀 —— 使用字符串紧跟着一个冒号,它用于查找存在的Factory,不指定前缀,Vert.x将根据提供名字后缀来查找对应Factory
js:foo.js // 使用JavaScript的Factory
- 同时Verticle的部署是异步方式,可能在
deploy
方法调用返回后一段时间才会完成部署。
如果您想要在部署完成时被通知则可以指定一个完成处理器:
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", res -> {
if (res.succeeded()) {
System.out.println("Deployment id is: " + res.result());
} else {
System.out.println("Deployment failed!");
}
});
11.撤销Verticle
我们可以通过 undeploy
方法来撤销部署好的 Verticle。
撤销操作也是异步的,因此若您想要在撤销完成过后收到通知则可以指定另一个完成处理器:
vertx.undeploy(deploymentID, res -> {
if (res.succeeded()) {
System.out.println("Undeployed ok");
} else {
System.out.println("Undeploy failed!");
}
});
12.Context 对象
每个
Verticle
在部署的时候都会被分配一个Context
(根据配置不同,可以是Event Loop Context 或者 Worker Context),之后此Verticle
上所有的普通代码都会在此Context
上执行(即对应的 Event Loop 或Worker 线程)。一个Context
对应一个 Event Loop 线程(或 Worker 线程),但一个 Event Loop 可能对应多个Context
。
您可以通过 getOrCreateContext
方法获取 Context
实例:
Context context = vertx.getOrCreateContext();
if (context.isEventLoopContext()) {
System.out.println("Context attached to Event Loop");
} else if (context.isWorkerContext()) {
System.out.println("Context attached to Worker Thread");
} else if (context.isMultiThreadedWorkerContext()) {
System.out.println("Context attached to Worker Thread - multi threaded worker");
} else if (! Context.isOnVertxThread()) {
System.out.println("Context not attached to a thread managed by vert.x");
}
当您获取了这个 Context
对象,您就可以在 Context
中异步执行代码了。换句话说,您提交的任务将会在同一个 Context
中运行:
vertx.getOrCreateContext().runOnContext(v -> {
System.out.println("This will be executed asynchronously in the same context");
});
当在同一个 Context
中运行了多个处理函数时,可能需要在它们之间共享数据。 Context
对象提供了存储和读取共享数据的方法
final Context context = vertx.getOrCreateContext();
context.put("data", "hello");
context.runOnContext((v) -> {
String hello = context.get("data");
});
Event Bus
概念
Event Bus 是 Vert.x 的神经系统。可实现不同verticle实例间的异步通信,可传递不同类型的数据,但最好使用json
支持三种通信模式
点到点
请求/响应模式
发布/订阅消息模式
- 每一个 Vert.x 实例都有一个单独的 Event Bus 实例。您可以通过
Vertx
实例的eventBus
方法来获得对应的EventBus
实例。
EventBus eb = vertx.eventBus();//对于每个Vertx来说,是单例的
- 注册处理器。当一个消息达到您的处理器,该处理器会以
message
为参数被调用。
eb.consumer("news.uk.sport", message -> {
System.out.println("I have received a message: " + message.body());
});
或者
MessageConsumer<String> consumer = eb.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("I have received a message: " + message.body());
});
同一个地址可以注册许多不同的处理器,一个处理器也可以注册在多个不同的地址上。
- 撤销处理器
consumer.unregister(res -> {
if (res.succeeded()) {
System.out.println("The handler un-registration has reached all nodes");
}
else {
System.out.println("Un-registration failed!");
}
});
-
发布消息
eventBus.publish("news.uk.sport", "Yay! Someone kicked a ball");
这个消息将会传递给所有在地址 news.uk.sport
上注册过的处理器。
- 发送消息(点对点:发送(
send
)的消息只会传递给在该地址注册的其中一个处理器)
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball");
- 添加消息头
DeliveryOptions options = new DeliveryOptions();
options.addHeader("some-header", "some-value");
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball", options);
- 应答/回复消息:当发送者需要知道消费者何时收到消息并 处理 了消息。
接收者:
MessageConsumer<String> consumer = eventBus.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("I have received a message: " + message.body());
message.reply("how interesting!");
});
发送者:
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball across a patch of grass", ar -> {
if (ar.succeeded()) {
System.out.println("Received reply: " + ar.result().body());
}
});