【我的项目】仿RabbitMQ的消息队列项目总结


不听反方向的钟,想要什么就冲。


消息队列项目总结

  • [1 RabbitMQ解决的问题](#1 RabbitMQ解决的问题)
  • [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那么不同的模块的确可以部署到不同的机器上,但是还是有很多问题需要解决。比如:

  1. 信息的发送者和接收者如何维持这个连接,如果一方的连接中断,这期间的数据如何防止丢失?
  2. 如何降低发送者和接收者的耦合度?
  3. 如何让Priority高的接收者先接到数据?
  4. 如何做到Load balance?有效均衡接收者的负载?
  5. 如何有效的将数据发送到相关的接收者?也就是说将接收者subscribe 不同的数据,如何做有效的filter。
  6. 如何保证接收者接收到了完整,正确的数据?
  7. AMDQ协议解决了以上的问题,而RabbitMQ实现了AMQP。

2 消息队列优缺点

2.1 优点

流量削峰:以典型的双十一交易秒杀为例

  1. 消息队列可以控制活动人数,超过此一定阀值的订单直接丢弃
  2. 通过消息队列的缓冲,可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
  3. 用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面。
  4. 秒杀业务程序根据消息队列中的请求信息,再做后续处理。

应用解耦:交易场景下的解耦

比如网络交易时,订单系统调用库存系统的接口。如果库存系统出现了故障时,订单就会失败!此时订单系统和库存是高耦合的!

客户端请求和服务端处理数据可以通过中间的消息队列进行解耦,降低耦合度。就算服务端程序出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

加入消息队列后

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,获取下单消息,进行库操作。

就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

2.2 缺点

关于消息队列的优点也就是上面列举的,就是在特殊场景下有其对应的好处,解耦、异步、削峰。缺点有以下几个:

  1. 系统可用性降低
    系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人家ABCD 四个系统好好的,没啥问题,你偏加个 消息队列 进来,万一 消息队列挂了咋整,消息队列一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以点击这里查看。
  2. 系统复杂度提高
    硬生生加个 MQ 进来,你怎么[保证消息没有重复消费]?怎么[处理消息丢失的情况]?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。
  3. 一致性问题
    A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

抽象的来看,消息队列可以理解为机场,每天都有很多的人(消息)需要坐飞机(消息处理)。那么机场检票口(交换机)发挥的作用就是通过登机牌(绑定信息)引导人们到指定的候机区(队列)。飞机(消费者)就绪了(订阅一个队列)就会取出飞机能力之内的乘客,然后起飞(异步处理请求)

所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10 倍。但是关键时刻,用,还是得用的。

3 为什么选择RabbitMQ

  1. ActiveMQ,性能不是很好,因此在高并发的场景下,直接被pass掉了。它的Api很完善,在中小型互联网公司可以去使用。
  2. kafka,主要强调高性能,如果对业务需要可靠性消息的投递的时候。那么就不能够选择kafka了。但是如果做一些日志收集呢,kafka还是很好的。因为kafka的性能是十分好的。
  3. RocketMQ,它的特点非常好。它高性能、满足可靠性、分布式事物、支持水平扩展、上亿级别的消息堆积、主从之间的切换等等。MQ的所有优点它基本都满足。但是它最大的缺点:商业版收费。因此它有许多功能是不对外提供的。

4 RabbitMQ结构

  1. RabbitMQ Server :也叫broker server,它不是运送食物的卡车,而是一种传输服务。原话是RabbitMQ isn't a food truck, it's a delivery service. 他的角色就是维护一条从Producer到Consumer的传输路线,保证数据能够按照指定的方式进行传输。但是这个保证也不是100%的保证,但是对于普通的应用来说这已经足够了。当然对于商业系统来说,可以再做一层数据一致性的guard,就可以彻底保证系统的一致性了。
  2. 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的规则。
  3. 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(绑定关系)。

  1. Exchanges are where producers publish their messages:消息交换机,它指定消息按什么规则,路由到哪个队列
  2. Queues are where the messages end up and are received by consumers:消息队列载体,每个消息都会被投入到一个或多个队列
  3. Bindings are how the messages get routed from the exchange to particular queues:绑定,它的作用就是把exchange和queue按照路由规则绑定起来

Routing Key:路由关键字,exchange根据这个关键字进行消息投递

还有几个概念是上述图中没有标明的,那就是Connection(连接),Channel(通道,频道),Vhost(虚拟主机)。

  1. Connection:就是一个TCP的连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server的。以后我们可以看到,程序的起始处就是建立这个TCP连接。
  2. Channel:虚拟连接。它建立在上述的TCP连接中。数据流动都是在Channel中进行的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。
  3. 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 消息队列执行过程

  1. 客户端连接到消息队列服务器,打开一个Channel
  2. 客户端通过channel声明一个交换机,并设置相关属性
  3. 客户端通过channel声明一个队列,并设置相关属性
  4. 客户端使用routing_key,在交换机和queue中建立绑定关系
  5. 客户端投递消息到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个部分:

  1. Exchange持久化,在声明时指定durable => 1
  2. Queue持久化,在声明时指定durable => 1
  3. 若Exchange和Queue都是持久化的,那么它们之间的Binding也是持久化的;而Exchange和Queue两者之间有一个持久化,一个非持久化,就不允许建立绑定。
  4. 消息持久化,在投递时指定delivery_mode => 2(1是非持久化)

Consumer从durable queue中取回一条消息之后并发回了ack消息,RabbitMQ就会将其标记,方便后续垃圾回收。如果一条持久化的消息没有被consumer取走,RabbitMQ重启之后会自动重建exchange和queue(以及binding关系),消息通过持久化日志重建再次进入对应的queues,exchanges。

9 消息Message模块

在我们的复刻项目中交换机,队列和绑定信息持久化是通过SQLite3轻量级数据库实现的!消息则是我们自己创建的文件格式"len(8字节长度) + 数据" 储存到文件队列名.mqd中

信息Message的持久化存储:

  • 对于需要持久化存储的队列,其中的队列消息也采取持久化,储存在特殊队列文件中。
  • 恢复历史消息数据的时通过MessageMapper进行一次垃圾回收:
    1. 首先先创建一个临时文件用来进行数据中转,防止数据丢失
    2. 然后打开历史数据文件,将文件中的消息读取出来,储存到临时容器中(消息采用智能指针管理)。这个过程中遇到无效消息(标志位为0)的就跳过。
    3. 遍历临时容器,将消息储存到临时文件中。将临时文件重命名为储存文件
  • 通过MessageMapper获取历史消息,将消息储存到持久化存储表中。
  • 收到一条消息后,根据消息属性来判断是否需要持久化存储。
  • 删除消息时,首先在待确认消息表中找到目标消息,然后根据其属性判断是否需要删除持久化。若删除持久化则更新有效消息数量(总消息大于2000条且有效消息少于50%时进行垃圾回收)

虚拟机中需要对所有的队列消息进行管理,因此构建了一个队列消息总体管理类MessageManager,提供给虚拟机进行使用。这里接口的框架都是先上锁取出对应队列的操作句柄,然后在进行操作!这样可以避免业务处理影响锁的长时间占有!

  1. 内部管理一个队列名称-队列持久化指针 映射表
  2. 初始化接口,通过队列名称进行初始化。并进行恢复历史数据
  3. 销毁接口,销毁队列消息数据,对队列持久化信息进行清空
  4. 新增信息接口,将消息储存到对应队列中去,通过队列消息进行维护管理。
  5. 应答ACK接口,调用一次删除消息操作即可

10 消息队列的信道管理

网络通信中,生产者和消费者的操作都是通过请求来进行处理的,通过protobuf快速生成各种请求!并使用智能指针进行管理。

信道管理的成员:

  1. 信道ID std::string _cid 请求/响应会有ID来判断要交给哪一个信道进行管理
  2. 匹配的消费者指针:每个信道跟随一个消费者
  3. 信道管理的连接 muduo::net::TcpConnectionPtr _conn 用于向客户端发送数据
  4. Protobuf协议处理器操作句柄 ProtobufCodecPtr _codec 网络通信前的协议处理
  5. 消费者管理句柄 ConsumerManager::ptr _cmp 信道关闭/取消订阅时通句柄删除订阅者信息
  6. 虚拟机句柄 VirtualHost::ptr _host 交换机/队列/消息数据管理
  7. 工作线程池操作句柄 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取数据时临时拉取。

该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。

相关推荐
Ace'28 分钟前
每日一题之既约分数
c++
理智的灰太狼36 分钟前
1015 Reversible Primes
数据结构·c++·算法
Dream it possible!1 小时前
LeetCode 热题 100_杨辉三角(82_118_简单_C++)(动态规划)
c++·算法·leetcode·动态规划
江湖人称菠萝包1 小时前
侯捷 C++ 课程学习笔记:C++内存管理机制
c++·笔记
执笔论英雄1 小时前
【DeepSeek学C++】移动构造函数
c++
A好名字A2 小时前
蓝桥杯真题------R格式(高精度乘法,高精度加法)
c++·算法·蓝桥杯
不懂的浪漫2 小时前
夯实 kafka 系列|第五章:基于 kafka 分布式事件框架 eval-event
分布式·kafka
阿巴~阿巴~2 小时前
Day1 蓝桥杯省赛冲刺精炼刷题 —— 位运算与循环(2)
c语言·c++·算法·蓝桥杯
kill bert3 小时前
第30周Java分布式入门 docker
java·分布式·docker
Hi-Dison3 小时前
Open HarmonyOS 5.0 分布式软总线子系统 (DSoftBus) 详细设计与运行分析报告
分布式·华为·harmonyos