天天看点

消息队列面试解析系列(六)- 异步编程妙用(下)总结面试场景题快问快答

异步实现的性能

由于流程时序和同步一样,在少量请求场景下,平均响应时延一样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));    
    }
}      

先定义一个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()方法,参数就是将转账完成打印在控台上这个操作,这样就可以实现在转账完成后,在控制台打印“转账完成!”了。

总结

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

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

同时我们也需注意异步模型问题:相比同步,异步实现的复杂度大很多,代码可读性和可维护性都显著下降。

虽然使用一些异步编程框架会在一定程度上简化异步开发,但是并不能解决异步模型高复杂度的问题。

异步性能虽好,勿滥用,只有类似MQ这种业务逻辑简单且需超高吞吐量场景,或须长时等待资源,才考虑使用异步模型。

如果系统的业务逻辑比较复杂,在性能足够满足业务需求的情况下,采用符合人类自然的思路且易于开发和维护的同步模型是更加明智的选择。

面试场景题快问快答

实现转账服务时,未考虑处理失败情况。异步实现中,若调用账户服务失败,如何将错误报告给客户端?在两次调用账户服务的Add方法时,如果某一次调用失败了,该如何处理才能保证账户数据是平的?

  1. 调用账户失败,可以在异步callBack里执行通知客户端的逻辑
  2. 若是第一次失败,后面那步就不用执行了,所以转账失败;如果是第一次成功但是第二次失败,首先考虑重试,如果转账服务是幂等的,可以考虑一定次数的重试,如果不能重试,可以考虑采用补偿机制,undo第一次的转账操作。

在异步实现中,回调方法OnComplete()是在什么线程中运行的?我们是否能控制回调方法的执行线程数

CompletableFuture默认是在ForkjoinPool commonpool里执行的,也可以指定一个Executor线程池执行,借鉴guava的ListenableFuture的时间,回调可以指定线程池执行,这样就能控制这个线程池的线程数目了。

在异步实现中,回调方法 OnComplete()在执行OnAllDone()回调方法的那个线程,可通过一个异步线程池控制回调方法的线程数,如Spring中的async就是通过结合线程池来实现异步。