C C++最全为什么需要消息队列(MQ)_为什么用消息队列,2024年最新【大牛系列教学

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

我们首先简要回答,为什么需要消息队列。

在高并发场景下,由于服务端来不及同步处理数量过多的请求,可能导致请求堵塞。例如,大量的 INSERT、UPDATE 之类的请求同时到达 MySQL 服务端,在执行这些请求的过程中,会出现大量的行锁、表锁,甚至到最后,由于请求堆积过多,触发“too many connections”错误。在这类高并发场景下,通过使用消息队列,我们就可以异步处理这些请求,从而缓解系统的压力。

2 分布式架构相关知识

为了进一步深入的探讨使用消息队列的意义,我们首先介绍一下分布式的相关知识。

美国计算机科学家、LaTex 的作者 Leslie Lamport 曾说:“分布式系统就是这样的一个系统:系统中一个你甚至都不知道的计算机出了故障,却可能导致你自己的计算机不可用。”,Leslie Lamport 一语道破了分布式系统的玄机,那就是它的复杂性与不可控性,所以 Martin Fowler 强调:“分布式调用的第一原则就是不要分布式。”,这句话看似颇具哲理,然而就企业应用系统而言,只要整个系统在不停地演化,并有多个子系统共同存在时,这条原则就会被迫打破。毕竟在当今的企业应用系统中,很难找到完全不需要分布式调用的场景。Martin Fowler 提出这条原则,一方面是希望系统设计者能够审慎地对待分布式调用,另一方面分布式系统自身确实也存在缺陷。无论是 CORBA、还是 EJB 2,无论是 RPC 平台、还是 Web Service,都因为使用了驻留在不同进程空间的分布式组件,而引入了额外的复杂度,并可能对系统的效率、可靠性、可预测性等诸多方面带来负面影响。

然而,不可否认的是,在企业应用系统领域,我们总是会面对不同系统之间的通信、集成与整合,尤其当面临异构系统时,这种分布式调用与通信越重要,它(分布式)在架构设计中就更能凸显其价值。并且,从业务分析与架构质量的角度来说,我们也希望在系统架构中尽可能地重用服务,通过独立运行在进程中服务的形式,我们就可以彻底解除客户端与服务端的耦合,进而达到服务重用的目的——这常常是架构演化的必然道路。在我的同事陈金洲发表在 InfoQ 上的文章《架构腐化之谜》中,就认为可以通过“将独立的模块放入独立的进程”来解决架构因为代码规模变大而腐化的问题。

随着网络基础设施的逐渐成熟,从 RPC 进化到 Web Service,到业界开始普遍推行 SOA,再到后来的 RESTful平台,以及云计算中的 PaaS 与 SaaS 概念的推广,分布式架构在企业应用中逐渐呈现出不同的风貌,然而殊途同归,这些分布式架构的目标仍是希望回到建造巴别塔的时代:系统之间的交流不再因为不同语言或平台的隔阂而产生障碍。正如 Martin Fowler 在《企业集成模式》一书的序中写道:“集成之所以重要,是因为相互独立的应用是没有生命力的。我们需要一种技术,能将设计时并未考虑互相操作的应用集成起来,打破它们之间的隔阂,从而获得比单个应用更多的效益”。这,或许是分布式架构存在的主要意义。

3 集成模式中的消息模式

归根结底,企业应用系统就是对数据的处理,而对于一个拥有多个子系统的企业应用系统而言,它的基础无疑就是对消息的处理。与对象不同,消息本质上是一种数据结构(当然,对象也可以看做是一种特殊的消息),它包含消费者(客户端)与生产者(服务端)双方都能识别的数据,这些数据需要在不同的进程(机器)之间传递,并可能会被多个完全不同的客户端消费。在众多的分布式技术中,消息传递相对于文件传递和远程过程调用(RPC)而言,似乎更胜一筹,因为它具有更好的平台无关性,并能够很好地支持并发与异步调用。对于 Web Service 与 RESTful 而言,则可以看做是消息传递技术的一种衍生或封装。在《面向模式的软件架构(卷四)》一书中,将关于消息传递的模式划归为分布式基础设施的范畴,这是因为诸多消息中间件产品的出现,使得原来需要开发人员自己实现的功能,现在可以直接重用了,这极大地降低了包括设计成本、实现成本在内的开发成本。因此,对于架构师的要求也就从原来的设计实现,转变为对业务场景和功能需求的判断、进而能够正确地进行架构决策、技术选型与模式运用了。

3.1 常用的消息模式

