为什么需要消息队列
主要原因是由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达数据库,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。它常用来实现:异步处理、服务解耦、流量控制(削峰)
消息基于什么传输
由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。
消息队列的结构
消息队列的核心概念
Producer: 消息的生产者。
Connection:连接。
Channel: 消息通道,所有消息的流转都是通过channel,每个channel代表的是一次会话。
broker: server,消息队列服务器的实体,他是一个中间件应用,负责接受消息生产的消息,然后将消息发送至消息接受端或者其他的broker。比如 ,你在windows安装了 一个 activemq客户端,启动它,就是启动了 一个 broker
Exchange: 消息交换机,是消息到达的第一个地方,消息通过它指定的路由规则,分发到不同的消息队列中。
Queue: 消息队列
Binding: 绑定,作用就是将exchange和队列进行绑定。
Routing key:路由关键字,Exchange根据该关键字进行指定的消息投送。
consumer: 消息的消费者。
Virtual host: 虚拟主机,通过虚拟主机来继续对用户进行不同权限的分离。通过Virtual host来进行隔离exchange和Queue,不同的vitual host可以有相同的exchange和Queue
生产者消息运转
1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
2.Producer声明一个交换器并设置好相关属性。
3.Producer声明一个队列并设置好相关属性。
4.Producer通过路由键将交换器和队列绑定起来。
5.Producer发送消息到Broker,其中包含路由键、交换器等信息。
6.相应的交换器根据接收到的路由键查找匹配的队列。
7.如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。
8.关闭信道。
9.管理连接。
消费者消息运转
1.Producer先连接到Broker,建立连接Connection,开启一个信道(Channel)。
2.向Broker请求消费响应的队列中消息,可能会设置响应的回调函数。
3.等待Broker回应并投递相应队列中的消息,接收消息。
4.消费者确认收到的消息,ack。
5.RabbitMq从队列中删除已经确定的消息。
6.关闭信道。
7.关闭连接。
什么是RoutingKey路由键
生产者将消息发送给交换器的时候,会指定一个RoutingKey,用来指定这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
Binding绑定
通过绑定将交换器和队列关联起来,一般会指定一个BindingKey,这样RabbitMq就知道如何正确路由消息到队列了。
交换器的3种类型
主要有以下3种。
(1)direct路由模式:把消息路由到BindingKey和RoutingKey完全匹配的队列中。(精确匹配routing key)
该方式一个路由键对应一个消息队列,一个消息队列可以对应多个路由键,一个消息队列对应一个消费者,当一个队列下有多个消费者时,MQ采用的是轮询机制,选取一个消费者消费该队列下的消息,其他消费者则轮空。该模式给消息指明了准确的路线,告诉消息必须按照我制定的路线规则来走,适合于比较简单的场景,缺点是路由规则不够灵活。
适用于单发送,单接收的简单的应用场景
(2)topic通配符主题订阅模式:基本思想和路由模式是一样的,只不过路由键支持模糊匹配,符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词,路由规则变得灵活多变,可拓展性非常的强(模糊匹配routing key)
适用于单发送单接收以及单发送多接收的场景
(3)fanout路由模式:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。(忽略routing key)
该模式之所以称之为广播模式,是因为他的交换机没有绑定任何路由规则,交换机直接和队列相关联,所有和交换机关联的队列都会收到生产者发出的消息,也就是说该模式的生产者在发出消息的那一刻并不知道消息将会发给谁,将会有多少队列接受消息,这样的机制增加了很多的不确定性,但是也给适合该模式的场景提供了很大的便利。
适用于单发送,多接收的应用场景
总结:topic和direct两种模式的消息路线为:生产者–>交换机–>路由键–>队列–>消费者–>消息处理代理类;fanout模式的消息路线为:生产者–>交换机–>队列–>消费者–>消息处理代理类。
消息确认机制
RabbitMQ的消息确认机制是为了确保消息发送者知道自己发布的消息被正确接收,如果没有收到确认时就会认为消息发送过程发送了错误,此时就会马上采取措施,以保证消息能正确送达(类似于HTTP的建立连接时的确认答复)。
具体做法如下:
当RabbitMQ发送消息以后,如果收到消息确认,才将该消息从Quque中移除。如果RabbitMQ没有收到确认,如果检测到消费者的RabbitMQ链接断开,则RabbitMQ 会将该消息发送给其他消费者;否则就会重新再次发送一个消息给消费者。
如何保证消息正确的发送至rabbitmq
RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。
发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
如何确保消息接收方消息了消息
接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。
这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。
下面罗列几种特殊情况:
消息队列的优点和缺点
优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰
缺点呢?显而易见的
系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,本来ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。
系统复杂性提高:硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已
一致性问题:A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,最好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10倍。但是关键时刻,用,还是得用的。。。
死信队列
DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
导致死信的几种原因
(1)消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
(2)消息TTL过期
(3)队列满了,无法再添加。
如何保证消息不丢失
1.消息持久化。包含了Exchange设置持久化,Queue设置持久化,Message持久化发送
2.Ack确认机制。就是消费端消费完成要通知服务端,服务端才把消息从内存删除。
3 .设置集群镜像模式。把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案
4.消息补偿机制。在消息发送、接收时记录DB日志,定时轮训DB日志,查明哪些发送消息没有成功消费,启动重新发送消息机制。
rabbitmq的重试机制
消费端在处理消息过程中可能会报错,此时该如何重新处理消息呢?解决方案有以下两种。
在redis或者数据库中记录重试次数,达到最大重试次数以后消息进入死信队列或者其他队列,再单独针对这些消息进行处理;
使用spring-rabbit中自带的retry功能;
这里主要说下第二种方案
rabbitmq默认不开启重试机制,需要在配置文件开启配置
我亲测了以下5种情况,得出以下结论
(1)手动确认,不配置重试机制:消息会发送一次,消息未确认
(2)手动确认,配置重试机制:消息会发送多次,但是消息不会被确认
(3)自动确认,不配置重试机制:消息会不断发送,消息不会被确认
(4)自动确认,配置重试机制:消息会发送多次,消息会被确认
(5)不配置确认机制以及重试机制:消息会不断发送,消息不会被确认
所以这里要使用重试机制就有2种情况了: 1.如果消息是自动确认,由于异常,多次重试还是失败,消息被自动确认,那消息就丢失了 2.如果消息是手动确认,由于异常,多次重试还是失败,消息没被确认,也无法nack,就一直是unacked状态,导致消息积压
如何既保证重试又能不丢失消息呢?这只是我的构思:
首先是手动确认,然后在catch中throw异常触发重试机制,然后定义一个局部变量错误次数retryCount,失败后++,当retryCount=重试次数,则nack并且requeue=false到死信队列中进行入库操作,方便后续人工补偿。
消费端模拟异常代码如下:
注意:不答消息被消费了之后是手动确认还是自动确认,代码中不能使用try/catch捕获异常,否则重试机制失效
通过控制台可以看到重试次数是三次,随后进入到了死信队列
消费端死信队列的创建
将死信队列和交换器绑定
如何保证消息不重复消费
消息重复消费的原因有两个:1.生产时重复消费 2.消费时消息重复
生产时重复消费:
由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。
生产者中如果消息未被确认,或确认失败,我们可以使用定时任务+(redis/db)来进行消息重试。
消费时重复消费:
消费者消费成功后,再给MQ确认的时候出现了网络波动,MQ没有接收到确认,为了保证消息被消费,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。
解决重复消费的要点是要保持幂等性
让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:
(1)消费者获取到消息后先根据id去查询redis/db是否存在该消息
(2)如果不存在,则正常消费,消费完毕后写入redis/db
(3)如果存在,则证明消息被消费过,直接丢弃