消息队列高手课
李玥
美团高级技术专家
52199 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 42 讲
进阶篇 (21讲)
消息队列高手课
15
15
1.0x
00:00/00:00
登录|注册

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

异步实现的性能
无意义的等待
线程资源限制
性能表现
采用异步实现解决等待问题
同步实现的性能瓶颈
思考题
小结
简单实用的异步框架: CompletableFuture
异步设计如何提升系统性能?
如何使用异步设计提升系统性能?

该思维导图由 AI 生成,仅供参考

你好,我是李玥,这一讲我们来聊一聊异步。
对于开发者来说,异步是一种程序设计的思想,使用异步模式设计的程序可以显著减少线程等待,从而在高吞吐量的场景中,极大提升系统的整体性能,显著降低时延。
因此,像消息队列这种需要超高吞吐量和超低时延的中间件系统,在其核心流程中,一定会大量采用异步的设计思想。
接下来,我们一起来通过一个非常简单的例子学习一下,使用异步设计是如何提升系统性能的。

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

假设我们要实现一个转账的微服务 Transfer( accountFrom, accountTo, amount),这个服务有三个参数:分别是转出账户、转入账户和转账金额。
实现过程也比较简单,我们要从账户 A 中转账 100 元到账户 B 中:
先从 A 的账户中减去 100 元;
再给 B 的账户加上 100 元,转账完成。
对应的时序图是这样的:
在这个例子的实现过程中,我们调用了另外一个微服务 Add(account, amount),它的功能是给账户 account 增加金额 amount,当 amount 为负值的时候,就是扣减响应的金额。
需要特别说明的是,在这段代码中,我为了使问题简化以便我们能专注于异步和性能优化,省略了错误处理和事务相关的代码,你在实际的开发中不要这样做。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