在我参与过的所有企业应用系统中,无一例外都采用(或在某些子系统和模块中部分采用)了**基于消息的分布式架构。**但是在此过程中,让我们做出架构决策的证据却迥然而异,这也直接决定了我们所要选用的消息模式。

3.1.1 消息通道(Message Channel)模式

我们经常运用的消息模式是消息通道模式,消息通道模式如下图所示:

消息通道作为在客户端(消费者,Consumer)与服务端(生产者,Producer)之间引入的间接层,可以有效地解除二者之间的耦合:只要规定了双方通信的消息格式,以及处理消息的机制与时机,就可以做到消费者对生产者的“无感知”。事实上,该模式可以支持多个生产者与消费者。例如,我们可以让多个生产者向消息通道发送消息,因为消费者对生产者的无感知性,它不必考虑消息通道中的消息究竟是哪个生产者发来的。

虽然消息通道解除了生产者与消费者之间的耦合,使得我们可以任意地对生产者与消费者进行扩展,但消息通道同时也引入了两者对它的依赖,因为生产者和消费者必须要知道消息通道资源的位置。要解除这种对消息通道的依赖,可以考虑引入 Lookup 服务来查找该通道资源。例如,在 JMS 中就可以通过 JNDI 来获取消息通道 Queue。如果要做到充分的灵活性,则可以将与通道相关的信息存储到配置文件中,Lookup 服务首先通过读取配置文件来获得通道。

消息通道通常以队列的形式存在,这种先进先出的数据结构无疑最适合这种处理消息的场景。微软的 MSMQ、IBM MQ、JBoss MQ 以及开源的 RabbitMQ、Apache ActiveMQ,都通过队列的形式,实现了消息通道模式。因此,在选择使用消息通道模式时,更多地是要从质量属性的层面对各种实现了该模式的产品(如RabbitMQ)进行全方位的分析与权衡。例如,消息通道对并发的支持以及在性能上的表现;消息通道是否充分地考虑了错误处理;消息通道对消息安全的支持;以及关于消息持久化、灾备(fail over)与集群等方面的支持。因为消息通道传递的消息往往是一些重要的业务数据,一旦消息通道成为故障点或者安全性的突破点,那么就可能会对系统造成灾难性的后果。在本文的第二部分,我将给出一个实际案例来阐释进行架构决策时应该考虑的架构因素,并据此做出正确地决策。

3.1.2 发布者-订阅者(Publisher-Subscriber)模式

在介绍 Publisher-Subscriber 模式之前,首先介绍拉模型和推模型。

继承前文所述,一旦消息通道需要支持多个消费者时,就可能面临两种模型的选择:拉模型推模型

拉模型是由消息的消费者发起的,主动权把握在消费者手中,它会根据自身的情况对生产者发起调用。如下图所示:

拉模型的另一种体现是,当生产者的状态发生变更时,通知消费者其状态发生了改变,但得到通知的消费者却会以回调方式,通过调用(生产者)传递过来的消费者对象,获取更多细节消息。

在基于消息的分布式系统中,拉模型的消费者通常以“Batch Job”的形式,根据事先设定的时间间隔,定期侦听通道的情况。一旦发现有消息传递进来,就会将消息传递给真正的处理器(也可以看做是消费者)处理消息,执行相关的业务。在本文第二部分介绍的医疗卫生系统,正是通过引入 Quartz.NET 实现了“Batch Job”,完成对消息通道中消息的处理。

推模型的主动权常常掌握在生产者手中,消费者被动地等待生产者发出的通知,这就要求生产者必须了解消费者的相关信息。如下图所示:

对于推模型而言,消费者无需了解生产者。当生产者通知消费者时,传递的内容往往是消息(或事件),而非生产者自身。同时,生产者还可以根据不同的情况,注册不同的消费者,然后在封装的通知逻辑中,根据不同的状态变化,通知不同的消费者。

拉模型和推模型两种模型都有各自的优势。拉模型的优势在于可以进一步解除消费者对通道的依赖,通过后台任务(如 Batch Job)去定期访问消息通道,坏处是需要引入一个单独的服务进程,以 Schedule 形式执行。而对于推模型而言,消息通道事实上会作为消费者观察的主体,一旦发现消息进入,就会通知消费者对消息进行处理。

无论推模型还是拉模型,对于消息对象而言,都可能采用类似 Observer 模式的机制,实现消费者对生产者的订阅,因此这种机制通常又被称为 Publisher-Subscriber 模式,如下图所示:

通常情况下,发布者和订阅者都会被注册到用于传播变更的**基础设施(即消息通道)**上。发布者会主动地了解消息通道,使其能够将消息发送到通道中;消息通道一旦接收到消息,会主动地调用注册在通道中的订阅者,进而完成对消息内容的消费。

