Java知识点整理 7 — 消息队列

一. 什么是中间件?

中间件是一类提供系统软件和应用软件连接、便于软件各部分之间沟通的软件,应用软件可以借助中间件在不同技术架构之间共享信息与资源。

常用的中间件包括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,会被重新放到消息队列中,以保证每个任务都能得到执行。

线程池更适用于多线程并发处理任务,消息队列适用于分布式场景下的信息传输、应用解耦、负载均衡以及保证消息可靠性。

理论上来说分布式环境下不适合用线程池,因为可能无法保证消息的顺序性,需要单独设计额外的机制保证。消息队列可以保证消息执行的先后顺序。需要根据具体情况和业务场景来权衡使用线程池或者消息队列。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Phoenixxxxxxxxxxxxx

感谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值