面试官:你来举个栗子,如何使用异步设计提升系统性能?

Java实现异步的原因、场景、实现异步的方式 有时某一个耗时很长的异步任务执行结束之后,你想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future都是彼此之间都是孤立的,所以才有了后面的CompletableFuture,CompletableFuture就可以将多个Future串联起来形成任务流。响应式编程,其实就是对数据流的编程,而对流的处理对数据流的变化进行响应,是通过异步监听的方式来处理的。传送消息的链路称为信道。 阅读详情

最新互联网大厂面试真题、Java程序员面试策略(面试前的准备、面试中的技巧)请移步GitHub

对于开发者来说,异步是一种程序设计的思想,使用异步模式设计的程序可以显著减少线程等待,从而在高吞吐量的场景中,极大提升系统的整体性能,显著降低时延。

因此,像消息队列这种需要超高吞吐量和超低时延的中间件系统,在其核心流程中,一定会大量采用异步的设计思想。

接下来,我们一起来通过一个非常简单的例子学习一下,使用异步设计是如何提升系统性能的。

异步设计如何提升系统性能?

假设我们要实现一个转账的微服务 Transfer( accountFrom, accountTo, amount),这个服务有三个参数:分别是转出账户、转入账户和转账金额。

实现过程也比较简单,我们要从账户 A 中转账 100 元到账户 B 中:

  1. 先从 A 的账户中减去 100 元;
  2. 再给 B 的账户加上 100 元,转账完成。

对应的时序图是这样的:


在这个例子的实现过程中,我们调用了另外一个微服务 Add(account, amount),它的功能是给账户 account 增加金额 amount,当 amount 为负值的时候,就是扣减响应的金额。

需要特别说明的是,在这段代码中,我为了使问题简化以便我们能专注于异步和性能优化,省略了错误处理和事务相关的代码,你在实际的开发中不要这样做。

同步实现的性能瓶颈

首先我们来看一下同步实现,对应的伪代码如下:

Transfer(accountFrom, accountTo, amount) {
	//先从accountFrom的账户中减去相应的钱数
	Add(accountFrom, -1 * amount)
	//再把减去的钱数加到accountTo的账户中
	Add(accountTo, amount)
	return OK
}

上面的伪代码首先从 accountFrom 的账户中减去相应的钱数,再把减去的钱数加到accountTo 的账户中,这种同步实现是一种很自然方式,简单直接。那么性能表现如何呢?接下来我们就来一起分析一下性能。

假设微服务 Add 的平均响应时延是 50ms,那么很容易计算出我们实现的微服务 Transfer的平均响应时延大约等于执行 2 次 Add 的时延,也就是 100ms。那随着调用 Transfer 服务的请求越来越多,会出现什么情况呢?

在这种实现中,每处理一个请求需要耗时 100ms,并在这 100ms 过程中是需要独占一个线程的,那么可以得出这样一个结论:每个线程每秒钟最多可以处理 10 个请求。我们知道,每台计算机上的线程资源并不是无限的,假设我们使用的服务器同时打开的线程数量上限是 10,000,可以计算出这台服务器每秒钟可以处理的请求上限是: 10,000 (个线程)*10(次请求每秒) = 100,000 次每秒。

如果请求速度超过这个值,那么请求就不能被马上处理,只能阻塞或者排队,这时候Transfer 服务的响应时延由 100ms 延长到了:排队的等待时延 + 处理时延 (100ms)。也就是说,在大量请求的情况下,我们的微服务的平均响应时延变长了。

这是不是已经到了这台服务器所能承受的极限了呢?其实远远没有,如果我们监测一下服务器的各项指标,会发现无论是 CPU、内存,还是网卡流量或者是磁盘的 IO 都空闲的很,那我们 Transfer 服务中的那 10,000 个线程在干什么呢?对,绝大部分线程都在等待 Add服务返回结果。

也就是说,采用同步实现的方式,整个服务器的所有线程大部分时间都没有在工作,而是都在等待

如果我们能减少或者避免这种无意义的等待,就可以大幅提升服务的吞吐能力,从而提升服务的总体性能。

采用异步实现解决等待问题

接下来我们看一下,如何用异步的思想来解决这个问题,实现同样的业务逻辑。