对于订阅者而言,有两种处理消息的方式。一种是广播机制,这时消息通道中的消息在出列的同时,还需要复制消息对象,将消息传递给多个订阅者。例如,有多个子系统都需要获取从 CRM 系统传来的客户信息,并根据传递过来的客户信息,进行相应的处理,此时的消息通道又被称为 Propagation 通道。另一种方式属于抢占机制,它遵循同步方式,在同一时间只能有一个订阅者能够处理该消息,实现 Publisher-Subscriber 模式的消息通道会选择当前空闲的唯一订阅者,并将消息出列,并传递给订阅者的消息处理方法。

目前,有许多消息中间件都能够很好地支持 Publisher-Subscriber 模式,例如 JMS 接口规约中对于 Topic 对象提供的 MessagePublisher 与 MessageSubscriber 接口。RabbitMQ 也提供了对该模式的实现。微软的 MSMQ 虽然引入了事件机制,可以在队列收到消息时触发事件,以通知订阅者,但它并非严格意义上的 Publisher-Subscriber 模式实现。由微软 MVP Udi Dahan 作为主要贡献者的 NServiceBus,则对 MSMQ 以及 WCF 做了进一层封装,并能够很好地实现这一模式。

3.1.3 消息路由(Message Router)模式

无论是 Message Channel 模式,还是 Publisher-Subscriber 模式,队列在其中都扮演了举足轻重的角色。然而,在企业应用系统中,当系统变得越来越复杂时,对性能的要求也会越来越高,此时对于系统而言,可能就需要支持同时部署多个队列,并可能要求分布式部署不同的队列,这些队列可以根据定义,接收不同的消息,例如订单处理的消息,日志信息,查询任务消息等。这时,对于消息的生产者和消费者而言,并不适宜承担决定消息传递路径的职责。事实上,根据S单一职责原则,这种职责分配也是不合理的,它既不利于业务逻辑的重用,也会造成生产者、消费者与消息队列之间的耦合,从而影响系统的扩展。

既然这三种对象(组件)都不宜承担这样的职责,那么就有必要引入一个新的对象,专门负责传递路径选择的功能,这就是所谓的消息路由(Message Router)模式,如下图所示:

通过消息路由,我们可以通过配置路由规则,来指定消息传递的路径,以及指定具体的消费者对应的生产者。例如,指定路由的关键字,并由它来绑定具体的队列及指定的生产者(或消费者)。路由的支持提供了消息传递和处理的灵活性,也有利于提高整个系统的消息处理能力。同时,路由对象有效地封装了寻找和匹配消息路径的逻辑,就好似一个调停者(Meditator),负责协调消息、队列与路径寻址之间关系。

除了以上的模式之外,Messaging模式提供了一个通信基础架构,使得我们可以将独立开发的服务整合到一个完整的系统中。 Message Translator模式则完成对消息的解析,使得不同的消息通道能够接收和识别不同格式的消息,而且通过引入这样的对象,也能够很好地避免系统内部盘根错节,彼此依赖多个服务。Message Bus模式可以为企业提供一个面向服务的体系架构,它可以完成对消息的传递,对服务的适配与协调管理,并要求这些服务以统一的方式完成协作。

4 消息模式的应用场景

在介绍过几种常见的消息模式后,接下来我们看一下这些消息模式的应用场景。

基于消息的分布式架构总是围绕着消息来做文章。例如,可以将消息封装为对象,或者指定消息的规范(如 SOAP),或者对实体对象进行序列化与反序列化,这些方式的目的只有一个,就是将消息设计为生产者和消费者都能够识别的格式,并且能够通过消息通道进行传递。

4.1 场景一:基于消息的统一服务架构

在制造工业的 CIMS系统中,我们尝试将各种业务以服务的形式公开给客户端的调用者,例如,定义如下接口:

public interface IService {
    IMessage Execute(IMessage aMessage);
    void SendRequest(IMessage aMessage);
}

之所以能够设计这样的服务,原因在于我们**对业务信息进行了高度的抽象,以消息的形式在服务之间传递,**此时的消息其实算是生产者与消费者之间的契约(或接口)了,只要遵循这样的契约,按照规定的格式对消息进行转换和抽取,就能很好地支持系统的分布式处理。

在这个 CIMS系统中,我们将消息划分为 ID,Name 和 Body,通过定义如下接口方法,可以获得消息主体的相关属性:

public interface IMessage:ICloneable
{
     string MessageID { get; set; }
     string MessageName() { get; set; }
     IMessageItemSequence CreateMessageBody();
     IMessageItemSequence GetMessageBody();
}

