目录
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
重点
如何保证消息不丢失
消息丢失场景:
生产者生产消息到RabbitMQ Server消息丢失;
RabbitMQ Server存储的消息丢失;
RabbitMQ Server到消费者消息丢失。
消息丢失从三个方面来解决:生产者确认机制、消费者手动确认消息、和持久化。
1、生产者确认机制
生产者发送消息到队列,无法确保发送的消息成功的到达server。
解决方法:
事务机制。在一条消息发送之后会使发送端阻塞,等待RabbitMQ的回应,之后才能继续发送下一条消息。性能差。
开启生产者确认机制,只要消息成功发送到交换机之后,RabbitMQ就会发送一个ack给生产者(即使消息没有Queue接收,也会发送ack)。如果消息没有成功发送到交换机,就会发送一条nack消息,提示发送失败。
路由不可达消息:生产者确认机制只确保消息正确到达交换机,对于从交换机路由到Queue失败的消息,会被丢弃掉,导致消息丢失。对于不可路由的消息,有两种处理方式:Return消息机制和备份交换机。
2、消费者手动消息确认
有可能消费者收到消息还没来得及处理MQ服务就宕机了,导致消息丢失。因为消息者默认采用自动ack,一旦消费者收到消息后会通知MQ Server这条消息已经处理好了,MQ 就会移除这条消息。
解决方法:消费者设置为手动确认消息。消费者处理完逻辑之后再给broker回复ack,表示消息已经成功消费,可以从broker中删除。当消息者消费失败的时候,给broker回复nack,根据配置决定重新入队还是从broker移除,或者进入死信队列。只要没收到消费者的 acknowledgment,broker 就会一直保存着这条消息,但不会 requeue,也不会分配给其他 消费者。
3、消息持久化
如果RabbitMQ服务异常导致重启,将会导致消息丢失。RabbitMQ提供了持久化的机制,将内存中的消息持久化到硬盘上,即使重启RabbitMQ,消息也不会丢失。
消息持久化需要满足以下条件:
消息设置持久化。
Queue设置持久化。
交换机设置持久化。
当发布一条消息到交换机上时,Rabbit会先把消息写入持久化日志,然后才向生产者发送响应。一旦从队列中消费了一条消息的话并且做了确认,RabbitMQ会在持久化日志中移除这条消息。在消费消息前,如果RabbitMQ重启的话,服务器会自动重建交换机和队列,加载持久化日志中的消息到相应的队列或者交换机上,保证消息不会丢失。
消息重复消费怎么处理?
消息重复的原因有两个:
1.生产时消息重复,
2.消费时消息重复。
生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,这时候生产者就会重新发送这条消息,导致MQ会接收到重复消息。
消费者消费成功后,给MQ确认的时候出现了网络波动,MQ没有接收到确认,为了保证消息不丢失,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。由于重复消息是由于网络原因造成的,无法避免。
解决方法:发送消息时让每个消息携带一个全局的唯一ID,在消费消息时先判断消息是否已经被消费过,保证消息消费逻辑的幂等性。具体消费过程为:
消费者获取到消息后先根据id去查询redis/db是否存在该消息;
如果不存在,则正常消费,消费完毕后写入redis/db;
如果存在,则证明消息被消费过,直接丢弃。
RocketMQ如何保证消息的顺序性
总结:1. 保证一对一 2. 自定义负载均衡模式(哈希取余) 3. 使用有序消费模式
为什么会出现乱序(负载均衡)
Broker中的每个Topic都有多个Queue,写入消息的时候会平均分配(负载均衡机制,默认轮询,也可以自定义)给不同的Queue,假如我们有一个消费者组ComsumerGroup,这个消费组中的每一台机器都会负责一部分Queue,那么就会导致顺序的乱序问题
例子
Producer先后发送了2条消息,一条insert,一条update,分别分配到2台Queue中,消费者组中的两台机器分别处理这两个Queue中的消息,这时候顺序是无法保证的
如何解决
1. 保证Producer、Queue、Comsumer是一对一对一的关系
问题:
1. 吞吐量降低
消息队列的吞吐量降低(绝对不容忍这样的情况发生)
2. 有阻塞的风险
如果Comsumer服务炸了,后面的消息就无法消费,被阻塞了
2. 把需要保持顺序消费的消息放到同一个Queue中,且让同一台机子处理
自定义负载均衡模式,把这一批顺序消息有共同的唯一ID,把唯一ID与队列的数量进行 hash取余运算,保证这批消息进入到同一个队列
存在的问题
还要考虑Comsumer消费失败的重试问题
3. 使用有序消费的模式
如果失败了会返回这个状态
ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT:稍后消费
基础
什么是 RabbitMQ
高级消息队列协议实现了高级消息队列协议(AMQP)高级消息队列协议,用于在不同的应用程序之间进行异步通信的一种消息队列技术。
什么是AMQP?
是一个提供统一消息服务的应用层标准高级消息队列协议。
RabbitMQ 的优缺点?
优点:
- 解耦 系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!
- 异步 将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度。
- 削峰 并发量大的时候,所有的请求直接到数据库,造成数据库连接异常。
缺点:
- 降低了系统的稳定性 本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低。
- 增加了系统的复杂性 加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
RabbitMQ 的构造
(1)生产者Publisher:生产消息,就是投递消息的一方。消息一般包含两个部分:消息体(payload)和标签(Label)。
(2)消费者Consumer:消费消息,也就是接收消息的一方。消费者连接到RabbitMQ服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
(3)Broker服务节点:表示消息队列服务器实体。一般情况下一个Broker可以看做一个RabbitMQ服务器。
(4)Queue:消息队列,用来存放消息。一个消息可投入一个或多个队列,多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
(5)Exchange:交换器,接受生产者发送的消息,根据路由键将消息路由到绑定的队列上。
(6)Routing Key:路由关键字,用于指定这个消息的路由规则,需要与交换器类型和绑定键(Binding Key)联合使用才能最终生效。
(7)Binding:绑定,通过绑定将交换器和队列关联起来,一般会指定一个BindingKey,通过BindingKey,交换器就知道将消息路由给哪个队列了。
(8)Connection :网络连接,比如一个TCP连接,用于连接到具体broker。
(9)Channel:信道,AMQP 命令都是在信道中进行的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接,一个TCP连接可以用多个信道。客户端可以建立多个channel,每个channel表示一个会话任务。
(10)Message:消息,由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
(11)Virtual host:虚拟主机,用于逻辑隔离,表示一批独立的交换器、消息队列和相关对象。一个Virtual host可以有若干个Exchange和Queue,同一个Virtual host不能有同名的Exchange或Queue。最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段。
交换器类型
RabbitMQ 常用的交换器类型有 Direct、Topic、Fanout、Headers 这四种。
1、 Direct 类型
Direct 类型的交换器由路由规则很简单,它会把消息路由到那些 BindingKey 和 RoutingKey 完全匹配的队列中。
Direct Exchange 是 RabbitMQ 默认的交换器模式,也是最简单的模式。它根据 RoutingKey 完全匹配去寻找队列。
2、 Topic 类型
上面讲到 Direct 类型的交换器由规则是完全匹配 BindingKey 和 RoutingKey,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。Topic 类型的交换器在匹配规则上进行了扩展,它与 Direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定:
(1)RoutingKey 为一个点号 “.” 分隔的字符串(被点号 “.” 分隔开的每一段独立的字符串称为一个单词),如:com.rabbitmq.client、java.util.concurrent、com.hidden.client;
(2)BindingKey 和 RoutingKey 一样也是点号 “.” 分隔的字符串;
(3)BindingKey 中可以存在两种特殊字符串星号 “*” 和井号 “#”,用于做模糊匹配,其中星号 “*” 用于匹配一个单词,井号 “#”用于匹配多个规则单词(0个或者多个单词);
3、 Fanout 类型
消息广播的模式,即将消息广播到所有绑定到它的队列中,而不考虑 RoutingKey 的值(不管路键或是路由模式)。如果设置了 RoutingKey ,则 RoutingKey 依然被忽略。
4、 Headers 类型
Headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。Headers 类型的交换器性能会很差,而且不实用,基本上不会看到它的存在。
介绍死信队列
DLX,全称为 Dead-Letter-Exchange,可以称为死信交换机,也有人称之为死信邮箱,当消息在一个正常队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX,绑定DLX的队列就称之为死信队列。
消息变成死信,可能由于以下原因:
消息被拒绝;
消息过期;
队列达到最大长度;
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何队列上被指定。实际上就是设置某一个队列的属性,当这个队列中存在死信时,rabbitMQ就会自动的将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
想要使用死信队列,只需要在定义队列的时候设置队列参数【x-dead-letter-exchange 指定交换机】即可。
延时队列
延时队列存储的对象是对应的延时消息。所谓 “延时消息” 是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
在RabbitMQ中并没有支持延时队列的功能,延时队列可以通过 【过期时间 + 死信队列】 来实现注意:创建的延时队列千万不要有消费者,一旦有消费者进行这个队列的消费,那么就不会是特定延时了,目的就是要这个队列里的消息TTL过时
生产者 --> 交换机 --> (路由键) --> 队列 --> (过期) --> 死信交换机 --> (路由键) --> 死信队列 --> 消费者
拓展
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
1. 分析
这一系列问题的本质其实就是消费端出问题了!!!(消费慢或者不消费了)
2. 线上出问题怎么解决(修复Comsumer、紧急扩容)
思路:临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据
1)修复consumer的BUG,确保其恢复消费速度,然后将现有cnosumer都停掉
2)创建10倍新队列:新建一个topic,queue是原来的10倍,临时建立好原先10倍或者20倍的queue数量
3)写一个临时的分发数据的consumer程序:这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
目的:把大量消息均匀分发到新队列上
4)Comsumer10倍扩容:临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据
5)打完收工:等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息
消息过期失效了怎么办?
(人工介入,半夜执行代码)
RabbitMQ的消息会有失效时间,写一个程序,把失效的消息ID找回来,等机子空闲的时候再手动塞进MQ里
消息队列满了怎么办?
(快速消费,假消费)
快速消费掉所有消息,如假装消费
(直接返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS),
先把消息保存下来,等空闲的时候再手动塞进MQ里