TransferAsync(accountFrom, accountTo, amount, OnComplete()) {
	//异步从accountFrom的账户中减去相应的钱数,然后调用OnDebit方法。
	AddAsync(accountFrom, -1 * amount, OnDebit(accountTo, amount, OnAllDone(OnComplete())
}
//扣减账户accountFrom完成后调用
OnDebit(accountTo, amount, OnAllDone(OnComplete())) {
	//再异步把减去的钱数加到accountTo的账户中,然后执行OnAllDone方法
	AddAsync(accountTo, amount, OnAllDone(OnComplete()))
}
//转入账户accountTo完成后调用
OnAllDone(OnComplete()) {
	OnComplete()
}

细心的你可能已经注意到了,TransferAsync 服务比 Transfer 多了一个参数,并且这个参数传入的是一个回调方法 OnComplete()(虽然 Java 语言并不支持将方法作为方法参数传递,但像 JavaScript 等很多语言都具有这样的特性,在 Java 语言中,也可以通过传入一个回调类的实例来变相实现类似的功能)。

这个 TransferAsync() 方法的语义是:请帮我执行转账操作,当转账完成后,请调用OnComplete() 方法。调用 TransferAsync 的线程不必等待转账完成就可以立即返回了,待转账结束后,TransferService 自然会调用 OnComplete() 方法来执行转账后续的工作。

异步的实现过程相对于同步来说,稍微有些复杂。我们先定义 2 个回调方法:

  • OnDebit():扣减账户 accountFrom 完成后调用的回调方法;
  • OnAllDone():转入账户 accountTo 完成后调用的回调方法。

整个异步实现的语义相当于:

  1. 异步从 accountFrom 的账户中减去相应的钱数,然后调用 OnDebit 方法;
  2. 在 OnDebit 方法中,异步把减去的钱数加到 accountTo 的账户中,然后执行OnAllDone 方法;
  3. 在 OnAllDone 方法中,调用 OnComplete 方法。

绘制成时序图是这样的:


你会发现,异步化实现后,整个流程的时序和同步实现是完全一样的,区别只是在线程模型上由同步顺序调用改为了异步调用和回调的机制

接下来我们分析一下异步实现的性能,由于流程的时序和同步实现是一样,在低请求数量的场景下,平均响应时延一样是 100ms。在超高请求数量场景下,异步的实现不再需要线程等待执行结果,只需要个位数量的线程,即可实现同步场景大量线程一样的吞吐量。

由于没有了线程的数量的限制,总体吞吐量上限会大大超过同步实现,并且在服务器CPU、网络带宽资源达到极限之前,响应时延不会随着请求数量增加而显著升高,几乎可以一直保持约 100ms 的平均响应时延。

看,这就是异步的魔力。

简单实用的异步框架: CompletableFuture

在实际开发时,我们可以使用异步框架和响应式框架,来解决一些通用的异步编程问题,简化开发。Java 中比较常用的异步框架有 Java8 内置的CompletableFuture和 ReactiveX 的RxJava,我个人比较喜欢简单实用易于理解的 CompletableFuture,但是 RxJava 的功能更加强大。有兴趣的同学可以深入了解一下。

Java 8 中新增了一个非常强大的用于异步编程的类:CompletableFuture,几乎囊获了我们在开发异步程序的大部分功能,使用 CompletableFuture 很容易编写出优雅且易于维护的异步代码。

接下来,我们来看下,如何用 CompletableFuture 实现的转账服务。

首先,我们用 CompletableFuture 定义 2 个微服务的接口:

/**
*账户服务
*/
public interface AccountService {
    /**
    *变更账户金额
    * @param account账户ID
    * @param amount增加的金额,负值为减少
    */
    CompletableFuture<Void> add(int account, int amount);
}
/**
*转账服务
*/
public interface TransferService {
    /**
    *异步转账服务
    * @param fromAccount转出账户
    * @param toAccount转入账户
    * @param amount转账金额,单位分
    */
    CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount);
}

可以看到这两个接口中定义的方法的返回类型都是一个带泛型的 CompletableFeture,尖括号中的泛型类型就是真正方法需要返回数据的类型,我们这两个服务不需要返回数据,所以直接用 Void 类型就可以。

然后我们来实现转账服务:

/**
*转账服务的实现
*/
public class TransferServiceImpl implements TransferService {
	@Inject
	private  AccountService accountService;
	//使用依赖注入获取账户服务的实例
	@Override
	public CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount)
	//异步调用add方法从fromAccount扣减相应金额
	return accountService.add(fromAccount, -1 * amount)
	//然后调用add方法给toAccount增加相应金额
	.thenCompose(v -> accountService.add(toAccount, amount));
  }
}

在转账服务的实现类 TransferServiceImpl 里面,先定义一个 AccountService 实例,这个实例从外部注入进来,至于怎么注入不是我们关心的问题,就假设这个实例是可用的就好了。

然后我们看实现 transfer() 方法的实现,我们先调用一次账户服务 accountService.add()方法从 fromAccount 扣减响应的金额,因为 add() 方法返回的就是一个CompletableFeture 对象,可以用 CompletableFeture 的 thenCompose() 方法将下一次调用 accountService.add() 串联起来,实现异步依次调用两次账户服务完整转账。