异步设计思想是一种能够显著提升系统性能的程序设计思路。通过对比同步实现和异步实现的性能表现,本文展示了异步设计的优势。异步设计通过异步调用和回调的机制,极大地提升了系统的整体性能,减少了线程等待,从而提高了系统的吞吐能力,降低了响应时延。文章介绍了Java中常用的异步框架CompletableFuture的使用方法,并通过一个转账微服务例子展示了其实现过程。异步编程模型虽然不能加快程序本身的速度,但可以减少或避免线程等待,只用很少的线程就可以达到超高的吞吐能力。然而,异步实现的复杂度较高,代码的可读性和可维护性会显著下降。因此,在系统的业务逻辑比较复杂且性能足够满足业务需求的情况下,采用符合人类自然的思路且易于开发和维护的同步模型是更加明智的选择。异步设计思想对于需要超高吞吐量和超低时延的中间件系统尤为重要,能够有效提升系统性能。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《消息队列高手课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(66)

  • 最新
  • 精选
  • 小伟
    置顶
    个人的一点想法: 异步回调机制的本质是通过减少线程等待时占用的CPU时间片,来提供CPU时间片的利用率。 具体做法是用少数线程响应业务请求,但处理时这些线程并不真正调用业务逻辑代码,而是简单的把业务处理逻辑扔到另一个专门执行业务逻辑代码的线程后就返回了,故不会有任何等待(CPU时间片浪费)。专门执行业务逻辑的线程可能会由于IO慢导致上下文切换而浪费一些CPU时间片,但这已经不影响业务请求的响应了,而业务逻辑执行完毕后会把回调处理逻辑再扔到专门执行回调业务逻辑的线程中,这时的执行业务逻辑线程的使命已完成,线程返回,然后会去找下一个需要执行的业务逻辑,这里也没有任何等待。回调业务处理线程也是同理。 以上于《摩登时代》里的卓别林很像,每个人只做自己的那点事(卓别林只拧螺丝)。有的线程只负责响应请求(放螺丝),有的线程只负责执行业务逻辑(拧螺丝),有的线程只负责执行回调代码(敲螺丝),完成后就返回并继续执行下一个相同任务(拧完这个螺丝再找下一个需要拧的螺丝),没有相互依赖的等待(放螺丝的不等螺丝拧好就直接放下一个螺丝)。 有利就有弊,分开后是不用等别人了,但想知道之前的步骤是否已经做好了就难了。比如螺丝没有拧紧就开始敲,会导致固定不住。如果发现螺丝没拧好,敲螺丝的人就要和工头说这块板要返工,螺丝取下,重新放,重新拧,之后才能敲。 个人感觉把关联性强且无需长时间等待的操作(如大量磁盘或网络IO)打包成同步,其他用异步,这样可以在规避CPU时间片浪费的同时兼顾了一致性,降低了补偿的频率和开销。

    作者回复: 人工置顶🔝

    2019-08-22
    27
    136
  • 笑傲流云
    个人的思路,欢迎老师点评下哈。 1,调用账户失败,可以在异步callBack里执行通知客户端的逻辑; 2,如果是第一次失败,那后面的那一步就不用执行了,所以转账失败;如果是第一次成功但是第二次失败,首先考虑重试,如果转账服务是幂等的,可以考虑一定次数的重试,如果不能重试,可以考虑采用补偿机制,undo第一次的转账操作。 3,CompletableFuture默认是在ForkjoinPool commonpool里执行的,也可以指定一个Executor线程池执行,借鉴guava的ListenableFuture的时间,回调可以指定线程池执行,这样就能控制这个线程池的线程数目了。

    作者回复: 👍👍👍

    2019-08-13
    11
    93
  • senekis
    老师,我一直有一个困惑,就是想不明白为何异步可以节省线程。每次发起一个异步调用不都会创建一个新的线程吗?我理解了好几次,感觉只是异步处理线程在等待时可以让出时间片给其他线程运行啊?一直想不明白这个问题,困扰了好久,求老师解惑。

    作者回复: 太多的线程会造成频繁的cpu上下文切换,你可以想象一下,假设你的小公司只有8台电脑,你雇8个程序员一直不停的工作显然是效率最高的。考虑到程序员要休息不可能连轴转,雇佣24个人,每天三班倒,效率也还行。 但是,你要雇佣10000个人,他们还是只能用这8台电脑,大部分时间不都浪费在换人、交接工作上了吗?

    2019-08-13
    30
    69
  • 付永强
    老师可能里面过多提到线程这两个字,所以很多人把异步设计理解成节约线程,其实李玥老师这里想说明的是异步是用来提高cup的利用率,而不是节省线程。 异步编程是通过分工的方式,是为了减少了cpu因线程等待的可能,让CPU一直处于工作状态。换句话说,如果我们能想办法减少CPU空闲时间,我们的计算机就可以支持更多的线程。 其实线程是一个抽象概念,我们从物理层面理解,就是单位时间内把每毫核分配处理不同的任务,从而提高单位时间内CPU的利用率。

    作者回复: 👍👍👍 线程就是为了能自动分配CPU时间片而生的。

    2019-08-13
    6
    60
  • 我丢了一只小凳子
    老师,有一点不太懂,异步转账时,假如专门几个线程(Threads_quest)处理转账请求,其他的线程处理账户增减金额逻辑,虽然大量请求来的时候,Threads_quest 这几个线程可以接受请求之后扔给其他线程处理增减金额,但是由于请求量过大,不也会导致其他线程处理变慢吗,导致完整的处理也变慢

    作者回复: 首先,你要理解,为什么请求多了程序会变慢这个事儿。 计算的资源,比如说CPU、磁盘IO,它的处理能力是恒定的,都不会因为请求量大而“变慢”。 比如说CPU执行一次加法,任何情况下耗时都是差不多的。 我们所看到的请求量大“系统变慢”的现象,一定是因为某一种资源忙,达到了瓶颈。比如说,一个单核CPU,每做一次加法需要0.1秒,那它每秒最多做10次加法。 一下子100个程序同时都来请求CPU做一次加法,这个CPU就需要10秒才能算完。对于这个CPU,它并没变慢,仍然是每秒做10次加法。 但是对于某一个请求CPU的程序来说,它看到的现象是,我让CPU做了一次加法,它八秒才做完,看起来就是“变慢”了。 所以,程序变慢一定是因为某一个资源忙,遇到了瓶颈。同步程序因为线程数量的限制,它的瓶颈往往是线程数量。并不能发挥服务器的全部处理能力。异步程序不需要那么多线程,所以可以发挥出服务器的全部处理能力,直到把CPU或者磁盘IO打满,所以要快得多。

    2020-02-19
    5
    54
  • linqw
    尝试回答课后习题,老师有空帮忙看下哦 思考题一、如果在异步实现中,如果调用账户服务失败,可以以账单的形式将转账失败的记录下来,比如客户在转账一段时间后 查看账单就可以知道转账是否成功,只要保证转账失败,客户的钱没有少就可以。两次调用账户服务,感觉可以这样写 /** * 转账服务的实现 */ 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).exceptionally(add(fromAccount, amount)) // 然后调用 add 方法给 toAccount 增加相应金额 .thenCompose(v -> accountService.add(toAccount, amount)).exceptionally(add(toAccount, -1 * amount)); } } 思考题二、在异步实现中,回调方法OnComplete()可以在另一个线程池执行,比如rocketmq中异步实现, 再异步发送消息时,会将封装一个ResponseFuture包含回调方法、通道、请求消息id,将其请求id做为key,ResponseFuture做为value放入map中 等响应返回时,根据请求id从map中获取ResponseFuture,然后将ResponseFuture中的回调方法封装成任务放入到线程池中执行。然后执行 特定的回调方法。CompletableFuture有点需要注意的是,在不同的业务中需创建各自的线程池,否则都是共用ForkJoinPool。 写下对异步的理解,如果同步一个请求线程需要等待处理结果完,才可以处理其他请求,这样的话会导致如果请求多,创建很多线程 但是这些线程大部分都是等待处理结果,如果有后续的请求,没有其他线程及时处理会导致延迟(等待线程时间+处理时间), 会出现机器的cpu、磁盘、内存都不高(因为等待的线程是不占CPU的)很多请求超时之类的情况。异步的话,就是让线程调用处理接口就直接返回 不用等待处理结果,后续的处理结果可以用回调的形式来处理。如果对那些不需要实时拿到结果的业务就很适合,可以提高整个系统的吞吐率

    作者回复: 总结的非常好! 有一点需要改进一下,转账服务的实现中,异常处理的部分,还是需要先检查再补偿,否则有可能出现重复补偿的情况。

    2019-08-13
    2
    8
  • Better me
    对于思考题: 1、应该可以通过编程式事物来保证数据的完整性。如何将错误结果返回给客户端,感觉这边和老师上次答疑网关如何接收服务端秒杀结果有点类似,通过方法回调,在回调方法中保存下转账成功或失败 2、在异步实现中,回调方法 OnComplete()在执行OnAllDone()回调方法的那个线程,可以通过一个异步线程池去控制回调方法的线程数,如Spring中的async就是通过结合线程池来实现异步调用 看了两遍才稍微有点思路,老师有空看看

    作者回复: 👍👍👍

    2019-08-13
    8
  • timmy21
    我有一个困惑,客户端发起一个请求转账服务,此时转账服务会启动一个线程A处理该请求,然后转账再使用线程池异步调用账户服务。但是线程A还是存在,并等待处理结果的。我的问题来了,如果有10万个转账请求,转账服务还是最少开启10万个线程A的吧?

    作者回复: 线程A在异步调用完账户服务后就可以结束了,不需要等待,响应可以由其他的线程负责返回给调用者,由于这个过程中不涉及任何具体的业务逻辑,是非常快的,可以认为瞬间就结束了。所以并不需要很多的线程。

    2019-08-21
    5
  • 王建坤
    老师你好,CompletableFuture这种回调底层还是forkjoin框架,forkjoin对于io这种操作还是会阻塞线程,而且CompletableFuture默认线程数是与cpu核数一样的。在现在容器化的场景下,Cpu核数都不会很多(一般都是个位数),那么使用CompletableFuture是执行io操作是不是会更早的无响应?因为个位数的线程很快就都被阻塞了。

    作者回复: 这里说一下我的理解: CompletableFuture还不能等完全同于ForkJoin。 可以简单的理解为 CompletableFuture.then() 等于 Fork CompletableFuture.get() 等于 Join 但不是所有场景下,CompletableFuture都需要用get()结束的。也就是说,有时候是不需要调用阻塞的get()方法的。 另外,虽然CompletableFuture 默认使用 ForkJoinPool,但你完全可以给它提供一个自定义的执行器。

    2020-07-07
    4
  • 小祺
    老师,你用CompletableFuture这种方式跟我自己用一个固定大小的线程池去submit, 然后返回Future再get有什么本质的区别吗?吞吐量上呢?

    作者回复: 性能上没有区别。 区别是代码结构更清晰简单,易于维护。

    2019-09-06
    3
收起评论
显示
设置
留言
66
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部