消息队列高手课
李玥
京东零售技术架构部资深架构师
立即订阅
8426 人已学习
课程目录
已完结 41 讲
0/4登录后,你可以任选4讲全文学习。
课前必读 (2讲)
开篇词 | 优秀的程序员,你的技术栈中不能只有“增删改查”
免费
预习 | 怎样更好地学习这门课?
基础篇 (8讲)
01 | 为什么需要消息队列?
02 | 该如何选择消息队列?
03 | 消息模型:主题和队列有什么区别?
04 | 如何利用事务消息实现分布式事务?
05 | 如何确保消息不会丢失?
06 | 如何处理消费过程中的重复消息?
07 | 消息积压了该如何处理?
08 | 答疑解惑(一) : 网关如何接收服务端的秒杀结果?
进阶篇 (21讲)
09 | 学习开源代码该如何入手?
10 | 如何使用异步设计提升系统性能?
11 | 如何实现高性能的异步网络传输?
12 | 序列化与反序列化:如何通过网络传输结构化的数据?
13 | 传输协议:应用程序之间对话的语言
14 | 内存管理:如何避免内存溢出和频繁的垃圾回收?
加餐 | JMQ的Broker是如何异步处理消息的?
15 | Kafka如何实现高性能IO?
16 | 缓存策略:如何使用缓存来减少磁盘IO?
17 | 如何正确使用锁保护共享数据,协调异步线程?
18 | 如何用硬件同步原语(CAS)替代锁?
19 | 数据压缩:时间换空间的游戏
20 | RocketMQ Producer源码分析:消息生产的实现过程
21 | Kafka Consumer源码分析:消息消费的实现过程
22 | Kafka和RocketMQ的消息复制实现的差异点在哪?
23 | RocketMQ客户端如何在集群中找到正确的节点?
24 | Kafka的协调服务ZooKeeper:实现分布式系统的“瑞士军刀”
25 | RocketMQ与Kafka中如何实现事务?
26 | MQTT协议:如何支持海量的在线IoT设备?
27 | Pulsar的存储计算分离设计:全新的消息队列设计思路
28 | 答疑解惑(二):我的100元哪儿去了?
案例篇 (7讲)
29 | 流计算与消息(一):通过Flink理解流计算的原理
30 | 流计算与消息(二):在流计算中使用Kafka链接计算任务
31 | 动手实现一个简单的RPC框架(一):原理和程序的结构
32 | 动手实现一个简单的RPC框架(二):通信与序列化
33 | 动手实现一个简单的RPC框架(三):客户端
34 | 动手实现一个简单的RPC框架(四):服务端
35 | 答疑解惑(三):主流消息队列都是如何存储消息的?
测试篇 (2讲)
期中测试丨10个消息队列热点问题自测
免费
期末测试 | 消息队列100分试卷等你来挑战!
结束语 (1讲)
结束语 | 程序员如何构建知识体系?
消息队列高手课
登录|注册

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

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

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

