不听反方向的钟,想要什么就冲。
消息队列项目总结
- [1 RabbitMQ解决的问题](#1 RabbitMQ解决的问题)
- [2 消息队列优缺点](#2 消息队列优缺点)
-
- [2.1 优点](#2.1 优点)
- [2.2 缺点](#2.2 缺点)
- [3 为什么选择RabbitMQ](#3 为什么选择RabbitMQ)
- [4 RabbitMQ结构](#4 RabbitMQ结构)
- [4 为什么使用Channel信道机制](#4 为什么使用Channel信道机制)
- [5 消息队列执行过程](#5 消息队列执行过程)
- [6 消息队列的创建](#6 消息队列的创建)
- [7 消息的ack应答机制](#7 消息的ack应答机制)
- [8 消息队列持久化处理](#8 消息队列持久化处理)
- [9 消息Message模块](#9 消息Message模块)
- [10 消息队列的信道管理](#10 消息队列的信道管理)
- [11 RabbitMQ的集群方式](#11 RabbitMQ的集群方式)
-
- [11.1 普通模式:默认的集群模式。](#11.1 普通模式:默认的集群模式。)
- [11.2 镜像模式:把需要的队列做成镜像队列,存在于多个节点。](#11.2 镜像模式:把需要的队列做成镜像队列,存在于多个节点。)
1 RabbitMQ解决的问题
比如登录一个网站,首次登录需要进行注册:将数据储存到数据库(50ms),发送注册邮件(50ms),发送注册短信(50ms)。
- 串行执行: 客户得到响应的时间就是执行完所有任务的时间150ms!
- 并发执行: 客户得到响应的时间是数据库处理(50ms) + 并发执行发送邮件,短信(50ms)
- 消息队列:客户得到响应的时间是数据库处理(50ms) + 消息队列处理(5ms)。发送注册邮件和短袖交给消息队列进行异步处理!
对于一个大型的软件系统来说,它会有很多的组件或者说模块或者说子系统或者(Subsystem or Component or Submodule)。那么这些模块的如何通信?这和传统的IPC有很大的区别。传统的IPC很多都是在单一系统上的,模块耦合性很大,不适合扩展(Scalability);如果使用socket那么不同的模块的确可以部署到不同的机器上,但是还是有很多问题需要解决。比如:
- 信息的发送者和接收者如何维持这个连接,如果一方的连接中断,这期间的数据如何防止丢失?
- 如何降低发送者和接收者的耦合度?
- 如何让Priority高的接收者先接到数据?
- 如何做到Load balance?有效均衡接收者的负载?
- 如何有效的将数据发送到相关的接收者?也就是说将接收者subscribe 不同的数据,如何做有效的filter。
- 如何保证接收者接收到了完整,正确的数据?
- AMDQ协议解决了以上的问题,而RabbitMQ实现了AMQP。
2 消息队列优缺点
2.1 优点
流量削峰:以典型的双十一交易秒杀为例
- 消息队列可以控制活动人数,超过此一定阀值的订单直接丢弃
- 通过消息队列的缓冲,可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
- 用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面。
- 秒杀业务程序根据消息队列中的请求信息,再做后续处理。
应用解耦:交易场景下的解耦
比如网络交易时,订单系统调用库存系统的接口。如果库存系统出现了故障时,订单就会失败!此时订单系统和库存是高耦合的!
客户端请求和服务端处理数据可以通过中间的消息队列进行解耦,降低耦合度。就算服务端程序出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。
加入消息队列后
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统:订阅下单的消息,获取下单消息,进行库操作。
就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。
2.2 缺点
关于消息队列的优点也就是上面列举的,就是在特殊场景下有其对应的好处,解耦、异步、削峰。缺点有以下几个:
- 系统可用性降低
系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人家ABCD 四个系统好好的,没啥问题,你偏加个 消息队列 进来,万一 消息队列挂了咋整,消息队列一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以点击这里查看。 - 系统复杂度提高
硬生生加个 MQ 进来,你怎么[保证消息没有重复消费]?怎么[处理消息丢失的情况]?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。 - 一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
抽象的来看,消息队列可以理解为机场,每天都有很多的人(消息)需要坐飞机(消息处理)。那么机场检票口(交换机)发挥的作用就是通过登机牌(绑定信息)引导人们到指定的候机区(队列)。飞机(消费者)就绪了(订阅一个队列)就会取出飞机能力之内的乘客,然后起飞(异步处理请求)
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10 倍。但是关键时刻,用,还是得用的。
3 为什么选择RabbitMQ
- ActiveMQ,性能不是很好,因此在高并发的场景下,直接被pass掉了。它的Api很完善,在中小型互联网公司可以去使用。
- kafka,主要强调高性能,如果对业务需要可靠性消息的投递的时候。那么就不能够选择kafka了。但是如果做一些日志收集呢,kafka还是很好的。因为kafka的性能是十分好的。
- RocketMQ,它的特点非常好。它高性能、满足可靠性、分布式事物、支持水平扩展、上亿级别的消息堆积、主从之间的切换等等。MQ的所有优点它基本都满足。但是它最大的缺点:商业版收费。因此它有许多功能是不对外提供的。
4 RabbitMQ结构

- RabbitMQ Server :也叫broker server,它不是运送食物的卡车,而是一种传输服务。原话是
RabbitMQ isn't a food truck, it's a delivery service
. 他的角色就是维护一条从Producer到Consumer的传输路线,保证数据能够按照指定的方式进行传输。但是这个保证也不是100%的保证,但是对于普通的应用来说这已经足够了。当然对于商业系统来说,可以再做一层数据一致性的guard,就可以彻底保证系统的一致性了。 - Producer :生产者,数据的发送方。
Create messages and Publish (Send) them to a broker server (RabbitMQ)
。一个Message有两个部分:Payload(有效载荷)和Label(标签)。- Payload顾名思义就是传输的数据,
- Label是Exchange的名字或者说是一个tag,它描述了payload,而且RabbitMQ也是通过这个label来决定把这个Message发给哪个Consumer。
- AMQP协议仅仅描述了label,而RabbitMQ决定了如何使用这个Label的规则。
- Consumer :也叫消费者,数据的接收方。
Consumers attach to a broker server (RabbitMQ) and subscribe to a queue
。把queue比作是一个有名字的邮箱。当有Message到达某个邮箱后,RabbitMQ把它发送给它的某个订阅者即Consumer。当然可能会把同一个Message发送给很多的Consumer。在这个Message中,只有payload,label已经被删掉了。对于Consumer来说,它是不知道谁发送的这个信息的。就是协议本身不支持。但是当然了如果Producer发送的payload包含了Producer的信息就另当别论了。
对于一个数据从Producer到Consumer的正确传递,还有三个概念需要明确:exchanges(交换机), queues(队列), bindings(绑定关系)。
- Exchanges are where producers publish their messages:消息交换机,它指定消息按什么规则,路由到哪个队列
- Queues are where the messages end up and are received by consumers:消息队列载体,每个消息都会被投入到一个或多个队列
- Bindings are how the messages get routed from the exchange to particular queues:绑定,它的作用就是把exchange和queue按照路由规则绑定起来
Routing Key:路由关键字,exchange根据这个关键字进行消息投递
还有几个概念是上述图中没有标明的,那就是Connection(连接),Channel(通道,频道),Vhost(虚拟主机)。
- Connection:就是一个TCP的连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server的。以后我们可以看到,程序的起始处就是建立这个TCP连接。
- Channel:虚拟连接。它建立在上述的TCP连接中。数据流动都是在Channel中进行的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。
- Vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。每个virtual host本质上都是一个RabbitMQ Server,拥有它自己的queue,exchagne,和bings rule等等。这保证了你可以在多个不同的application中使用RabbitMQ。
4 为什么使用Channel信道机制
对于操作系统来说,建立和关闭TCP连接是有代价的,频繁的建立关闭TCP连接对于系统的性能有很大的影响,并且TCP的连接数是有限制的,那么这也会限制处理高并发的能力。
但是在TCP连接中建立Channel是没有上述代价的!
对于消费者和生产者可以并发的使用多个信道进行publish以及receive!
有实验表明,1s的数据可以Publish10K的数据包。当然对于不同的硬件环境,不同的数据包大小这个数据肯定不一样,但是我只想说明,对于普通的Consumer或者Producer来说,这已经足够了。如果不够用,你考虑的应该是如何细化split你的设计。
5 消息队列执行过程
- 客户端连接到消息队列服务器,打开一个Channel
- 客户端通过channel声明一个交换机,并设置相关属性
- 客户端通过channel声明一个队列,并设置相关属性
- 客户端使用routing_key,在交换机和queue中建立绑定关系
- 客户端投递消息到Exchange
Exchange接收到消息后,就根据消息的routing_key和已经设置的binding_key,进行消息路由,将消息投递到一个或多个队列里。有三种类型的Exchanges:direct,fanout,topic,每个实现了不同的路由算法(routing algorithm):
- Direct exchange:完全根据key进行投递的叫做Direct交换机。如果Routing key匹配, 那么Message就会被传递到相应的queue中。其实在queue创建时,它会自动的以queue的名字作为routing key来绑定那个exchange。例如,绑定时设置了Routing key为"abc",那么客户端提交的消息,只有设置了key为"abc"的才会投递到队列。
- Fanout exchange:不需要key的叫做Fanout交换机。它采取广播模式,一个消息进来时,投递到与该交换机绑定的所有队列。
- Topic exchange:对key进行模式匹配后进行投递的叫做Topic交换机。比如符号"#"匹配一个或多个词,符号 "*" 匹配正好一个词。
6 消息队列的创建
- 消费者和生产者都可以通过信道对应的接口创建队列。
- 消费者没有权限去删除一个队列!但是可以订阅其他队列,也可以创建私有的queue。这样只有当前程序可以使用。
- 队列中可以设置自动删除标志位,在所有的消费者取消订阅之后进行删除。
- 如果这个队列没有被任何的消费者订阅,那么,当这个队列有数据到达时,这个数据会被缓存,不会进行丢弃!当有消费者,这个数据会被立刻发送到消费者!
- 如果queue队列不存在,消费者不会接受到任何消息。如果queue队列不存在,生产者传入的消息都会被丢弃。所以消费者和生产者在使用时最好都要尝试声明队列!
- 当一个队列中有多个消费者订阅时,队列会采取轮询方式进行选取调用!
7 消息的ack应答机制
默认情况下,当一个消息已经被某个消费者正确的接收到的时候,那么该消息就会从队列中移除。当然也可以让同一个消息发送到多个消费者。
- 每个消息都有进行应答确认ack,我们可以显示的在程序中去ack,也可以进行自动的应答(设置好对应的标志位即可)!
- 如果有数据没有被应答,那么这个消息还会继续发送到下一个消费者中!
- 如果一个程序的有问题,不会进行ack,那么消息队列不会再发送数据给他,是会认为其处理能力有限
- 而且ack的机制可以起到限流的作用!在消费者处理完成数据后再发送ack确认,甚至是在额外的延时后发送ack,将有效的平衡消费者的负载
如果没有没有正确响应:
- 如果Consumer接收了一个消息就还没有发送ack就与RabbitMQ断开了,RabbitMQ会认为这条消息没有投递成功会重新投递到别的Consumer。
- 如果Consumer本身逻辑有问题没有发送ack的处理,RabbitMQ不会再向该Consumer发送消息。RabbitMQ会认为这个Consumer还没有处理完上一条消息,没有能力继续接收新消息。
8 消息队列持久化处理
RabbitMQ支持消息的持久化,也就是数据写在磁盘上,为了数据安全考虑,大多数用户都会选择持久化。消息队列持久化包括3个部分:
- Exchange持久化,在声明时指定durable => 1
- Queue持久化,在声明时指定durable => 1
- 若Exchange和Queue都是持久化的,那么它们之间的Binding也是持久化的;而Exchange和Queue两者之间有一个持久化,一个非持久化,就不允许建立绑定。
- 消息持久化,在投递时指定delivery_mode => 2(1是非持久化)
Consumer从durable queue中取回一条消息之后并发回了ack消息,RabbitMQ就会将其标记,方便后续垃圾回收。如果一条持久化的消息没有被consumer取走,RabbitMQ重启之后会自动重建exchange和queue(以及binding关系),消息通过持久化日志重建再次进入对应的queues,exchanges。
9 消息Message模块
在我们的复刻项目中交换机,队列和绑定信息持久化是通过SQLite3轻量级数据库实现的!消息则是我们自己创建的文件格式"len(8字节长度) + 数据" 储存到文件队列名.mqd中
信息Message的持久化存储:
- 对于需要持久化存储的队列,其中的队列消息也采取持久化,储存在特殊队列文件中。
- 恢复历史消息数据的时通过MessageMapper进行一次垃圾回收:
- 首先先创建一个临时文件用来进行数据中转,防止数据丢失
- 然后打开历史数据文件,将文件中的消息读取出来,储存到临时容器中(消息采用智能指针管理)。这个过程中遇到无效消息(标志位为0)的就跳过。
- 遍历临时容器,将消息储存到临时文件中。将临时文件重命名为储存文件
- 通过MessageMapper获取历史消息,将消息储存到持久化存储表中。
- 收到一条消息后,根据消息属性来判断是否需要持久化存储。
- 删除消息时,首先在待确认消息表中找到目标消息,然后根据其属性判断是否需要删除持久化。若删除持久化则更新有效消息数量(总消息大于2000条且有效消息少于50%时进行垃圾回收)
虚拟机中需要对所有的队列消息进行管理,因此构建了一个队列消息总体管理类MessageManager,提供给虚拟机进行使用。这里接口的框架都是先上锁取出对应队列的操作句柄,然后在进行操作!这样可以避免业务处理影响锁的长时间占有!
- 内部管理一个队列名称-队列持久化指针 映射表
- 初始化接口,通过队列名称进行初始化。并进行恢复历史数据
- 销毁接口,销毁队列消息数据,对队列持久化信息进行清空
- 新增信息接口,将消息储存到对应队列中去,通过队列消息进行维护管理。
- 应答ACK接口,调用一次删除消息操作即可
10 消息队列的信道管理
网络通信中,生产者和消费者的操作都是通过请求来进行处理的,通过protobuf快速生成各种请求!并使用智能指针进行管理。
信道管理的成员:
- 信道ID std::string _cid 请求/响应会有ID来判断要交给哪一个信道进行管理
- 匹配的消费者指针:每个信道跟随一个消费者
- 信道管理的连接 muduo::net::TcpConnectionPtr _conn 用于向客户端发送数据
- Protobuf协议处理器操作句柄 ProtobufCodecPtr _codec 网络通信前的协议处理
- 消费者管理句柄 ConsumerManager::ptr _cmp 信道关闭/取消订阅时通句柄删除订阅者信息
- 虚拟机句柄 VirtualHost::ptr _host 交换机/队列/消息数据管理
- 工作线程池操作句柄 Thread::ptr _pool 一条消息发布到队列 需要将消息推送给对应消费者这个过程交给线程池
根据底层虚拟机的接口进行再一次封装,通过请求进行获取对应的参数,传入给底层进行处理!
一个连接可以匹配多个信道!
11 RabbitMQ的集群方式
11.1 普通模式:默认的集群模式。
对于Queue来说,消息实体只存在于其中一个节点,A、B两个节点仅有相同的元数据,即队列结构,但队列的元数据仅保存有一份,即创建该队列的rabbitmq节点(A节点),当A节点宕机,你可以去其B节点查看。
当消息进入A节点的Queue中后,consumer从B节点拉取时,RabbitMQ会临时在A、B间进行消息传输,把A中的消息实体取出并经过B发送给consumer,所以consumer应平均连接每一个节点,从中取消息。
该模式存在一个问题就是当A节点故障后,B节点无法取到A节点中还未消费的消息实体。如果做了队列持久化或消息持久化,那么得等A节点恢复,然后才可被消费,并且在A节点恢复之前其它节点不能再创建A节点已经创建过的持久队列;如果没有持久化的话,消息就会失丢。
这种模式更适合非持久化队列,只有该队列是非持久的,客户端才能重新连接到集群里的其他节点,并重新创建队列。假如该队列是持久化的,那么唯一办法是将故障节点恢复起来。
11.2 镜像模式:把需要的队列做成镜像队列,存在于多个节点。
该模式解决了普通模式的问题,其实质不同之处在于,消息实体会主动在镜像节点间同步,而不是在consumer取数据时临时拉取。
该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。