Q:为何要采用异步编程
A:异步编程首先不会节约线程,因为异步操作都会重新开一个线程。异步编程是提高了CPU的使用率,采用同步编程的方式,整个服务器的所有线程大部分都没有在工作,而是在等待。因为线程同步操作
要等整个事件处理完成才能提交,所以CPU的利用率很低;当采用异步编程,线程不需要等待,减少时间分片的占用,提高系统的吞吐量。在超高请求数量场景下,异步的实现不再需要线程等待执行结果,只需要个位数量的线程,即可实现同步场景大量线程一样的吞吐量。
举例:
实现一个转账的微服务 Transfer( accountFrom, accountTo, amount),这个服务有三个参数:
分别是转出账户、转入账户和转账金额。
实现过程也比较简单,我们要从账户 A 中转账 100 元到账户 B 中:先从 A 的账户中减去 100 元;再给 B 的账户加上 100 元,转账完成。
采用同步操作:
Transfer(accountFrom, accountTo, amount) {
// 先从accountFrom的账户中减去相应的钱数
Add(accountFrom, -1 * amount)
// 再把减去的钱数加到accountTo的账户中
Add(accountTo, amount)
return OK
}
同步操作我们很容易想到的,那同步操作的效率怎么样呢?
假设内部add方法执行效率在50ms,那一个线程执行烧水泡茶的时延在100ms,按照这种实现,每个线程每分钟最多处理10个请求,假设我们使用的服务器同时打开的线程数量上限是 10,000,
可以计算出这台服务器每秒钟可以处理的请求上限是: 10,000 (个线程)* 10(次请求每秒) = 100,000 次每秒。如果请求速度超过这个值,那么请求就不能被马上处理,只能阻塞或者排队,
这时候 Transfer 服务的响应时延由 100ms 延长到了:排队的等待时延 + 处理时延 (100ms)。也就是说,在大量请求的情况下,我们的微服务的平均响应时延变长了。 这已经到CPU的极限了吗?
其实不然,如果我们监测一下服务器的各项指标,会发现无论是 CPU、内存,还是网卡流量或者是磁盘的 IO 都空闲的很,多大部分线程都在等待每一个服务返回结果。
采用异步操作:
是在线程模型上由同步顺序调用改为了异步调用和回调的机制。
接下来,我们来看下,如何用 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); } /** * 转账服务的实现 */ 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)); } }
我们先调用一次账户服务 accountService.add() 方法从 fromAccount 扣减响应的金额,因为 add() 方法返回的就是一个 CompletableFeture 对象,可以用 CompletableFeture 的 thenCompose() 方法将下一次调用 accountService.add() 串联起来,实现异步依次调用两次账户服务完整转账。