RabbitMq基本概念
代码地址:https://github.com/zhangbaoss/spring-boot-amqp
一、概念
MQ全称是Message Queue(消息队列),是一种分布式应用程序的通信方法,它是消费者-生产者模型的典型代表,producer往消息队列中不断的写入消息,而另一端的consumer则可以读取或者订阅队列中的消息,RabbitMq是Mq的一种,是一种基于AMQP协议可复用的企业消息系统.
二、作用
- 1.解耦:通过基于数据的接口层,让不同模块各自扩展修改,实现解耦
- 2.持久化:可以对数据进行持久化直到它们被完全处理,避免数据丢失
- 3.扩展性:通过解耦可以方便增加应用的处理过程,提高消息入队和处理的效率,实现扩展
- 4.削峰:Mq可以支撑关键组件突发访问压力,缓冲流量差,实现削峰
- 5.可恢复性:部分组件失效时,加入消息队列的消息仍然可以在系统恢复后处理
- 6.顺序保证:MQ支持一定的顺序性
- 7.异步处理:不需要立即处理的消息可以通过MQ进行异步处理
三、组件介绍
1.Queue(队列)
Queue(队列)是RabbitMq中的一个组件,用于存储消息,RabbitMQ中的消息只能存储在Queue中.模型图如下:
2.简单队列
模型图如下:
- P:消息生产者(用户生产消息)
- C:消息消费者(获取队列中消息)
- 红色:队列(储存消息容器)
基本流程:生产者将消息发送到队列中,消费者从队列中获取消息
代码在com.zhangbao.amqp.simple包下
简单队列的不足:
耦合性高,生产者和消费者一一对应,多个消费者想要同时消费同一个队列里的消息时不能满足,队列名称修改时需要同时修改
3.工作队列(Work Queues)
使用工作队列的原因:
应用程序在使用消息系统时,生产者P发送消息到队列中很快速,而消费者C接收完消息再进行处理需要耗费一定的时间,此时就会导致很多消息堆积在一个队列中,一一对应的简单队列中消费者就不够用了
工作队列的优点:
在同一个队列上创建多个消费者,让他们相互竞争,此时可以轻易的并行工作,如果积压的工作过多,可以增加消费者来解决问题,使系统的伸缩性更加容易
工作队列有两种工作模式:
3.1.轮询分发
特点:假如一个队列有两个消费者,无论消费者1处理的速度比消费者2处理的速度快多少,最终队列中的消息都会平均(而不是所有)分给两个消费者进行处理
模型图如下:
代码:com.zhangbao.amqp.work.poll包下
3.2.公平分发(能者多劳)
特点:由于轮询分发会因为每个消费者处理的速度不同导致效率不高,我们可以使用
模型图如下:
channel.basicQos(prefetchCount = 1)
方法来限制队列发给每个消费者的消息数.队列每次只发1条消息给一个消费者,当消息处理完后,给队列反馈才会进行第二次发送.(需要手动反馈给RabbitMq)
注意: 使用公平分发,必须关闭自动应答,改为手动应答.
//修改自动应答为手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
代码在com.zhangbao.amqp.work.fair包下
4.消息应答
一旦rabbitMq将消息发送给消费者后就会将rabbitMq中的该消息删除 假如处理消息的消费者在处理消息的过程中宕机,则会丢失该消息 可以将autoAck设置为false变为手动确认模式, 如果rabbitMq没有收到回执,则认为第一个消费者处理失败,将会将消息发送给另一个消费者 直到收到消息回执,才会删除内存中的消息数据.这里没有处理超时概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。 但是还会有另外一个问题,如果在处理完业务逻辑后,没有发送回执给队列,就会导致队列中堆积的消息会越来越多;消费者重启后还会重复获取这些消息并重复执行业务逻辑
5.持久化
如果希望在rabbitMq服务重启后消息也不会丢失,可以将队列和消息都设置为持久化
//队列持久化durable = true
boolean durable = false;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
这样可以保证大部分情况下都可以保证消息不会丢失,但是如果RabbitMq服务器已经获取到生产者发送的消息还没来的及持久化就宕机了,可以使用事务来保证消息不会丢失.
注意
rabbitMq不能在代码中更改已经存在的queue的参数,即: 如果想将一个已经创建好的queue的持久化方式从内存改为磁盘直接更改durable参数是不可以的.
6.交换机(路由器Exchange)
以上的消息都是直接投递到队列Queue中的,但是实际应用中,生产者将消息发送到路由器(Exchange)中,再由Exchange将消息发送给符合规则的队列中.
模型图如下:
注意: 路由器没有存储消息的能力,只有队列能存储消息.
6.1路由器类型(Exchange Types)
- fanout:消息发送给路由器,路由器会将消息发送给所有与该交换机绑定的队列
- direct:路由器会将消息发送给bindingKey和routingKey完全匹配的队列
模型图如下:
- topic:路由器将消息发送给bingingKey和routingKey规则匹配的队列,routingKey是一个由"."分割的几个单词组成的字符串,如"tianmao.goods.add",bindingKey也是由"."分割的字符串,其中可以有"#"和"*"字符用来模糊匹配,"#"代表匹配任意数量的任意字母,"*"表示匹配单个任意字母.比如"#.goods.ad*"可以匹配到"tianmao.goods.add"队列.没有匹配到队列的消息将会丢失,因为存储消息的只有队列,路由器不能存储消息
模型图如下:
- headers:发送消息时携带键值对的headers,和队列绑定交换机时的键值对进行比对,如果匹配则将消息路由到该路由中
6.2绑定(Binding)
RabbitMQ将路由器(Exchange)和队列(Queue)关联在一起,将消息发送给路由器时会携带一个bindingKey(routingKey),在队列和路由器绑定时也会有一个routingKey,当两个routingKey相同时,路由器(Exchange)才会将消息发送到队列(Queue)中.
Producer:
//声明转发器,完全匹配规则设置类型为direct
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//发送消息到路由,并设置路由为error
String routingKey = "error";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes());
Consumer1:
//绑定交换机并设置路由为error
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
Consumer2:
//绑定交换机并设置路由为error和info
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
-------------------------------------------------------------------------
Producer:
//声明路由器,模糊匹配规则设置类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//设置routingKey为goods.add
String routingKey = "goods.update";
Consumer1:
//绑定交换机并设置路由为goods.add和goods.delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.add");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.delete");
Consumer2:
//绑定交换机并设置路由为goods.#
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");
7.发布订阅模式(pub/sub)
前面我们讲到了简单队列模式(生产者和消费者一一对应),工作队列(消息可以平均(轮询)或根据效率(公平)分发给多个消费者进行处理),但是如果每个消费者都需要获取生产者的所有消息怎么实现呢?此时将交换机的类型设置为Fanout类型,就可以实现发布订阅模式.交换机会将消息发送给所有与该交换机绑定的队列中.
模型图如下:
代码见com.zhangbao.amqp.pubsub包中
生产者将消息发送给rabbitMq,那么消息到底有没有到达RabbitMq呢?假如消息在到达rabbitMq服务器之前丢失,即使是开启持久化也解决不了问题,因为消息根本就没有到达服务器. Rabbit给我们提供了两种解决方式:
- 1.通过AMQP事务机制处理该问题
- 2.将channel设置为confirm模式处理该问题
8.事务
RabbitMq与事务处理机制有关的方法一共有三个:txSelect();txCommit();txRollback();
- txSelect():用于将当前 channel 设 置成 transaction 模式,
- txCommit():用于提交事务,
- txRollback():用于回滚事务.
在通过 txSelect 开启事务之后,我们便 可以发布消息给 broker 代理服务器了,如果 txCommit 提交成功了,则消息一定到达了 broker 了,如果在 txCommit 执行之前 broker 异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback 回滚事务了。
代码见com.zhangbao.amqp.tx包
9.confirm模式
上面使用事务来解决消息有没有发送到rabbitMq服务器,但是使用事务会影响消息吞吐量,所以可以使用confirm模式来解决上面问题.
实现原理:
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID) ,这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写 入磁盘之后发出,broker 回传给生产者的确认消息中 deliver-tag 域包含了确认消息的序列号,此外 broker 也可以设 置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继 续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
开启confirm方式:
注意:已经在 transaction 事务模式的 channel 是不能再设置成 confirm 模式的,即这两种模式是不能共存的.
生产者通过调用 channel 的 confirmSelect 方法将 channel 设置为 confirm 模式
//生产者通过调用channel的confirmSelect方法将channel设置为confirm模
channel.confirmSelect();
编程模式:
* 1. 普通 confirm 模式:每发送一条消息后,调用 waitForConfirms()方法,等待服务器端 confirm。实际上是一种串行 confirm 了。
* 2. 批量 confirm 模式:每发送一批消息后,调用 waitForConfirms()方法,等待服务器端 confirm。
* 3. 异步 confirm 模式:提供一个回调方法,服务端 confirm 了一条或者多条消息后 Client 端会回调这个方法.
代码见com.zhangbao.amqp.confirm包
10.SpringBoot中使用RabbitMq
- 1.添加依赖:
org.springframework.boot spring-boot-starter-amqp - 2.添加配置文件:
spring.rabbitmq.username=
spring.rabbitmq.password=
spring.rabbitmq.virtual-host= - 3.具体实现见包
com.zhangbao.amqp.spring