假设我们要实现一个转账的微服务 Transfer( accountFrom, accountTo, amount),这个服务有三个参数:分别是转出账户、转入账户和转账金额。
实现过程也比较简单,我们要从账户 A 中转账 100 元到账户 B 中:
先从 A 的账户中减去 100 元;
再给 B 的账户加上 100 元,转账完成。
对应的时序图是这样的:
在这个例子的实现过程中,我们调用了另外一个微服务 Add(account, amount),它的功能是给账户 account 增加金额 amount,当 amount 为负值的时候,就是扣减响应的金额。
需要特别说明的是,在这段代码中,我为了使问题简化以便我们能专注于异步和性能优化,省略了错误处理和事务相关的代码,你在实际的开发中不要这样做。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《消息队列高手课》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(41)

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

    作者回复: 人工置顶🔝

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

    作者回复: 👍👍👍

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

    作者回复: 太多的线程会造成频繁的cpu上下文切换,你可以想象一下,假设你的小公司只有8台电脑,你雇8个程序员一直不停的工作显然是效率最高的。考虑到程序员要休息不可能连轴转,雇佣24个人,每天三班倒,效率也还行。

    但是,你要雇佣10000个人,他们还是只能用这8台电脑,大部分时间不都浪费在换人、交接工作上了吗?

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

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

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

    作者回复: 👍👍👍

    2019-08-13
    4
  • 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
    3
  • 谢清
    学习了,一点思路,欢迎老师点评
    第一个问题:
    两次add方法保持最终一致性,第一次add失败不在调用第二次,告知客户转账失败;第一次成功调第二次失败,告知用户:转账进行中,转账对象收款中;可设置补偿策略,还是失败的话,转账后台人工介入补偿,还是不行则人工还原账户金额并告知用户:转账失败
    第二个问题:
    笑傲流云的答案不错。结合前面课程,用流量控制也可实现
    2019-08-13
    2
  • 长期规划
    同步会有等待,导致线程没活干,CPU利用率低,但创建的大量线程占着内存等资源。如果换成异步,将一个任务切成多个片段,切点是IO阻塞的地方。维护一个线程池,当执行每一个片段时,从线程池取线程。这样,同样多任务,用更少线程数就可以,线程少了,线程等待时间少,利用率提高了。
    2019-09-23
    1
  • mark
    go 语言的本质就是 用同步的方式,写出异步的代码。

    每个用户过来直接启动一个 go routine ,可以同时启动上百万的协程
    go processConn()

    而go 的内部 只启动少量线程,io 的等待全部做了异步处理。
    2019-09-20
    1
  • 业余草
    异步虽好,但使用场景有限!
    2019-08-13
    1
  • 蓝魔丶
    老师,转账例子代码中给转入账号加钱写错了吧

    作者回复: 感谢指正,我尽快让编辑小姐姐改正。

    2019-08-13
    1
  • 不似旧日
    不觉明历
    2019-11-22
  • 不似旧日
    看不懂
    2019-11-21
  • Tulane
    老师我还有点疑问想确认下, 望解答.
    如果就是在单机环境下, 不是微服务的话, 是不是说同步与异步调用, 其实是一致的, 异步并不会缩短线程等待时间
    因为同一时间下, 同步与异步, 都是有一个线程去跑service的, 线程不会处于无用的等待状态, 而是一直在处理任务
    而在微服务下, 一个方法调取另一个服务的方法, 从时间片上来看, 就是在同一时间产生了两个线程, 原方法的线程在等待, 而另一个服务的线程在执行. 微服务下的异步可以优化掉上一个等待线程.

    作者回复: 你举的那个微服务异步调用的例子是没问题的,但它和单机还是远程调用没关系,单机同样可以做异步调用。

    2019-11-15
  • xfan
    这一篇写的有点歧义,我觉得举的例子主要是网络IO方面的情况,其实发送请求本身就是串行的,不论是回调还是阻塞发送,其实都是串行的,不同的是:同步等待这种情况是利用了当前线程栈,而异步发送时创建了新线程发送,而抛弃了原有线程。从CPU时间片利用效率来说,这两者没有区别,而且异步可能会更消耗CPU时间片,因为创建线程需要。再者网络IO时间也不会减少,吞吐量也不会增加,还是走了两趟。如果真的需要减少时间增加吞吐,那需要计算好结果发送给ADD,比如-100 +80 =-20 最后发送直接发-20即可。
    2019-10-31
  • PeterLu
    异步的处理方式我理解跟现实生活里的流水线类似,比如一共有10个工人(对应于线程),把货车上的东西搬到仓库里,那如果每个人跳上车搬起货物,然后再下车,再走进仓库,这个时间很大程度上都浪费在了路上(也就是线程等待),那如果流水线式的工作,几个人专门负责把货物从车上搬下来,其余人负责吧货物搬进仓库,这样的话效率就较高了,这只是我个人的理解,不知道对不对

    作者回复: 是的,就是这个意思。

    2019-10-26
  • 开心小毛
    为什么不用同步非阻塞呢?老师文中的Add微服务是不是单纯的慢,但吞吐能力足够,我可以这样理解么?

    作者回复: 你说的“同步非阻塞”什么情况呢?在我的理解,非阻塞就是异步。

    2019-10-07
    2
  • 囊子
    看了大家的讨论,此处的异步执行应该也有一点意义,就算netty处理了所有的异步,那也是在网络传输层的异步。在业务层面,到最终发起tcp请求前也干了好多事,如果能异步处理也是好的。个人浅见。
    2019-09-28
  • 逍遥子
    这里的异步不止是业务代码上的异步吧?是不是用到了网络编程io模型中的同步非阻塞模型呢?拿多路复用模型来说,应用进程向操作系统内核发起select命令请求数据,内核创建去向另外一台机器发起tcp连接。内核获取到数据通知应用进程,进程向内核发起recvfrom命令将数据从内核拷贝到用户空间,拷贝完成应用进程就能读取到数据。
    不知这里的异步用的是哪种模型呢?

    作者回复: 同学你说的内容我们会在下一节课讲。

    2019-09-17
  • 小祺
    老师,我理解异步是可以解决请求超时的问题,但是像文中举例这种转账操作,转出转入两个操作是前后依赖的没法并行,那么这种前后依赖的任务使用异步跟同步又有什么区别呢?
    另外,当10万请求过来之后,虽然用了异步可以瞬间返回,但是其实几万个请求对象在CompletableFuture内部线程池内部还是排队啊,所以最后来的请求还是要等很久才能被执行到。那么既然同步or异步都需要排队,异步究竟快在哪里了呢?

    作者回复: 第一个问题,转入转出这两个操作不需要串行,是可以并行的。甚至执行顺序都没什么要求。我们唯一要保证的是这两个操作在一个事务中执行, “要么都成功,要么都失败”,就可以了。

    你这个场景是在调用方(转账服务)异步,而服务提供方(账户服务)还是同步服务的情况下,才会出现。

    你仔细看一下我们的异步设计,服务提供方提供的也是异步服务,那调用账户服务也是一瞬间就完成了,这样就不会出现你说的“几万个请求对象在CompletableFuture内部线程池内部还是排队”的情况了。

    2019-09-06
收起评论
41
返回
顶部