消息主体类 Message 实现了 IMessage 接口。在该类中,消息体 Body 为 IMessageItemSequence 类型。这个类型用于获取和设置消息的内容,即 Value 和 Item,如下:

public interface IItemValueSetting {
     string getSubValue(string name);
     void setSubValue(string name, string value);  
}
public interface IMessageItemSequence:IItemValueSetting, ICloneable
{      
     IMessageItem GetMessageItem(string aName);
     IMessageItem CreateMessageItem(string aName);       
}

Value 为字符串类型,它利用了 HashTable 存储 Key 和 Value 的键值对。Item 则为 IMessageItem 类型,在 IMessageItemSequence 的实现类中,同样利用了 HashTable 存储 Key 和 Item 的键值对。

IMessageItem 支持消息体的嵌套,它包含了两部分:SubValue 和 SubItem。实现的方式和 IMessageItemSequence 相似。通过定义这样的嵌套结构,使得消息的扩展成为可能。一般的消息结构如下所示:

IMessage——Name
                       ——ID
                       ——Body(IMessageItemSequence)
                               ——Value
                               ——Item(IMessageItem)
                                   ——SubValue
                                   ——SubItem(IMessageItem)
                                          ——……

各个消息对象之间的关系如下图所示:

在实现服务进程通信之前,我们必须定义好各个服务或各个业务的消息格式,通过消息体的方法在服务的一端设置消息的值,然后发送,并在服务的另一端获得这些值。例如发送消息端定义如下的消息体:

IMessageFactory factory = new MessageFactory();
IMessage message = factory.CreateMessage();
message.SetMessageName("service1");

IMessageItemSequence body = message.CreateMessageBody();
body.SetSubValue("subname1","subvalue1");
body.SetSubValue("subname2","subvalue2");

IMessageItem item1 = body.CreateMessageItem(”item1”);
item1.SetSubValue("subsubname11","subsubvalue11");
item1.SetSubValue("subsubname12","subsubvalue12");

//Send Request Message
MyServiceClient service = new MyServiceClient("Client");
service.SendRequest(message);

我们在客户端引入了一个 ServiceLocator 对象,它通过 MessageQueueListener 对消息队列进行侦听,一旦接收到消息,就获取该消息中的 name 去定位它所对应的服务,然后调用服务的 Execute(aMessage) 方法,执行相关的业务。

ServiceLocator 承担的定位职责,其实是对存储在 ServiceContainer 容器中的服务进行查询。ServiceContainer 容器可以读取配置文件,在启动服务的时候初始化所有的分布式服务(注意,这些服务都是无状态的),并对这些服务进行管理。它封装了服务的基本信息,诸如服务所在的位置,服务的部署方式等,从而避免服务的调用者直接依赖于服务的细节,既减轻了调用者的负担,还能够较好地实现服务的扩展与迁移。

在这个系统中,我们主要引入了 Messaging 模式,通过定义的 IMessage 接口,使得我们更好地对服务进行抽象,并以一种扁平的格式存储数据信息,从而解除服务之间的耦合。只要各个服务就共用的消息格式达成一致,请求者就可以不依赖于接收者的具体接口。通过引入的 Message 对象,我们就可以建立一种在行业中通用的消息模型与分布式服务模型。事实上,基于这样的一个框架与平台,在对制造行业的业务进行开发时,开发人员最主要的活动是与领域专家就各种业务的消息格式进行讨论,这样一种面向领域的消息语言,很好地扫清了技术人员与业务人员的沟通障碍;同时在各个子系统之间,我们也只需要维护服务间相互传递的消息接口表。每个服务的实现都是完全隔离的,有效地做到了对业务知识基础设施的合理封装与隔离。

对于消息的格式和内容,我们考虑引入了 Message Translator 模式,负责对前面定义的消息结构进行翻译和解析。为了进一步减轻开发人员的负担,我们还可以基于该平台搭建一个“消息-对象-关系”的映射框架,引入实体引擎(Entity Engine),将消息转换为领域实体,使得服务的开发者能够以完全面向对象的思想开发各个服务组件,并通过调用持久层实现消息数据的持久化。同时,利用消息总线(此时的消息总线可以看作是各个服务组件的连接器)连接不同的服务,并允许异步地传递消息,对消息进行编码。这样一个基于消息的分布式架构如下图(基于 Message Bus 的 CIMS 分布式架构)所示:

4.2 场景二:消息中间件的架构决策

