一、rabbitMq组件:
Producer : 消息生产者,投递消息的程序;
Consumer : 消息消费者, 接受消息的程序;
Server (Broker) : 消息节点,维护生产者到消费者的路线;
Connection : 连接, 应用程序和 broker 的网络连接
Channel : 信道, 所有操作都在 channe 中进行, channel 是进行消息读写的通道
是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给 queue
Message : 消息, 服务器和应用程序之间传递的数据; 由 properties 和 Body 组成,
Virtual host :虚拟机是个概念,每个 broker 有数个 vhost, 每个 vhost 有数个 Exchanage 和 message Queue, 同一个 virtual host 不能有 名称相同的 Exchange
和 Queue。
拥有独立的权限系统,可以做到 vhost 范围的用户控制
Exchange : 交换机, 接受消息, 根据路由键转发消息到绑定的队列。 如果没有队列绑定到交换机,交换机就丢弃生产者的消息;
exchange 内部实现为保存 binding 关系的查找表
Binding Key : Exchange 和 Queue 之间的虚拟链接, 用于指定当前队列和交换机 Exchange 的 key, 只用 routing key 与 binding key 相同的时候,交换机才
根据路由规则分发到匹配的队列;
Routing key : 路由键,生产者 将消息发送给交换机,一般会指定一个routing key 来指定路由规则;
Queue : (Message) Qeueu 消息队列, 保存消息并发给消费者
二、消息分发流程
2.1、消息分发流程
生产者发送消息流程
1、生产者连接到 RabbitMq Broker, 建立连接(Connection), 开启信道;
2、生产者声明一个交换器,并设置相关属性,比如交换机类型, 是否持久化等
3、生产者声明一个队列并设置相关属性, 是否排他、持久化、自动化删除等
4、生产者通过路由键将交换器和队列绑定;
5、生产者发送消息至RabbitMq Broker, 其中包含路由键,交换器等信息;
6、相应的交换器根据收到的路由键查找相匹配的队列;
消费者接受消息流程
1、生产者连接到 Rabbit Mq Broker, 建立连接(Connection), 开启信道;
2、消费者象 RabbitMq Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,和准备工作;
3、等待 RabbitMq Broker 回应并投递相应队列中的消息,消费者接受消息;
4、消费者确认 ACK 接受到的消息
5、RabbitMQ 从队列中删除以及确认的额消息
6、关闭信道,关闭连接
2.2、消息如何路由
从概念上来说,消息路由必须有三部分:交换器、路由、绑定。生产者把消息发布到交换器上;绑定决定了消息如何从路由器路由到特定的队列;消息最终到达队列,并被消费者接收。
- 消息创建时,会设定一个路由键 routing key, 并发送发送到交换器;
- 消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入 “黑洞”。
常用的交换器主要分为一下三种:
- direct:如果路由键完全匹配,消息就被投递到相应的队列
- fanout:如果交换器收到消息,将会广播到所有绑定的队列上
- topic:可以使来自不同源头的消息能够到达同一个队列。 使用topic交换器时,可以使用通配符,比如:“*” 匹配特定位置的任意文本, “.” 把路由键分为了几部分,“#” 匹配所有规则等。特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是由"."隔开的一系列的标识符组成。
2.3、消息基于什么分发
RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。
2.4、消息如何分发
若该队列至少有一个消费者订阅,消息将以轮询(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。
2.5、如何确保消息正确的发送到 rabbitMq
RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。
如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
2.6、如何确保消息接收方消费了消息
接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。
下面罗列几种特殊情况:
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
- 如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
2.7、如何避免消息重复投递或重复消费?
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
这个问题针对业务场景来答分以下几点:
1.比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
2.再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
3.如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
如何保证消息不被重复消费?(如何保证消息消费时的幂等性): https://blog.csdn.net/dsa572713470/article/details/99683457?utm_medium=distribute.pc_relevant.none-task-blog-title-4&spm=1001.2101.3001.4242
2.8、如何解决丢数据的问题?
1.生产者丢数据
生产者的消息没有投递到MQ中怎么办?从生产者弄丢数据这个角度来看,RabbitMQ提供transaction(事务)和confirm(确认)模式来确保生产者不丢消息。
transaction机制就是说,发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。
然而缺点就是吞吐量下降了。因此,按照博主的经验,生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
2.消息队列丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步
①、将queue的持久化标识durable设置为true,则代表是一个持久的队列
②、发送消息的时候将deliveryMode=2
这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)
3.消费者丢数据
启用手动确认模式可以解决这个问题
①自动确认模式,消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。
②手动确认模式,如果消费者来不及处理就死掉时,没有响应ack时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在finally里ack,也会一直重复发送消息(重试机制)。
③不确认模式,acknowledge="none" 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。
2.9、死信队列和延迟队列的使用
死信队列
- 消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false
- 消息过期了
- 队列达到最大的长度
过期消息
在 rabbitmq 中存在2种方可设置消息的过期时间,第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。
队列设置:在队列申明的时候使用 x-message-ttl 参数,单位为 毫秒
单个消息设置:是设置消息属性的 expiration 参数的值,单位为 毫秒
延时队列:在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
三、六种消息分发模式
交换器一共有四种类型:fanout、direct 、topic、headers, 还有RPC 这里不介绍
0、默认的: 当没有设定以上任何一种情况的时候,采用默认的方式;
直接根据消息 附带的 队列信息 发送到对应的队列上
1、fanout(订阅模式) : 如果交换器收到消息,将会广播到所有绑定的队列上
2、direct(路由模式) : 消息根据 routerKey 发送通过对应的 bindKey 发到 queue 队列上
处理路由键,需要将一个队列绑定到交换机上, 要求该消息与一个特定的路由键完全匹配,这是一个完整的匹配
如果,一个队列绑定到该交换机上要求路由键 “dog”
则只有被标记为“dog”的消息才被转发,不会转发dog.puppy
也不会转发dog.guard,只会转发dog
3、topic(通配模式) 实现 bingkey 的模糊匹配
将路由键和某模式进行匹配,此时,队列需要绑定要一个模式上
符号“#”匹配一个,或者多个词
符号“*”匹配不多不少一个词
因此,“audit.#”能够匹配到“audit.irs.corporate”
但是“audit.*” 只会匹配到“audit.irs”
4、header
四、需求分析
有了以上的基础知识,我们完成以下需求:
需求:用户在系统中创建一个订单,如果超过时间用户没有进行支付,那么自动取消订单。
分析:
1、上面这个情况,我们就适合使用延时队列来实现,那么延时队列如何创建
2、延时队列可以由 过期消息+死信队列 来时间
3、过期消息通过队列中设置 x-message-ttl 参数实现
4、死信队列通过在队列申明时,给队列设置 x-dead-letter-exchange 参数,然后另外申明一个队列绑定x-dead-letter-exchange对应的交换器。
ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); factory.setPort(AMQP.PROTOCOL.PORT); factory.setUsername("guest"); factory.setPassword("guest"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); // 声明一个接收被删除的消息的交换机和队列 String EXCHANGE_DEAD_NAME = "exchange.dead"; String QUEUE_DEAD_NAME = "queue_dead"; channel.exchangeDeclare(EXCHANGE_DEAD_NAME, BuiltinExchangeType.DIRECT); channel.queueDeclare(QUEUE_DEAD_NAME, false, false, false, null); channel.queueBind(QUEUE_DEAD_NAME, EXCHANGE_DEAD_NAME, "routingkey.dead"); String EXCHANGE_NAME = "exchange.fanout"; String QUEUE_NAME = "queue_name"; channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); Map<String, Object> arguments = new HashMap<String, Object>(); // 统一设置队列中的所有消息的过期时间 arguments.put("x-message-ttl", 30000); // 设置超过多少毫秒没有消费者来访问队列,就删除队列的时间 arguments.put("x-expires", 20000); // 设置队列的最新的N条消息,如果超过N条,前面的消息将从队列中移除掉 arguments.put("x-max-length", 4); // 设置队列的内容的最大空间,超过该阈值就删除之前的消息 arguments.put("x-max-length-bytes", 1024); // 将删除的消息推送到指定的交换机,一般x-dead-letter-exchange和x-dead-letter-routing-key需要同时设置 arguments.put("x-dead-letter-exchange", "exchange.dead"); // 将删除的消息推送到指定的交换机对应的路由键 arguments.put("x-dead-letter-routing-key", "routingkey.dead"); // 设置消息的优先级,优先级大的优先被消费 arguments.put("x-max-priority", 10); channel.queueDeclare(QUEUE_NAME, false, false, false, arguments); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); String message = "Hello RabbitMQ: "; for(int i = 1; i <= 5; i++) { // expiration: 设置单条消息的过期时间 AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder() .priority(i).expiration( i * 1000 + ""); channel.basicPublish(EXCHANGE_NAME, "", properties.build(), (message + i).getBytes("UTF-8")); } channel.close(); connection.close();
五、消息队列的缺点
1.系统可用性降低:你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低
2.系统复杂性增加:要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输。因此,需要考虑的东西更多,系统复杂性增大。
-------------------------------------------------
【1】https://blog.csdn.net/Yang_Hui_Liang/article/details/103300388?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduend~default-1-103300388.nonecase&utm_term=rabbitmq%E5%87%A0%E7%A7%8D%E8%AE%A2%E9%98%85%E6%A8%A1%E5%BC%8F
【2】https://www.cnblogs.com/Jeely/p/10784013.html
【3】https://blog.csdn.net/jerryDzan/article/details/89183625