learn-tech/专栏/消息队列高手课/28答疑解惑(二):我的100元哪儿去了?.md
2024-10-16 06:37:41 +08:00

104 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
28 答疑解惑我的100元哪儿去了
你好,我是李玥。
今天这节课,是我们的“消息队列高手课第二阶段进阶篇的最后一节课,照例,我们在每一阶段的最后,安排一节课进行热点问题的答疑,针对同学们遇到的一些共同的问题,统一来进行详细的解答。
1. 我的 100 元哪儿去了?聊聊并发调用情况下的幂等性
在期中测试中,有这样一道题。
如果可以保证以下这些操作的原子性,哪些操作在并发调用的情况下具备幂等性?
A. f(n, a):给账户 n 转入 a 元
B. f(n, a):将账户 n 的余额更新为 a 元
C. f(n, b, a):如果账户 n 当前的余额为 b 元,那就将账户的余额更新为 n 元
D. f(n, v, a):如果账户 n 当前的流水号等于 v那么给账户的余额加 a 元,并将流水号加一
这道题的正确答案是 D。很多同学都留言提问选项 B 中,将账户 n 的余额更新为 a 元,这个操作不具备幂等性吗?
如果单单只是考虑这个操作,执行一次和执行多次,对系统的影响是一样的,账户 n 的余额都是 a 元。所以,这个操作确实是幂等的。但请你注意审题,我们的题目中说的是:“哪些操作在并发调用的情况下具备幂等性?”在并发调用的情况下,我们再来看一下 B 这个选项的操作是否还具备幂等性。
假设,账户余额 100 元,依次执行 2 次转账:
将账户余额设为 200 元;
将账户余额设为 300 元;
经过两次转账后,账户余额应该是 300 元。
再次注意,我们的题目中说的是在并发调用的情况下。
按照时间顺序,就有可能会出现下面这种情况:
t0 时刻客户端发送请求:将账户余额设为 200 元。
t1 时刻服务端收到请求,账户余额由 100 元变为 200 元,然后服务端发出给客户端操作成功的响应,但是这个响应在网络传输过程中丢失了。
t2 时刻客户端发送请求:将账户余额设为 300 元。
t3 时刻服务端收到请求,账户余额由 200 元变为 300 元,然后服务端发出给客户端操作成功的响应。
t4 时刻客户端:收到“将账户余额设为 300 元”这个请求的成功响应,本次调用成功。
t5 时刻客户端由于没收到“将账户余额设为 300 元”这个请求的成功响应,重新发送请求:将账户余额设为 200 元。
t6 时刻服务端收到请求,账户余额由 300 元变为 200 元,然后服务端给客户端发出操作成功的响应。
t7 时刻客户端收到响应,本次重试调用成功。
结果,账户余额错误地变成了 200 元。
同学,请把我的 100 块钱还给我!通过这个题,我们可以总结出来,一个操作是否幂等,还跟调用顺序有关系,在线性调用情况下,具备幂等性的操作,在并发调用时,就不一定具备幂等性了。如果你在设计系统的时候,没有注意到这个细节,那系统就有可能出现我们上面这个例子中的错误,在生产系中,这是非常危险的。
2. Kafka 和 RocketMQ 如何通过选举来产生新的 Leader
在《[22 | Kafka 和 RocketMQ 的消息复制实现的差异点在哪?]》这节课中,我给你讲了这两个消息队列是如何通过复制来保证数据一致性的。当 Broker 节点发生故障时,它们都是通过选举机制,来选出新的 Leader 来继续提供服务。当时限于篇幅,我们并没有深入进去来讲选举的实现原理。那 Kafka 和 RocketMQDledger都是怎么来实现的选举呢
先来说 Kafka 的选举,因为 Kafka 的选举实现比较简单。严格地说Kafka 分区的 Leader 并不是选举出来的,而是 Controller 指定的。Kafka 使用 ZooKeeper 来监控每个分区的多个副本如果发现某个分区的主节点宕机了Controller 会收到 ZooKeeper 的通知这个时候Controller 会从 ISR 节点中选择一个节点,指定为新的 Leader。
在 Kafka 中 Controller 本身也是通过 ZooKeeper 选举产生的。接下来我要讲的Kafka Controller 利用 ZooKeeper 选举的方法,你一定要记住并学会,因为这种方法非常简单实用,并且适用性非常广泛,在设计很多分布式系统中都可以用到。
这种选举方法严格来说也不是真正的“选举”,而是一种抢占模式。实现也很简单,每个 Broker 在启动后,都会尝试在 ZooKeeper 中创建同一个临时节点:/controller并把自身的信息写入到这个节点中。由于 ZooKeeper 它是一个可以保证数据一致性的分布式存储,所以,集群中只会有一个 Broker 抢到这个临时节点,那它就是 Leader 节点。其他没抢到 Leader 的节点,会 Watch 这个临时节点,如果当前的 Leader 节点宕机,所有其他节点都会收到通知,它们会开始新一轮的抢 Leader 游戏。
这就好比有个玉玺,也就是皇帝用的那个上面雕着龙纹的大印章,谁都可以抢这个玉玺,谁抢到谁做皇帝,其他没抢到的人也不甘心,时刻盯着这个玉玺,一旦现在这个皇帝驾崩了,所有人一哄而上,再“抢”出一个新皇帝。这个算法虽然不怎么优雅,但胜在简单直接,并且快速公平,是非常不错的选举方法。
但是这个算法它依赖一个“玉玺”,也就是一个可以保证数据一致性的分布式存储,这个分布式存储不一定非得是 ZooKeeper可以是 Redis可以是 MySQL也可以是 HDFS只要是可以保证数据一致性的分布式存储都可以充当这个“玉玺”所以这个选举方法的适用场景也是非常广泛的。
再来说 RocketMQ/Dledger 的选举,在 Dledger 中的 Leader 真的是通过投票选举出来的,所以它不需要借助于任何外部的系统,仅靠集群的节点间投票来达成一致,选举出 Leader。一般这种自我选举的算法为了保证数据一致性、避免集群分裂算法设计的都非常非常复杂我们不太可能自己来实现这样一个选举算法所以我在这里不展开讲。Dledger 采用的是Raft 一致性算法,感兴趣的同学可以读一下这篇经典的论文。
像 Raft 这种自我选举的算法,相比于上面介绍的抢占式选举,优点是不需要借助外部系统,完全可以实现自我选举。缺点也非常明显,就是算法实在是太复杂了,非常难实现。并且,往往集群中的节点要通过多轮投票才能达成一致,这个选举过程是比较慢的,一次选举耗时几秒甚至几十秒都有可能。
我们日常在设计一些分布式的业务系统时,如果需要选举 Leader还是采用 Kafka 的这种“抢玉玺”的方法更加简单实用。
3. 为什么说 Pulsar 存储计算分离的架构是未来消息队列的发展方向?
在上节课《[27 | Pulsar 的存储计算分离设计:全新的消息队列设计思路]》中,我给你留的思考题是:为什么除了 Pulsar 以外,大多数的消息队列都没有采用存储计算分离的设计呢?这个问题其实是一个发散性的问题,并没有什么标准答案。因为,本来架构设计就是在权衡各种利弊,做出取舍和选择,并没有绝对的对错之分。
很多同学在课后的留言中,都已经给出了自己的思路和想法,而且有些同学的想法和我个人的观点不谋而合。在这里我也和你分享一下我对这个问题的理解和看法。
早期的消息队列,主要被用来在系统之间异步交换数据,大部分消息队列的存储能力都比较弱,不支持消息持久化,不提倡在消息队列中堆积大量的消息,这个时期的消息队列,本质上是一个数据的管道。
现代的消息队列,功能上看似没有太多变化,依然是收发消息,但是用途更加广泛,数据被持久化到磁盘中,大多数消息队列具备了强大的消息堆积能力,只要磁盘空间足够,可以存储无限量的消息,而且不会影响生产和消费的性能。这些消息队列,本质上已经演变成为分布式的存储系统。
理解了这一点,你就会明白,为什么大部分消息队列产品,都不使用存储计算分离的设计。为一个“分布式存储系统”做存储计算分离,计算节点就没什么业务逻辑需要计算的了。而且,消息队列又不像其他的业务系统,可以直接使用一些成熟的分布式存储系统来存储消息,因为性能达不到要求。分离后的存储节点承担了之前绝大部分功能,并且增加了系统的复杂度,还降低了性能,显然是不划算的。
那为什么 Pulsar 还要采用这种存储和计算分离的设计呢我们还是需要用发展的眼光看问题。我在上节课说过Pulsar 的这种架构,很可能代表了未来消息队列的发展方向。为什么这么说呢?你可以看一下现在各大消息队列的 Roadmap发展路线图Kafka 在做 Kafka StreamsPulsar 在做 Pulsar Functions其实大家都在不约而同的做同一件事儿就是流计算。
原因是什么呢?现有的流计算平台,包括 Storm、Flink 和 Spark它们的节点都是无状态的纯计算节点是没有数据存储能力的。所以现在的流计算平台它很难做大量数据的聚合并且在数据可靠性保证、数据一致性、故障恢复等方面也做得不太好。
而消息队列正好相反,它很好地保证了数据的可靠性、一致性,但是 Broker 只具备存储能力,没有计算的功能,数据流进去什么样,流出来还是什么样。同样是处理实时数据流的系统,一个只能计算不能存储,一个只能存储不能计算,那未来如果出现一个新的系统,既能计算也能存储,如果还能有不错的性能,是不是就会把现在的消息队列和流计算平台都给替代了?这是很有可能的。
对于一个“带计算功能的消息队列”来说,采用存储计算分离的设计,计算节点负责流计算,存储节点负责存储消息,这个设计就非常和谐了。
到这里,我们课程的第二个模块–进阶篇,也就全部结束了。进阶篇的中讲解知识有一定的难度,特别是后半部分的几节源码分析课,从评论区同学们的留言中,我也能感受到,有些同学学习起来会有些吃力。
我给同学们的建议是,除了上课时听音频和读文稿之外,课后还要自己去把源代码下载下来,每一个流程从头到尾读一遍源码,最好是打开单步调试模式,一步一步地跟踪一下执行过程。读完源码之后,还要把类图、流程图或者时序图画出来,只有这样才能真正理解实现过程。
从下节课开始,我们的课程就进入最后一个模块:案例篇。在这个模块中,我会带你一起动手来写代码,运用我们在课程中所学的知识,来做一些实践的案例。首先我会带你一起做一个消息队列和流计算的案例,你可以来体会一下现在的流计算平台它是什么样的。然后,我们还会用进阶篇中所学到的知识,来一起实现一个类似 Dubbo 的 RPC 框架。