如何使用异步设计提升系统性能
异步是一种程序设计的思想,使用异步模式设计的程序可以显著减少线程等待,从而在高吞吐量的场景中,极大提升系统的整体性能,显著降低时延。
1.异步设计如何提升系统性能
eg.
实现转账的微服务Transfer(accountFrom,accountTo,amount),这个服务有三个参数:分别是转出账户、转入账户和转账金额。
实现过程:从账户A转账100元到账户B中:
1. 先从A的账户中减去100元;
2. 再给B的账户加上100元,转账完成。
时序图:
这个例子的实现过程中,调用了另外一个微服务Add(account,amount),它的主要功能是给账户account增加金额amount。
1.同步实现的性能瓶颈
同步实现的伪代码
Transfer(accountFrom, accountTo, amount) {
// 先从 accountFrom 的账户中减去相应的钱数
Add(accountFrom, -1 * amount)
// 再把减去的钱数加到 accountTo 的账户中
Add(accountTo, amount)
return OK
}
分析性能:
假设微服务Add的平均响应延时是50ms,那transfer的平均延时为2
add = 100ms。
每处理一个请求需要耗时100ms,这个过程需要独占1个线程,每个线程每秒钟处理10个请求。假设服务器同时打开线程数量为10000,这台服务器的处理上限为10000
10 = 100000 次/s
如果请求速度超过这个值,那么请求就不能被马上处理,只能阻塞或排队,这时候Transfer的响应市场由100ms延长到:排队的等待时延 + 处理时延(100ms)
结论:**采用同步实现的方式,整个服务器的所有线程大部分时间都没有在工作,而是在等待。**如果能减少或者避免这种无意义的等待,就可以大幅提升服务的吞吐能力,从而提升服务的总体性能。
2.采用异步实现解决等待问题
异步思想解决问题,代码如下:
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中,可以通过传入一个回调类的实例来变相实现类似的功能。
整个异步实现的语义相当于:
1. 异步从accountFrom的账户中减去相应的钱数,然后调用 OnDebit方法;
2. 在OnDebit方法中,异步把减去的钱数加到accountTo的账户中,然后执行OnAllDone方法;
3. 在OnAllDone方法中,调用OnComplete方法。
时序图:
整个流程的时序和同步实现完全一样,区别在
只是在线程模型上由同步顺序调用改为异步调用和回调的机制。
分析:在低请求数量的场景下,平均响应时延一样是100ms。在超高请求数量场景下,异步的实现不再需要线程等待执行结果,只需要个位数量的线程,即可实现同步场景大量线程一样的吞吐量。响应时延不会随着请求的数量增加而显著升高,几乎可以一直保持约100ms的平均响应时延。
自我理解:区别于同步时延下,减少的是等待时延的时间
2.简单实用的异步框架:CompletableFuture
java中常用的异步框架有java8内置的
CompletableFuture
和ReactiveX的
Rxjava
.
使用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);
}
实现转账服务:
/**
* 转账服务的实现
*/
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));
}
}
可以使用CompletableFuture的thenCompose()方法实现两个法的串联。
客户端使用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(" 转账完成!"));
}
}
3.小结
异步思想:
当我们要执行一项比较耗时的操作时,不去等待操作结束,而是给这个操作一个命令:“当操作完成后,接下来去执行什么。”
4.问题
-
在转账服务中,异步实现时,如果调用账户服务失败,如何将错误报告给客户端?在两次调用账户服务的Add方法时,如果某一次调用失败了,该如何处理才能保证账户数据是平的?
1,调用账户失败,可以在异步callBack里执行通知客户端的逻辑;
2,如果是第一次失败,那后面的那一步就不用执行了,所以转账失败;如果是第一次成功但是第二次失败,首先考虑重试,如果转账服务是幂等的,可以考虑一定次数的重试,如果不能重试,可以考虑采用补偿机制,undo第一次的转账操作。 -
在异步实现中,回调方法OnComlete()是在什么线程中运行的?我们是否能控制回调方法的执行线程数?
CompletableFuture默认是在ForkjoinPool commonpool里执行的,也可以指定一个Executor线程池执行,借鉴guava的ListenableFuture的时间,回调可以指定线程池执行,这样就能控制这个线程池的线程数目了