一. 什么是中间件?
中间件是一类提供系统软件和应用软件连接、便于软件各部分之间沟通的软件,应用软件可以借助中间件在不同技术架构之间共享信息与资源。
常用的中间件包括Redis、消息队列、分布式存储等。
以智能BI平台项目为例。现有的系统包括图表管理、用户管理等,随着系统应用量的增加,智能分析业务会消耗大量资源。此时,可以考虑将智能分析业务单独抽出来,变成一个独立的服务体系,专门用来分析数据。那么如何让原来的系统后台与这个独立的智能分析服务紧密协作呢?就可以使用中间件。
比如使用Redis存储共享数据。主系统和智能分析服务都可以读取其中的数据,Redis充当了中间人的角色,连接了两个系统,这就是中间件。
二. 什么是消息队列?
消息队列可以看作是存放消息的容器,由于队列是一种先进先出形式的数据结构,因此消息也按照顺序进行消费。
三. 消息队列的作用/优势
- 异步处理提高系统性能:生产者发送完消息后,可以立刻转向其它任务,消费者可以在任意时间去处理消息,这样生产者与消费者之间就不会发生阻塞。
- 削峰填谷:消息队列允许先将用户请求存储起来,消费者(或者说实际执行任务的应用)可以根据自身的处理能力和需求,逐步处理这些消息。
四. 分布式消息队列的作用/优势
除了上述消息队列的作用外,分布式消息队列也有其独特的优势。
- 数据持久化:将消息集中存储到硬盘中,服务器重启后数据也不会丢失。
- 可扩展性:可以根据实际需求,随时增加或减少节点,保证服务的稳定性。这是与单机最大的区别。
- 应用解耦:连接不同开发语言、不同技术架构的系统,让这些系统能够灵活的传输读取数据。
- 发布订阅模式:大的核心系统向消息队列中发布消息,其它子系统去订阅消息队列。
具体解释一下应用解耦:
例如现有一个订单系统,它通过调用库存系统与发货系统完成整个业务逻辑执行。传统情况下,如果有一个订单进来,它会分别调用库存系统减少库存,然后调用发货系统发货。但可能存在的一个问题就是,如果库存系统调用成功,而发货系统调用失败,那么整个业务流程就会出问题,也可能会影响到其它系统。
通过应用解耦可以解决这个问题。设置一个消息队列,订单系统来请求后直接将请求放入消息队列,然后就可以返回,库存系统与发货系统只需要从消息队列中取消息执行即可。这样即使后续发货系统崩溃,也不会影响库存系统,当发货系统恢复后,再从消息队列中找消息,继续执行业务逻辑即可。这样能够显著提高系统性能。
什么是发布订阅模式:
例如腾讯有非常多的子系统,比如王者荣耀、吃鸡、腾讯云等等,大多数情况下都可以通过QQ进行关联。如果QQ进行了一项重要的技术改革,应该如何通知这些子系统做出相应调整。最简单的方法就是一个个通知,但是非常耗时耗力,并且随着子系统的增多,出错的概论也会增大,而且有可能一些新的项目不知道QQ的这个技术改革,导致没对应更新,造成严重的信息漏洞。
这时就可以通过发布订阅模式解决问题。QQ这个大的核心系统只需要向消息队列中发布消息,也就是这个技术改革的通知,其它子系统都去订阅这个消息队列,如果子系统需要,直接从消息队列中取出消息执行即可。
五. 消息队列的应用场景
- 耗时的任务(异步处理)
- 高并发的任务(异步+削峰填谷)
- 分布式系统协作(尤其跨团队跨业务协作,应用解耦)
- 强稳定性的场景(比如金融业务,持久化、可靠性、削峰填谷)
六. 消息队列的缺点/问题
- 系统复杂性提升:为系统引入一个额外的中间件意味着系统变得更加复杂,并且增加了额外的维护成本。此外,还需要考虑消息是否被顺序消费,是否存在重复消费问题等。
- 数据的一致性问题:不仅是消息队列的问题,也是整个分布式系统存在的问题。分布式锁可以解决这类问题。
七. 消息队列模型
消息队列由消息生产者、消息消费者、消息队列、消息四部分组成。
举个例子:
快递员小王(消息生产者)在早上八点拿到了一个包裹(消息),收件人小李(消息消费者)说早上八点有事不在家,这时小王可以将包裹放在小李家旁边的快递柜(消息队列),等小李有空了去取即可。
消息队列不仅可以实现对消息的存储,还可以实现生产者与消费者的解耦。
继续探讨,如果有一个寄件人小张想要邮寄一个快递,他完全可以将快递放到快递柜中,等待邮寄或者让其它消费者来取。
这就说明,任何想要使用快递柜的人都可以是不同的个体,都是相互独立的,并且邮寄的东西也可以不同。这就对应了消息队列的应用解耦特点,任何开发语言、架构或框架的系统都可以通过消息队列实现数据的传输读取,只要它们能够在消息队列中放入或取出消息。
八. 主流分布式消息队列选型
RabbitMQ生态优秀,支持各种语言的客户端,时效性高并且易于学习理解,适合大多数中小规模的分布式系统,因此采用它。
九. RabbitMQ
1. AMQP协议
RabbitMQ基于AMQP协议实现,即Advanced Message Queuing Protocol,高级消息队列协议。
AMQP主要由以下几部分构成:
- 生产者:发送消息到交换机。
- 消费者:从队列中取出消息。
- 交换机(Exchange):负责将消息发送到指定的队列。
- 队列(Queue):存储消息。
- 路由(Route):转发规则。
2. 消息确认机制
RabbitMQ提供了消息确认机制来确保消费者成功从消息队列中取走了消息。就像收快递后要确认收货,这样消息队列才能知道消费者成功取走了消息,并停止传输。
RabbitMQ通过支持配置autoack(默认false),来实现消息确认。
ack:消费成功;nack:消费失败;reject:拒绝
问:如何保证消息不会丢失?例如当业务流程执行失败。
答:可以通过autoack配置来拒绝接收失败的消息,并重新启动。如果拒绝了某条消息,那么该消息就会被标记为失败,它将重新返回队列,等待重新处理。如果选择不重新入队该消息,那么它就会被丢弃,不再被处理。
3. 交换机
交换机类似于网络路由器,负责将发送的消息转发到指定目标。交换机主要提供转发消息的能力,根据消息的路由规则将消息发送到对应的队列或绑定的消费者。
交换机的核心概念是绑定(Binding),将交换机和队列关联,类似于转发规则。
交换机类型:fanout、direct、topic、headers。
(1)fanout
它会将所有发送到交换机中的消息转发给所有与其绑定的队列中,不做任何判断。常用来广播消息。
(2)direct
消息会根据路由键(Routing Key)转发到指定的队列中。路由键是控制消息转发给哪个队列,类似于转发规则。
一个路由键可以绑定多个队列,并且理论上来说交换机存储了所有路由键。
(3)topic
消息会根据模糊的路由键被转发到指定队列中。也就是在direct的基础上设置了更为细致的路由规则。
在路由键中以" . "作为分割字符串,用" * "和" # "作为模糊匹配的字符," * "匹配一个单词," # "匹配0个或多个单词。比如" *.A ",那么a.A、b.A都能匹配;" A.# ",那么A.aaa、A.b都能匹配。
direct可以理解为精确匹配,topic则是模糊匹配。
(4)headers(不推荐)
不根据路由键的匹配规则进行消息路由,而是依赖于消息中的头部信息或headers属性,来确定要发送到哪个队列中。通常不使用这种方法,复杂且性能较差。
4. 消息过期机制
给每条消息设定一个有效期,如果一段时间内没有被消费者处理,就会失效。
主要是为了清理和丢弃那些长时间未被消费的消息,避免队列中积累过多过期消息,保证系统的稳定性与效率。
适用场景:清理过期数据、模拟延迟队列、专门让某个程序处理过期请求。
具体讲一下模拟延迟队列:
延迟队列就是让消息延迟一段时间再处理。比如将某项操作延迟一段时间再执行,可用来区分普通用户与会员用户。消费者可以监听延迟队列,普通用户的请求由一个程序监听处理该延迟队列,而会员用户则由另一个程序监听处理高优先级的队列。
通常有两种消息过期机制,一种是给队列中所有消息设置一个统一的过期时间,另一种是指定某条消息的过期时间。
5. 死信队列
为了保证消息的可靠性,即每条消息都成功消费,需要一个容错机制,即失败的消息怎么办。
死信:过期的消息、被拒绝的消息、消息队列已满或处理失败的消息的统称。
死信队列:专门用来接收死信的队列。本质上是一个普通队列,只是专门存放死信。
死信交换机:专门用来将死信转发到死信队列的交换机。本质上是一个普通交换机,只是专门转发死信。
通过引入死信队列、死信交换机和其路由规则,可以灵活的处理那些失败的消息。
6. 延迟队列
第4部分提到了模拟延迟队列。延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
RabbitMQ中没有直接支持延迟队列实现的功能,需要通过死信交换机和消息存活时间(TTL)实现。在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖 Erlang/OPT 18.0 及以上。
7. 优先级队列
优先级高的队列会先被消费。可以通过x-max-priority参数来实现优先级队列。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。
十. BI项目中RabbitMQ的应用
首先分析一下系统现状不足之处。系统目前实现了异步功能,但异步的实现依赖于本地线程池。
本地线程池有哪些问题?
- 无法集中限制,只能单机限制。
- 任务存储在内存中执行,可能会丢失。即使从数据库中恢复重试,也需要额外的编码工作。
- 优化问题。随着服务量增多,可能需要应用解耦。
异步流程改进:
之前的做法是将任务提交到线程池,在线程池中编写处理程序的代码,任务在线程池中排队等待执行。那么如果程序中断,任务就会丢失,无法进一步处理。
流程改进:
- 将任务的提交方式改为向消息队列发送消息。
- 编写一个专门用于接收消息并处理任务的程序。
- 如果程序中断,消息未被确认,消息队列将重新发送消息,确保任务不会丢失。
- 现在所有的消息都被集中发送到消息队列中,可以部署多个后端程序,它们都从消息队列中取消息执行,从而实现分布式负载均衡。
目前任务不再依赖于线程池,而是将任务发送到消息队列,通过消息队列进行分发和处理。即使程序中断或出现故障,也能保证失败的任务得到正确处理,不会丢失。
改进步骤:
- 创建交换机和队列。
- 将线程池中执行代码移动到消费者类中。
- 根据消费者需求来确认消息的格式(chartId)。
- 将提交到线程池改造为发送到消息队列。
通过验证发现,如果消息没有ack,也没有nack,会被重新放到消息队列中,以保证每个任务都能得到执行。
线程池更适用于多线程并发处理任务,消息队列适用于分布式场景下的信息传输、应用解耦、负载均衡以及保证消息可靠性。
理论上来说分布式环境下不适合用线程池,因为可能无法保证消息的顺序性,需要单独设计额外的机制保证。消息队列可以保证消息执行的先后顺序。需要根据具体情况和业务场景来权衡使用线程池或者消息队列。