客户端使用 CompletableFuture 也非常灵活,既可以同步调用,也可以异步调用。

public class Client {
	@Inject
	private TransferService transferService;
	//使用依赖注入获取转账服务的实例
	private final static int A = 1000;
	private final static int B = 1001;
	public void syncInvoke() throws ExecutionException, InterruptedException {
		//同步调用
		transferService.transfer(A, B, 100).get();
		System.out.println("转账完成!");
	}
	public void asyncInvoke() {
		//异步调用
		transferService.transfer(A, B, 100)
		   .thenRun(() -> System.out.println("转账完成!"));
	}
}

在调用异步方法获得返回值 CompletableFuture 对象后,既可以调用CompletableFuture 的 get 方法,像调用同步方法那样等待调用的方法执行结束并获得返回值,也可以像异步回调的方式一样,调用 CompletableFuture 那些以 then 开头的一系列方法,为 CompletableFuture 定义异步方法结束之后的后续操作。比如像上面这个例子中,我们调用 thenRun() 方法,参数就是将转账完成打印在控台上这个操作,这样就可以实现在转账完成后,在控制台打印“转账完成!”了。

总结

简单的说,异步思想就是,当我们要执行一项比较耗时的操作时,不去等待操作结束,而是给这个操作一个命令:“当操作完成后,接下来去执行什么。”

使用异步编程模型,虽然并不能加快程序本身的速度,但可以减少或者避免线程等待,只用很少的线程就可以达到超高的吞吐能力。

同时我们也需要注意到异步模型的问题:相比于同步实现,异步实现的复杂度要大很多,代码的可读性和可维护性都会显著的下降。虽然使用一些异步编程框架会在一定程度上简化异步开发,但是并不能解决异步模型高复杂度的问题。

异步性能虽好,但一定不要滥用,只有类似在像消息队列这种业务逻辑简单并且需要超高吞吐量的场景下,或者必须长时间等待资源的地方,才考虑使用异步模型。如果系统的业务逻辑比较复杂,在性能足够满足业务需求的情况下,采用符合人类自然的思路且易于开发和维护的同步模型是更加明智的选择。

面试官:谈谈在项目中,如何应对高并发流量 前言 应对大流量的一些思路 限流的常用方式 限流神器:Guava RateLimiter 分布式场景下的限流 前言 在实际项目中,曾经遭遇过线上5W+QPS的峰值,也在压测状态下经历过10W+QPS的大流量请求,本篇博客的话题主要就是自己对高并发流量控制的一点思考。 应对大流量的一些思路 “ 首先,我们来说一下什么是大流量? 大流量,我们很可能会冒出:TPS(每秒事务量),QPS(每秒请求量),1W+,5W+,10W+,100W+...。其实并没有一个绝对的数字,如果这个量造成了系统的压 阅读详情

相关推荐

京东前端常考面试题(附答案)

京东前端常考面试题(附答案)

m0_67614517的博客 1511

异步编程CompeletableFuture方法详解(应对各种场景)

CompeletableFuture是Java 8中用于异步编程的一个新特性,它可以帮助我们更方便地处理异步任务的结果,避免了传统的回调地狱的问题。

ggjklncffd的博客 856

使用ONNX Runtime在Java Web应用中部署深度学习模型

当应用场景需要集成深度学习模型进行推理时,直接在Java Web应用中集成深度学习框架可能会面临性能、兼容性等问题。为了将深度学习模型无缝集成到Java Web应用中,是一项广受认可且实用的方法。本篇文档将介绍如何将模型转换为ONNX格式,并通过ONNX Runtime Server进行部署,并通过Java Web应用调用以进行回归或预测任务。本方案的主要目标是实现以下功能:将PyTorch模型转换为ONNX格式。部署ONNX Runtime Server,加载ONNX模型并提供推理服务。

shysea2019的博客 8815

异步编程的使用场景和作用

异步编程开发是程序员在学习软件开发的时候需要掌握的一种编程开发方法,而今天我们就一起来了解一下,异步编程的使用场景和作用都有哪些。 使用场景 针对某些客户端的请求,在服务端可能需要针对这些请求做一些附属的事情,这些事情其实用户并不关心或者用户不需要立即拿到这些事情的处理结果,这种情况就比较适合用异步的方式处理这些事情。 作用 缩短接口响应时间,使用户的请求快速返回,用户体验更好。 避...

转错的弯,走错的路 2837

Java异步编程的应用与实践

Java 是一种高级编程语言具有可移植性、安全性和面向对象等特点。在异步编程领域它具有重要的优势和广泛的应用场景。

BXA 2059

Java异步编程

java异步编程介绍

小张的博客 2218

rxjava获取异步请求的结果_面试官:你来栗子,如何使用异步设计提升系统性能?...