在一个医疗卫生系统中,我们面临了客户对系统性能/可用性的非功能性需求。在我们最初启动该项目时,客户就表达了对性能与可用性的特别关注,客户希望最终用户在进行复杂的替换、删除操作时,能够具有很好的用户体验,简言之,就是希望能够快速地得到操作的响应。问题在于这样的替换、删除操作需要处理比较复杂的业务逻辑,同时牵涉的关联数据量非常大,若需完成整个操作,最坏情况下可能需要几分钟的时间。我们通过引入缓存、索引、分页等多种方式,对数据库操作进行性能调优,但整个操作的耗时却始终无法达到客户的要求。同时,由于该系统是在一个遗留系统的基础上开发的,如果要引入 Map-Reduce 来处理这些操作(以满足质量需求),则会对架构产生太大影响,并且不能很好地重用之前系统的某些组件,显然,付出的成本与收益并不成正比。

通过对需求进行分析,我们注意到最终客户并不需要实时获得结果,只要能够保证最终结果的一致性和完整性即可。关键在于就用户体验而言,他们不希望经历漫长的等待,然后再通知他们操作究竟是成功还是失败——这是一个典型的需要通过后台任务进行异步处理的场景。

在企业应用系统中,我们常常会遇到这样的场景。

我们曾经在一个金融系统中尝试通过自己编写任务的方式来控制后台线程的并发访问,并完成对任务的调度。事实证明,这样的设计并非行之有效,对于这种典型的异步处理来说,基于消息传递的架构模式才是解决这一问题的最佳办法

随着消息中间件的逐步成熟,对于这一问题的架构设计,已经由原来对设计实现的关注转为如何进行产品选型和技术决策。例如,在 .NET 平台下,架构师需要重点考虑的是:应该选择哪种消息中间件来处理此等问题。这就需要我们必须结合具体的业务场景,来识别这种异步处理方式的风险,然后再根据这些风险去比较各种技术,以找到最适合的方案。

回到前述案例中,通过分析业务场景以及客户性质,我们发现该业务场景具有如下特征:

  • 在一些特定情形下,可能会集中发生批量的替换、删除操作,使得操作的并发量达到高峰。例如,FDA 要求召回一些违规药品时,就需要删除药品库中该药品的信息;
  • 操作结果不要求实时性,但需要保证操作的可靠性,不能因为异常失败而导致某些操作无法进行;
  • 自动操作的过程是不可逆转的,因此需要记录操作历史;
  • 基于性能考虑,大多数操作需要调用数据库的存储过程;
  • 操作的数据需要具备一定的安全性,避免被非法用户对数据造成破坏;
  • 与操作相关的功能以组件形式封装,保证组件的可重用性、可扩展性与可测试性;
  • 数据量可能随着最终用户的增多而逐渐增大。

针对如上的业务需求,我们决定从以下几个方面对各种技术方案进行横向的比较与考量:

  • 并发:选择的消息队列一定要很好地支持用户访问的并发性;
  • 安全:消息队列是否提供了足够的安全机制;
  • 性能伸缩:不能让消息队列成为整个系统的单一性能瓶颈;
  • 部署:尽可能让消息队列的部署更为容易;
  • 灾备:不能因为意外的错误、故障或其他因素导致处理数据的丢失;
  • API 易用性:处理消息的 API 必须足够简单、并能够很好地支持测试与扩展。

我们先后考察了 MSMQ、Resque、ActiveMQ 和 RabbitMQ,通过查询相关资料,以及编写 Spike 代码验证相关质量,我们最终选择了 RabbitMQ。下面说说选择 RabbitMQ 的具体原因。

我们选择放弃 MSMQ,是因为它严重依赖 Windows 操作系统,它虽然提供了易用的 GUI 方便管理人员对其进行安装和部署,但若要编写自动化部署脚本,却非常困难;同时,MSMQ 的队列容量不能查过4M字节,这也是我们无法接受的。Resque 的问题是目前仅支持 Ruby 的客户端调用,不能很好地与 .NET 平台集成,此外,Resque 对消息持久化的处理方式是写入到 Redis 中,因而需要在已有 RDBMS 的前提下,引入新的 Storage,这显然会增加系统的复杂度。我们比较倾心于 ActiveMQ 与 RabbitMQ,但通过编写测试代码,采用循环发送大数据消息以验证消息中间件的性能与稳定性时,我们发现 ActiveMQ 的表现并不太让人满意,至少,在我们的询证调研过程中,ActiveMQ 会因为频繁发送大数据消息而偶尔出现崩溃的情况。相对而言,RabbitMQ 在各个方面都比较适合我们的架构要求。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

715704322858)]
[外链图片转存中…(img-qP1QMUkO-1715704322859)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值