java实现同步与异步
1.简介
本教程的先前部分主要关注与微服务架构有关的或多或少的高级主题,例如多语言世界中的不同框架,通信风格和互操作性。 尽管它非常有用,但是从这一部分开始,我们将逐步扎根,将注意力集中在实际的方面,如开发人员所说,离代码更近。
我们将开始一个非常重要的讨论,讨论在实现微服务内部时可能遇到的各种范例。 对每个人所提供的适用性,好处和折衷的深刻理解将帮助您在每种特定情况下做出正确的实施选择。
2.同步
同步编程是当今最广泛使用的范例,因为它简单易懂。 在典型的应用程序中,它通常表现为一系列的函数调用,其中每个函数依次执行。 为了说明它的实际作用,让我们看一下Customer Service中 JAX-RS资源端点的实现。
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response register(@Context UriInfo uriInfo, @Valid CreateCustomer payload) {
final CustomerInfo info = conversionService.convertTo(payload, CustomerInfo.class);
final Customer customer = customerService.register(info);
return Response
.created(
uriInfo
.getRequestUriBuilder()
.path(customer.getUuid())
.build())
.build();
}
在阅读此代码时,一路走来并没有什么惊喜(除了获得异常的可能性)。 首先,我们将客户信息从RESTful Web API有效负载转换为服务对象,然后调用服务以注册新客户,最后将响应返回给调用方。 执行完成后,就可以知道其结果并进行充分评估。
那么,这种范例有什么问题呢? 令人惊讶的是,事实是当前调用必须等待上一个调用完成。 例如,如果我们必须在成功注册后向客户发送确认电子邮件该怎么办? 我们应该绝对等待确认发送出去,还是只返回回复并确保确认发送已安排? 让我们尝试在下一部分中找到正确的答案。
3.异步的
正如我们刚刚讨论的那样,为了继续执行流程,可能不一定需要某些操作的结果。 此类操作可以异步执行: 并发 , 并行执行,甚至在将来某个时候执行。 操作结果可能无法立即获得。
为了了解它的幕后工作原理,我们不得不谈论基于线程的 Java(通常是JVM)中的并发和并行性 。 Java中的任何执行都在线程上下文中进行。 这样,典型的方式来实现的特定操作的执行异步是借用线程从线程池 (或产生新的线程手动地),并在其上下文中执行的调用。
它看起来很简单,但是最好知道异步执行实际上何时完成,最重要的是,它的结果是什么。 由于我们主要关注Java,因此这里的关键成分是CompletableFuture ,它表示异步计算的结果。 它具有许多回调方法,这些回调方法允许在结果准备好时通知调用方。
资深的Java开发人员一定会记得CompletableFuture的前身Future接口。 因为它的功能非常有限,所以我们不会谈论Future也不建议使用它。
让我们返回到成功注册客户后发送确认电子邮件。 由于我们的客户服务使用CDI 2.0 ,因此将通知绑定到客户注册事件是很自然的。
@Transactional
public Customer register(CustomerInfo info) {
final CustomerEntity entity = conversionService.convertTo(info, CustomerEntity.class);
repository.saveOrUpdate(entity);
customerRegisteredEvent
.fireAsync(new CustomerRegistered(entity.getUuid()))
.whenComplete((r, ex) -> {
if (ex != null) {
LOG.error("Customer registration post-processing failed", ex);
}
});
return conversionService.convertTo(entity, Customer.class);
}
CustomerRegistered
事件是异步触发的,注册过程将继续执行,而无需等待所有观察者对其进行处理。 实现有些幼稚(因为事务可能失败,或者应用程序可能在处理事件时崩溃),但足以说明这一点: 异步性使得执行流程难以理解和推理。 更不用说它的隐性成本: 线程是宝贵而昂贵的资源。
异步调用的有趣特性是可以使超时(如果花费的时间太长)或/和请求取消(如果不再需要结果的话)的可能性。 但是,正如您所期望的,并非所有操作都可以被中断,因此某些条件适用。
4.封锁
在执行I / O操作的上下文中, 同步编程范例通常称为阻塞。 公平地说,同步和阻塞通常可以互换使用,但就我们的讨论而言,仅假定I / O操作属于此类。
确实,尽管从执行流的角度来看并没有太大区别(每个操作都必须等待上一个操作完成),但是进行I / O的机制与纯粹的计算工作却形成了鲜明的对比。 在大多数Java应用程序中,此类阻塞操作的典型示例是什么? 只要考虑一下关系数据库和JDBC驱动程序。
@Inject @CustomersDb
private EntityManager em;
@Override
public Optional findById(String uuid) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery query = cb.createQuery(CustomerEntity.class);
final Root root = query.from(CustomerEntity.class);
query.where(cb.equal(root.get(CustomerEntity_.uuid), uuid));
try {
final CustomerEntity customer = em.createQuery(query).getSingleResult();
return Optional.of(customer);
} catch (final NoResultException ex) {
return Optional.empty();
}
}
我们的客户服务实现不直接使用JDBC API,而是依赖于高级JPA规范( JSR-317 , JSR-338 )及其提供者。 尽管如此,很容易发现发生在哪里的数据库调用:
final CustomerEntity customer = em.createQuery(query).getSingleResult();
执行流程将在这里碰壁。 根据JDBC驱动程序的功能,您可以对事务或查询进行某些控制,例如取消它或设置超时。 但总的来说,这是一个阻塞的调用:只有在查询完成并提取结果后,执行才能恢复。
5.非阻塞
在I / O周期中,通常要花费大量时间等待磁盘操作或网络传输。 因此,正如我们在上一节中所看到的,执行流程必须通过阻止进一步的进展来付出代价。
既然我们已经对异步编程的概念进行了简要介绍,那么显而易见的问题是为什么不异步调用此类I / O操作? 总而言之,这是完全合情合理的,但是至少对于JVM(和Java)而言,它将问题从一个执行线程转移到另一个执行线程(例如,从专用I / O池中借用)。 从资源利用的角度来看,它仍然看起来效率很低。 甚至更多,可伸缩性也将遭受损失,因为应用程序不能无限期地产生或借用新线程。
幸运的是,有许多技术可以解决此问题,这些技术统称为非阻塞I / O (或异步I / O )。 非阻塞异步I / O的最广泛使用的实现之一是基于Reactor模式 。 下图描绘了它的简化视图。
Reactor模式的核心是单线程事件循环。 收到I / O操作请求后,它会委派给处理程序池(或委派给特定于应用程序正在运行的操作系统的更有效实现)。 I / O操作的结果可以(作为事件)注入到事件循环中,并最终在完成后将结果分配给应用程序。
在JVM上, Netty框架是实现异步,事件驱动的网络服务器和客户端的实际选择。 让我们来看看预订服务如何使用Netty之上的AsyncHttpClient库,以一种真正的非阻塞方式调用客户服务 ,以其标识符以真正的客户查找客户(省略了错误处理以使代码片段简短) 。
final AsyncHttpClient client = new DefaultAsyncHttpClient();
final CompletableFuture customer = client
.prepareGet("http://localhost:8080/api/customers/" + uuid)
.setRequestTimeout(500)
.setReadTimeout(100)
.execute()
.toCompletableFuture()
.thenApply(response -> fromJson(response.getResponseBodyAsStream()));
// ...
client.close();
有趣的是,对于调用方而言, 非阻塞调用与异步调用没有什么不同,但是如何完成调用的内部问题很重要。
6.React性
React式编程将异步和非阻塞范例提升到一个全新的水平。 React式编程实际上有很多定义,但是最引人注目的一个是这个。
React式编程是使用异步数据流进行编程。 – https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
这个相当简短的定义值得一本书 。 为了使讨论合理地简短,我们将集中讨论事物的实际方面,即React流 。
Reactive Streams是一项倡议,旨在为具有无阻塞背压的异步流处理提供标准。 – http://www.reactive-streams.org/
有什么特别之处? React流通过强调几个关键点来统一我们在应用程序中处理数据的方式:
- (大部分)一切都是流
- 这些流本质上是异步的
- 流支持无阻塞背压以控制数据流
该代码值一千个单词。 由于Spring WebFlux带有响应性,非阻塞HTTP客户端 ,因此让我们来看看预订服务如何调用客户服务以其响应性方式通过其标识符查找客户(为简单起见,省略了错误处理)。
final HttpClient httpClient = HttpClient
.create()
.tcpConfiguration(client ->
client
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
.doOnConnected(conn ->
conn
.addHandlerLast(new ReadTimeoutHandler(100))
.addHandlerLast(new WriteTimeoutHandler(100))
));
final WebClient client = WebClient
.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl("http://localhost:8080/api/customers")
.build();
final Mono customer = client
.get()
.uri("/{uuid}", uuid)
.retrieve()
.bodyToMono(Customer.class);
从概念上讲,它看起来很像AsyncHttpClient示例,只是多了一点仪式。 但是,使用React性类型(如Mono<Customer>
)可以释放React性流的全部功能。
关于React式编程的讨论不能不提及《React式宣言》及其对现代应用程序的设计和体系结构的巨大影响而完成。
…我们认为需要一种一致的系统架构方法,并且我们认为所有必要方面都已经被单独识别:我们希望系统具有响应性,弹性,弹性和消息驱动性。 我们称这些React系统。
作为React式系统构建的系统更加灵活,松耦合和 可扩展 。 这使它们更易于开发且易于更改。 他们对失败的容忍度更高,当 确实发生 失败 时,他们会优雅地面对而不是灾难。 React性系统具有高响应能力,可为 用户提供 有效的交互式反馈。 – https://www.reactivemanifesto.org/
React式系统的基本原理和前景非常适合微服务体系结构 ,催生了一类新的微服务 ,即React式微服务 。
7.未来是光明的
在过去的几年中,Java的创新步伐已大大提高。 有了新的发布节奏,每6个月Java开发人员就可以使用这些新功能。 但是,有许多正在进行的项目可能会对JVM和Java的未来产生巨大影响。
其中之一就是Loom项目 。 该项目的目的是探索轻量级用户模式线程 ( 光纤 ),定界的延续 (某种形式)以及相关功能的实现。 到目前为止,尽管某些库(例如Parallel Universe的 Quasar)正在尝试弥补这一空白,但JVM本身并不支持光纤 。
另外,引入光纤作为线程的替代,将有可能在JVM上有效支持协程 。
8.实施微服务–结论
在本教程的这一部分中,我们讨论了在实现微服务时可能要考虑的不同范例。 从传统的将执行流程构造为一系列连续的阻塞步骤的传统方式,到了React流 。
起初,思考和编写代码的异步和React式方法可能看起来很困难。 不必再担心了,但这并不意味着您所有的微服务都必须是React式的 。 每种选择都是一个折衷,由您决定在微服务架构和组织的背景下做出正确的选择。
9.接下来
在本教程的下一部分中,我们将讨论分布式计算的谬误以及如何减轻其对微服务体系结构的影响。
java实现同步与异步