推荐阅读:一线大厂为什么面试必问分布式?在一次又一次的失败中,我总结了这份万字的《MySQL性能调优笔记》并发编程详解:十三个工具类,十大设计模式,从理论基础到案例实战对于开发者来说,异步是一种程序设计的思想,使用异步模式设计程序可以显著减少线程等待,从而在高吞吐量的场景中,极大提升系统的整体性能,显著降低时延。因此,像消息队列这种需要超高吞吐量和超低时延的中间件系统,在其核心流程中,一定会大量...

weixin_39759890的博客 87

面试官视角解析如何拿到高薪offer(一)

互联网的寒冬已经开始了,想必大家已经有了切身的感受,公司内部晋升要求变高、名额变少,外部机会的要求也水涨船高,并且限制涨幅、职级等。 如何在较为恶劣的大环境下,从海量候选人中脱颖而出,获得心怡的工作机会,升职加薪,开启职场新篇章呢? 我们另辟蹊径,思考下,面试官会从哪些方面考察候选人呢? 整体上主要分为:专业能力、解决问题能力、沟通协作能力、成长潜力、文化匹配这五大方面。 于是我们就有了方向,在面...

达叔 3255

项目经历怎么写才能不拖后腿?3招让面试官追着问细节

栗子,别写'优化数据库',写'针对订单表亿级数据,设计冷热分离方案+异步归档策略,使查询响应时间从12s压缩至800ms'。有个兄弟在简历写'参与电商系统开发',我问他具体做了啥,他说'就是写接口'……1⃣ 把'CRUD'包装成'高性能服务',比如'日均百万订单处理系统,采用分库分表+读写分离架构'3⃣ 把'写SQL'转化成'数据分析',比如'构建用户行为模型,通过Hive优化查询效率提升3倍'2⃣ 把'调API'升级成'服务治理',比如'设计熔断降级策略,使支付服务可用性达99.99%'

2501_91346496的博客 269

面试官:怎么保证缓存和数据库一致性?

你知道的越多,不知道的就越多,业余的像一棵小草!成功路上并不拥挤,因为坚持的人不多。编辑:业余草juejin.im/post/5d5c99b66fb9a06ae072060d推荐:http...

xmt1139057136的专栏 877

【050期】面试官问:线上 5W+QPS 峰值,如何控制高并发流量?

作者:张丰哲往期精选 点击标题可。

erghtt的博客 992

【163期】面试官:你能谈谈数据库实现缓存最终一致性的一些方法吗?

程序员的成长之路互联网/程序员/技术/资料共享关注阅读本文大概需要 3.5 分钟。来自:https://juejin.im/post/6844903929281511438背景缓存是软件...

良月柒 514

一上来,面试官就问我数据库缓存一致性的四种方案!

作者:叶不闻来源:juejin.im/post/5d5c99b66fb9a06ae072060d背景缓存是软件开发中一个非常有用的概念,数据库缓存更是在项目中必然会遇到的场景。而缓存一致...

Java和Android架构 258

面试官:如果保证数据库和缓存数据的一致性?面试必问……

作者:牛牛码特 链接:https://juejin.cn/post/6844903929281511438 背景 缓存是软件开发中一个非常有用的概念,数据库缓存更是在项目中必然会遇到的场景。而缓存一致性的保证,更是在面试中被反复问到,这里进行一下总结,针对不同的要求,选择恰到好处的一致性方案。 缓存是什么 存储的速度是有区别的。缓存就是把低速存储的结果,临时保存在高速存储的技术。 如图所示,金字塔更上面的存储,可以作为下面存储的缓存。 我们本次的讨论,主要针对数据库缓存场景,将以redis作为mysql的

Java技术栈,分享最主流的Java技术 901

C++ bitset总结

数据结构bitset 术语:某1位置位是置1,某一位复位是某一位清零。 文章目录数据结构bitset一、3种初始化方式二、位访问三、位操作四、 位集操作总结实战练习 一、3种初始化方式 bitset<32> tmp; //默认构造函数,默认全0。 bitset<32> tmp(-1); //从整形数构造 bitset<32> tmp("101"); //从包含1和0的字符串构造 二、位访问 bitset<32> tmp(256)

qq_43807092的博客 701

SMART200 STEP 7-MicroWIN SMART 电脑上程序文件 项目密码解密软件.zip

SMART200电脑上程序.smart 文件 打开程序需要口令 项目密码 直接显示密码解密软件。软件永久不限制(注意是电脑上MWP程序文件,,不是去连PLC解上传密码)

上一篇: 公司新来的阿里P7大牛,只用十分钟就教会了我实现高层次的复用
下一篇: 一文详解消息队列——Kafka如何实现高性能IO?
程序员摩西
博客等级 码龄7年 44粉丝 104原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值