消息队列_08(异步设计提升系统性能)

  • Post author:
  • Post category:其他




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

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



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.问题

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

    1,调用账户失败,可以在异步callBack里执行通知客户端的逻辑;

    2,如果是第一次失败,那后面的那一步就不用执行了,所以转账失败;如果是第一次成功但是第二次失败,首先考虑重试,如果转账服务是幂等的,可以考虑一定次数的重试,如果不能重试,可以考虑采用补偿机制,undo第一次的转账操作。
  2. 在异步实现中,回调方法OnComlete()是在什么线程中运行的?我们是否能控制回调方法的执行线程数?

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



版权声明:本文为qq_21486655原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。