来自:消息队列核心知识点电子书。作者的个人公众号「yes的练级攻略」
1、什么是消息队列
在计算机科学领域,消息队列和邮箱都是软件工程组件,通常用于进程间或同一进程内的线程通信。它们通过队列来传递消息-传递控制信息或内容,群组通信系统提供类似的功能。
消息队列(消息中间件)就是一个使用队列来通信的组件。
2、消息队列的作用
本质上来说是因为互联网的快速发展,业务不断扩张,促使技术架构需要不断的演进。
它常用来实现:异步处理、服务解耦、流量控制。
2.1、异步处理
调用链路越长,响应就越慢。 相对于扣库存和生成订单,积分服务和短信服务没必要这么的 “及时”。因此只需要在生成订单流程结束后,扔个消息到消息队列中就可以直接返回响应了。而且积分服务和短信服务可以并行的消费这条消息。
消息队列可以减少请求的等待,还能让服务异步并发处理,提升系统总体性能。
2.2、服务解耦
订单的下游系统在不断的扩充,为了迎合这些下游系统订单服务需要经常地修改,任何一个下游系统接口的变更可能都会影响到订单服务 。所以一般会选用消息队列来解决系统之间耦合的问题,订单服务把订单相关消息塞到消息队列中,下游系统谁要谁就订阅这个主题。这样订单服务就解放啦!
2.3、流量控制
网关的请求先放入消息队列中,后端服务尽自己最大能力去消息队列中消费请求。超时的请求可以直接返回错误。
某些后台任务,不需要及时地响应,并且业务处理复杂且流程长,那么过来的请求先放入消息队列中,后端服务按照自己的节奏处理。
上面两种情况分别对应着生产者生产过快和消费者消费过慢两种情况,消息队列都能在其中发挥很好的缓冲效果。
2.4、注意
多引入一个中间件系统的稳定性就下降一层,运维的难度抬高一层。因此要权衡利弊,系统是演进的。
3、消息队列模型
消息队列有两种模型:队列模型和发布/订阅模型。 RabbitMQ
采用队列模型, RocketMQ
和 Kafka
采用发布/订阅模型。
3.1、队列模型
生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,即每条消息只能被一个消费者消费。
3.2、发布/订阅模型
发布/订阅模型是将消息发往一个 Topic
即主题中,所有订阅了这个 Topic
的订阅者都能消费这条消息。 发布/订阅模型兼容队列模型,即只有一个消费者的情况下和队列模型基本一致。
接下来的内容都基于发布/订阅模型
4、常用术语
Producer
: 发送生产消息方称为生产者Consumer
: 接受消费消息方称为消费者Broker
: 消息队列服务端
消息从 Producer
发往 Broker
, Broker
将消息存储至本地,然后 Consumer
从 Broker
拉取消息,或者 Broker
推送消息至 Consumer
,最后消费。
为了提高并发度,往往发布/订阅模型还会引入队列或者分区的概念。即消息是发往一个主题下的某个队列或者某个分区中。 RocketMQ 中叫队列, Kafka 叫分区,本质一样。
例如某个主题下有 5 个队列,那么这个主题的并发度就提高为 5 ,同时可以有 5 个消费者并行消费该主题的消息。一般可以采用轮询或者 key hash
取余等策略来将同一个主题的消息分配到不同的队列中。
与之对应的消费者一般都有组的概念 Consumer Group
, 即消费者都是属于某个消费组的。一条消息会发往多个订阅了这个主题的消费组。
假设现在有两个消费组分别是 Group 1
和 Group 2
,它们都订阅了 Topic-a
。此时有一条消息发往Topic-a
,那么这两个消费组都能接收到这条消息。
然后这条消息实际是写入 Topic
某个队列中,消费组中的某个消费者对应消费一个队列的消息。
在物理上除了副本拷贝之外,一条消息在 Broker
中只会有一份,每个消费组会有自己的 offset
即消费点位来标识消费到的位置。在消费点位之前的消息表明已经消费过了。当然这个 offset
是队列级别的。每个消费组都会维护订阅的 Topic
下的每个队列的 offset
。
来个图看看应该就很清晰了。
5、如何保证消息不丢失
一共有三个阶段,分别是生产消息、存储消息和消费消息。我们从这三个阶段分别入手来确保消息不会丢失。 保证消息的可靠性需要三方配合。
-
生产者 需要处理好
Broker
的响应,出错情况下利用重试、报警等手段。 -
Broker
需要控制响应的时机,单机情况下是消息刷盘后返回响应,集群多副本情况下,即发送至两个副本及以上的情况下再返回响应。 -
消费者
需要在执行完真正的业务逻辑之后再返回响应给Broker
。
注意 : 消息可靠性增强了,性能就下降了,等待消息刷盘、多副本同步后返回都会影响性能。因此还是看业务,例如日志的传输可能丢那么一两条关系不大,因此没必要等消息刷盘再响应。
6、如何处理重复的消息
正常业务而言消息重复是不可避免的,因此我们只能从另一个角度来解决重复消息的问题。
幂等处理重复消息: 幂等是数学上的概念,我们就理解为同样的参数多次调用同一个接口和调用一次产生的结果是一致的。
例如这条 SQL
update t1 set money = 150 where id = 1 and money = 100;
执行多少遍 money 都是150,这就叫幂等。因此需要改造业务处理逻辑,使得在重复消息的情况下也不会影响最终的结果。
数据库数据版本控制: version
即版本号控制 。
update t1 set money = 150 where id = 1 and version = 1; # 更新完后 version + 1
7、如何保证消息的有序性
7.1、全局有序
如果要保证消息的全局有序,首先只能由一个生产者往 Topic
发送消息,并且一个 Topic
内部只能有一个队列(分区)。消费者也必须是单线程消费这个队列。这样的消息就是全局有序的!
不过一般情况下我们都不需要全局有序,即使是同步 MySQL Binlog
也只需要保证单表消息有序即可。
7.2、部分有序
因此绝大部分的有序需求是部分有序,部分有序我们就可以将 Topic
内部划分成我们需要的队列数,把消息通过特定的策略发往固定的队列中,然后每个队列对应一个单线程处理的消费者。这样即完成了部分有序的需求,又可以通过队列数量的并发来提高消息处理效率。
图中画了多个生产者,一个生产者也可以,只要同类消息发往指定的队列即可。