RabbitMQ
1.1 MQ概述
MQ全称Message Queue (消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。
- MQ,消息队列,存储消息的中间件
- 分布式系统通信两种方式:直接远程调用和借助第三方完成间接通信
- 发送方称为生产者,接收方称为消费者
1.2MQ优势
1.应用解耦
MQ使得应用直接得到解耦,容错和可维护性提高
2.异步提速
MQ可以提高相应时间,增加系统吞吐量
3.削峰填谷
使用了MQ之后,限制消费消息的速度为1000,区忤一米,高峰用一工的以效X拍力么队IVL40IL古就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。
总结:
- 应用解耦:提高系统容错性和可维护性
- 异步提速:提升用户体验和系统吞吐量
- 削峰填谷:提高系统稳定性
1.3 MQ的劣势
-
系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。如何保证MQ的高可用?
-
系统复杂度提高
MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?
-
一致性问题
A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。如何保证消息数据处理的一致性?
总结:
1.生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
2.容许短暂的不一致性。
3.确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。
2.RabbitMQ中的相关概念:
AMQP
即Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP规范发布。类比HJTP。
RabbitMQ的整体架构及核心概念:
- virtual-host:虚拟主机,起到数据隔离的作用
- publisher:消息发送者
- consumer:消息的消费者
- queue:队列,存储消息
- exchange:交换机,负责路由消息
RabbitMQ提供了6种工作模式:
- 简单模式
- work queues
- Publish/Subscribe发布与订阅模式
- Routing路由模式
- Topics主题模式
- RPC远程调用模式(远程调用,不太算MQ;暂不作介绍)。
官网对应模式介绍: https://www.rabbitmq.com/getstarted.html
3.RabbitMQ的安装
参考教程:Linux安装RabbitMQ详细教程(最详细的图文教程)
- RabbitMQ控制台的默认端口为:15672
- RabbitMQ服务的默认端口为:5672
登录后别忘记设置自己的权限
4.RabbitMQ数据隔离
因为RabbitMq中存在一个virtual host的虚拟主机概念,在拥有虚拟主机控制权的用户可以任意对该virtual host进行操作,包括增加Queue,删除Queue,增加Exchanges等操作(需要有对应的权限).
这样各个虚拟主机相互隔离.
注意:创建一个Virtual Host的用户之后就拥有对该虚拟主机的掌控权,而其他用户是没有权限的(可以使用超级管理员权限进行添加)
5.spring整合RabbitMQ
AMQP
Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
SpringAmqp的官方地址:https://spring.io/projects/spring-amqp
<1>入门案例
简单模式:
- 利用控制台创建队列simple.queue
- 在publisher服务中,利用SpringAMQP直接向simple.queue发送消息
- 在consumer服务中,利用SpringAMQP编写消费者,监听simple.queue队列
(1)引入依赖:
在两个模块Publisher和Consumer添加依赖:
<!-―AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
(2)在每个服务里都编写yml文件:
spring:
rabbitmq:
#服务主机地址
host: 192.168.37.136
#服务端口
port: 5672
#虚拟主机名称
virtual-host: myadminhost
#用户名和密码
username: myadmin
password: 123456
(3)编写代码:
在spring-Rabbit中提供了对应的RabbitTemplate模板来实现消息的发送和接受,我们在使用时只需要将对应的模板类(RabbitTemplate)注入即可使用
@Service
public class ServicePublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 简单方式传输,Publisher-->Queue-->Consumer
* 调用convertAndSend方法,传入队列名称和对应的信息就可以实现发送
* @param queueName 消息队列名称
* @param message 消息1
*/
public void simplePublishMessage(String queueName,String message) {
rabbitTemplate.convertAndSend(queueName,message);
}
}
测试一下:
@SpringBootTest
public class PublishTest {
@Autowired
private ServicePublisher servicePublisher;
@Test
void testSimplePublish(){
servicePublisher.simplePublishMessage("myqueue","Hello!");
}
}
运行方法后在控制台中可以查看到信息:
(4)接收消息
SpringAMQP提供声明式的消息监听,我们只需要通过注解在方法上声明要监听的队列名称,将来SpringAMQP就会把消息传递给当前方法:
@Component
@Slf4j
public class MessageConsumer {
@RabbitListener(queues = "myqueue")
public void consume(String message){
log.info("接收消息:{"+message+"}");
log.info("接受消息成功");
}
}
使用@RabbitListener(queues = “myqueue”)指定监听的消息队列(可以指定多个队列)
当接受到消息时就会分装到参数message中,再执行其中的方法
SpringAMQP如何收发消息?
- 1.引入spring-boot-starter-amqp依赖
- 2.配置rabbitmq服务端信息
- 3.利用RabbitTemplate发送消息
- 4.利用@RabbitListener注解声明要监听的队列,监听消息
<2>工作模式
案例:
- 创建一个工作队列work.queue
- 在该队列绑定两个消费者
- 一个消费者处理消息能力快,一个处理消息能力慢
- 在发送方,向该队列中发送50条消息
消息消费方
@RabbitListener(queues = "work.queue")
public void workConsume1(String message) throws InterruptedException {
System.out.println("消费者1接受消息:{" + message + "}");
//模拟处理消息快
Thread.sleep(20);
}
@RabbitListener(queues = "work.queue")
public void workConsume2(String message) throws InterruptedException {
System.err.println("消费者2接受消息:{" + message + "}");
//模拟处理消息慢
Thread.sleep(200);
}
调用发送消息的方法后,观察输出:
- 会发现消息都是按照轮询的方式发送到各个消费者的
- 消费者1因为处速度够快,所以很快就将消息处理完了
- 消费者2因为处理速度慢,所以一直到很消费者1除了完成后还在处理
消费者消息推送限制
所以,我们希望消费者1能够处理更多的消息
这时就需要修改我们的配置文件:
listener:
simple:
prefetch: 1 #每个监听者只有当处理完成自己的一个消息后才能接受下一个消息
默认情况下,RabbitMQ的会将消息依次轮询投递给绑定在队列上的每一个消费者。但这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积。
因此我们需要修改application.yml,设置preFetch值为1,确保同一时刻最多投递给消费者1条消息:
Work模型的使用:
- 多个消费者绑定到一个队列,可以加快消息处理速度
- 同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量,处理完一条再处理下一条,实现能者多劳
<3>Fanout交换机模式
真正生产环境都会经过exchange来发送消息,而不是直接发送到队列,交换机的类型有以下三种:
- Fanout:广播
- Direct:定向
- Topic:话题
Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的queue,所以也叫广播模式
-
在控制台中新增一台交换机,指定类型为:fanout(广播)
-
新建两个队列
-
将两个队列绑定到交换机上
-
编写代码:
消息接收者
@RabbitListener(queues = "fanout.queue1") public void fanoutConsumer1(String message) throws InterruptedException { System.out.println("fanout消费者1接受消息:{" + message + "}"); } @RabbitListener(queues = "fanout.queue2") public void fanoutConsume2(String message) throws InterruptedException { System.out.println("fanout消费者2接受消息:{" + message + "}"); }
消息发送者:
/** * 广播发送 * @param exchange 交换机名称 * @param message 信息 */ public void fanoutPublishMessage(String exchange,String message){ rabbitTemplate.convertAndSend(exchange,null,message); }
这里需要传三个参数:交换机名称,key,消息内容
因为没有key,所以先传入null值
交换机的作用是什么?
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- FanoutExchange的会将消息路由到每个绑定的队列
<4>Direct交换机模式
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为定向路由。
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
案例:
- 有两个队列,分别绑定一个交换机
- 交换机与两个队列绑定key值
在控制台中完成建立交换机,队列,和绑定队列和设置键值的操作
编写代码:
消息接收者:
//key值:red,yello
@RabbitListener(queues = "direct.queue1")
public void directConsumer1(String message) {
System.out.println("direct消费者1接受消息:{" + message + "}");
}
//key值:red,blue
@RabbitListener(queues = "direct.queue2")
public void directConsumer2(String message) {
System.out.println("direct消费者2接受消息:{" + message + "}");
}
消息发送者:
/**
* 直接发送
* @param exchange 交换机名称
* @param routeKey 路由key
* @param message 消息
*/
public void directPublishMessage(String exchange,String routeKey,String message){
rabbitTemplate.convertAndSend(exchange,routeKey,message);
}
测试代码:
@Test
void testDirectPublish(){
servicePublisher.directPublishMessage("direct.exchange","blue","Hello Direct Everyone!");
}
最后结果:
<5>Topic交换机模式
TopicExchange与DirectExchange类似,区别在于routingKey可以是多个单词的列表,并且以"."分割。
Queue与Exchange指定BindingKey时可以使用通配符:
- #:代指0个或多个单词
- *****:代指一个单词
案例:
建立两个队列绑定到一台交换机上,设置Key值
编写代码:
消息接收者:
//key值:china.#
@RabbitListener(queues = "topic.queue1")
public void topicConsumer1(String message) {
System.out.println("topic消费者1接受消息:{" + message + "}");
}
//key值:#.new
@RabbitListener(queues = "topic.queue2")
public void topicConsumer2(String message) {
System.out.println("topic消费者2接受消息:{" + message + "}");
}
消息发送者:
@Test
void testTopicPublish(){
servicePublisher.topicPublishMessage("topic.exchange","china.new","Hello China!");
}
测试结果:
描述下Direct交换机与Topic交换机的差异?
-
Topic交换机接收的消息RoutingKey可以是多个单词,以.分割
-
Topic交换机与队列绑定时的bindingKey可以指定通配符
#:代表0个或多个词
*:代表1个词
<6>声明交换机和队列
SpringAMQP提供了几个类,用来声明队列、交换机及其绑定关系
- Queue:用于声明队列,可以用工厂类QueueBuilder构建
- Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建
- Binding:用于声明队列和交换机的绑定关系,可以用工厂类BindingBuilder构建
交换机有多种:
因为消息发送者不关系队列和交换机,他只要知道发送的交换机对象即可,故我们声明队列和交换机时通常是在消息接收者声明的
例如:
@Configuration
public class FanoutConfiguration {
/**
* 交换机Bean对象
* @return
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("config.exchange");
//不止可以使用return方法返回,还可以使用工厂方式返回:
//ExchangeBuilder.fanoutExchange("config.exchange").build();
}
/**
* 消息队列Bean对象
* @return
*/
@Bean
public Queue fanoutQueue(){
return new Queue("config.queue");
//也可以使用工厂模式:
//QueueBuilder.durable("config.queue").build();
//durable表示持久化,也就是对象是写在磁盘的
}
/**
* 绑定交换机和队列
* @param fanoutQueue
* @param fanoutExchange
* @return
*/
@Bean
public Binding faoutBinding(Queue fanoutQueue,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
}
/*
这里还有一种写法为:
@Bean
public Binding faoutBinding(){
return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
}
这里的逻辑是:因为fanoutQueue()和fanoutExchange已经在之前就创建了队列和交换机的对象,所以
在@Bean注释下,当调用fanoutQueue()和fanoutExchange()方法时就会到IOC容器中查找相应的Bean
对象,发现存在,所以就会直接返回对象,而不会再执行方法,所以就不需要传入参数
*/
}
若想指定key值,需要再@Bean中用with方法声明,但每次只能指定一个key值,若想绑定多个key,那就会十分的繁琐:
@Bean
public Binding directBinding1(Queue fanoutQueue, DirectExchange directExchange){
return BindingBuilder.bind(directQueue).to(directExchange).with("red");
}
@Bean
public Binding directBinding2(Queue fanoutQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue).to(directExchange).with("blue");
}
.......
所以我们一般使用注解来进行声明队列属性和绑定的交换机属性(如果没有spring会自动创建队列和交换机)
//key值:red,yello
@RabbitListener(bindings =@QueueBinding(
value = @Queue(value = "direct.queue1",durable = "true"),
exchange = @Exchange(value = "direct.exchange",type = ExchangeTypes.DIRECT),
key = {"red", "yello"}
))
public void directConsumer1(String message) {
System.out.println("direct消费者1接受消息:{" + message + "}");
}
//key值:red,blue
@RabbitListener(bindings =@QueueBinding(
value = @Queue(value = "direct.queue2",durable = "true"),
exchange = @Exchange(value = "direct.exchange",type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void directConsumer2(String message) {
System.out.println("direct消费者2接受消息:{" + message + "}");
}
通过binding属性指定绑定的队列和队列绑定的交换机,指定他们的名称,属性和绑定的key值
声明队列、交换机、绑定关系的Bean是什么?
- Queue
- FanoutExchange
- DirectExchange
- TopicExchange
- Binding
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
- @Queue
- @Exchange
<7>消息转化
如果往队列中传入的是对象数据呢?那么他肯定是需要进行序列化的.
现在创建一个object.queue来传入消息,向其中传入Map对象数据:
/**
* 发送对象
* @param object
*/
public void objectPublishMessage(String queuename,Object object){
rabbitTemplate.convertAndSend(queuename,object);
}
@Test
void testObjectPublish(){
Map<String,Object> map=new HashMap<>();
map.put("name","小明");
map.put("age",23);
servicePublisher.objectPublishMessage("object.queue",map);
}
传入之后查看其中的消息:
会发现他是经过序列化的结果,那么他是通过什么来序列化的呢?
通过查看源码可以发现,它默认是通过JDK的序列化的:
@Override
protected Message createMessage(Object object, MessageProperties messageProperties)
throws MessageConversionException {
byte[] bytes;
if (object instanceof String) {
try {
bytes = ((String) object).getBytes(this.defaultCharset);
}
catch (UnsupportedEncodingException e) {
throw new MessageConversionException("failed to convert Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
messageProperties.setContentEncoding(this.defaultCharset);
}
else if (object instanceof byte[]) {
bytes = (byte[]) object;
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES);
}
else {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
this.serializer.serialize(object, output);
}
catch (IOException e) {
throw new MessageConversionException("Cannot convert object to bytes", e);
}
bytes = output.toByteArray();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT);
}
if (bytes != null) {
messageProperties.setContentLength(bytes.length);
}
return new Message(bytes, messageProperties);
}
所以我们一般不采用这种方法进行序列化(如果是spring-mvc则自带消息转换器,不需要配置),我们引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
之后在子模块引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
这个可以让json和对象之间互相遍历的转化,我们这时配置Rabbitmq的消息转化器:
//配置消息转化器
@Configuration
public class MessageConfiguration {
//引入消息转化器
@Bean
public MessageConverter JsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
}
注意:这个消息转化器需要在发送和接收方都要配置,因为二者都要发送消息
再次发送结果:
可见字节数和可读性都提高了
6.MQ高级特性
在消息发送后可能因为各种各样的原因导致消息没有办法达到最后的结果:
有三个原因可能导致消息发送后达不到对应的结果:
- 发送者异常
- MQ异常
- 消费者异常
所以我们从这三个方面来解决:
<1>发送者的可靠性
1.生产者重连
有的时候由于网络波动,可能会出现客户端连接MQ失败的情况。通过配置我们可以开启连接失败后的重连机制:
代码:
template:
retry:
enabled: true #开启重试机制
initial-interval: 1000ms #初始等待时间
multiplier: 2 #设置
max-attempts: 3 #最大重试次数
配置之后就可以在连接不上时进行重试
注意:
- 当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。
- 如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
添加后测试结果:
可以看到他确实在连接不上时进行连接
2.生产者确定
RabbitMQ了Publisher Confirm和Publisher Return两种确认机制。开启确机制认后,在MQ成功收到消息后会发送认证消息给生产者。返回的结果有以下几种情况:
- 消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功。
- 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
- 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功。
- 其它情况都会返回NACK,告知投递失败
代码实现;
配置生成者的配置文件:
publisher-confirm-type: correlated #生成者确认机制,cocrrelated表示异步处理
publisher-returns: true #开启publisher-return机制,返回错误信息,一般不开启
这里publisher-confirm-type有三种模式可选:
- none:关闭confirm机制
- simple:同步阻塞等待MQ的回执消息
- correlated:MQ异步回调方式返回回执消息
建议开启的是异步回调,因为这样可以提高效率
- 生产者确认需要额外的网络和系统资源开销,尽量不要使用
- 如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
- 对于nack消息可以有限次数重试,依然失败则记录异常消息
一般处理的就是NACK的情况,进行消息重发,其他情况一般没关系
<2>MQ的可靠性
在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:
- 一旦MQ宕机,内存中的消息会丢失
- 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞
1.消息持久化
RabbitMQ实现数据持久化包括3个方面:.
- 交换机持久化
- 队列持久化
- 消息持久化
在创建交换机,队列和发送消息时都可以指定是否为持久化:
持久化的消息会将数据写入磁盘中,这样在MQ服务宕机重启后也可以存有数据
在spring中发送的消息默认是持久化的,我们也可以指定非持久化进行测试:
@Test
void testPageout(){
//手动创建消息
//指定字符集和发送模式:持久化或非持久化
Message msg= MessageBuilder.withBody("Hello".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT).build();
for (int i = 0; i < 1000000; i++) {
servicePublisher.pageoutPublishMessage("simple.queue",msg);
}
}
发送1000000条数据会导致mq内存空间占满,这时mq会将老的数据写入磁盘给新的消息腾出空间.这个操作被称为:Page out
但是这个操作会导致mq阻塞,降低处理速度,所以我们一般是使用持久化消息来实现mq不阻塞,增加处理速度
2.Lazy Queue
从RabbitMQ的3.6.0版本开始,就增加了Lazy Queue的概念,也就是惰性队列。惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
在3.12版本后,所有队列都是Lazy Queue模式,无法更改。
控制台指定Lazy Queue:
要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可:
例如在Configuration声明队列:
/**
* configuration配置lazy queue
* @return
*/
@Bean
public Queue lazyQueue(){
return QueueBuilder.durable("lazy.queue")
.lazy() //配置lazy queue
.build();
}
也可以通过注释声明监听队列:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "lazy.queue", durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")),

exchange = @Exchange(name = "lazy.exchange",durable = "true")
))
public void lazyConsumer(String message){
System.out.println("lazy消费者接收消息:{" + message + "}");
}
通过发送1000000条消息可以发现:
发出的消息直接存储到磁盘中(Page out),而没有存入内存,这样子的速度可以一直保持高速,增加效率
RabbitMQ怎么保持消息持久化:
- 首先通过配置可以让交换机、队列、以及发送的消息都持久化。这样队列中的消息会持久化到磁盘,MQ重启消息依然存在。
- RabbitMQ在3.6版本引入了LazyQueue,并且在3.12版本后会称为队列的默认模式。LazyQueue会将所有消息都持久化。
- 开启持久化和生产者确认时,RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执
<3>消费者可靠性
1.消费者确认
为了确认消费者是否成功处理消息,RabbitMQ提供了**消费者确认机制(**Consumer Acknowledgement)。当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
- ack:成功处理消息,RabbitMQ从队列中删除该消息
- nack:消息处理失败,RabbitMQ需要再次投递消息
- reiect:消息处理失败并拒绝该消息,RabbitMO从队列中删除该消息
SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:
- none:不处理(默认)。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
- manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
- auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack.当业务出现异常时,根据异常判断返回不同结果:
1.如果是业务异常,会自动返回nack
2.如果是消息处理或校验异常,自动返回reject
编写配置文件:
#配置消费者确认模式:auto
listener:
simple:
acknowledge-mode: auto
prefetch: 1
再编写消费者代码:
@RabbitListener(queues = "acknowledge.queue")
public void aknowledgeConsumer(String message){
System.out.println("acknowledge消费者接收到消息:{" + message + "}");
throw new RuntimeException("故意的");
}
故意抛出异常,模拟业务出错
之后开启测试:
@Test
void testAcknowledge(){
String msg="hello",queuename="acknowledge.queue";
servicePublisher.acknowledgePublishMessage(queuename,msg);
}
会发现:
消息的状态发生改变,unack
并且系统在不停的重试,重发消息
在停止服务后,消息再次变成Ready的状态
2.消费者重试
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。
我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:
listener:
simple:
retry:
enabled: true #开启重试机制
initial-interval: 1000ms #初始等待时间
multiplier: 1 #等待时间倍数
max-attempts: 3 #最大重试次数
stateless: true #当前消费者是否有状态,对于有事务的建议改为true,记录状态
失败消息处理策略:
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
下面演示第三种方案的图示:
可以将错误信息传入到一个专门处理异常信息的队列中,以后人工介入
如果想要实现第三中方案,那么就需要配置接收错误信息的交换机和错误信息的队列:
@Configuration
public class ErrorConfiguration {
//创建交换机
@Bean
public DirectExchange errorExchange(){
return new DirectExchange("error.exchange");
}
//创建队列
@Bean
public Queue errorQueue(){
return new Queue("error.queue");
}
//创建连接
@Bean
public Binding errorBinding(){
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with("error");
}
//指定错误消息交换机
@Bean
public MessageRecoverer republishMessageRecover(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.exchange","error");
}
}
在如上代码中,指定了错误交换机和队列,使用MessageRecoverer的Bean对象来连接这个错误交换机
测试结果:
acknowledge消费者接收到消息:{hello}
acknowledge消费者接收到消息:{hello}
acknowledge消费者接收到消息:{hello}
2023-10-21T15:42:01.844+08:00 WARN 37756 --- [ntContainer#7-1] o.s.a.r.retry.RepublishMessageRecoverer : Republishing failed message to exchange 'error.exchange' with routing key error
在三次重试消息使用完后,消息会被自动发送到交换机上
消费者如何保证消息一定被消费?
- 开启消费者确认机制为auto,由spring确认消息处理成功后返回ack,异常时返回nack
- 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
3.业务幂等性
幂等是一个数学概念,用函数表达式来描述是这样的:f(x)=f(f(x))。在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。
例如:
幂等:
- 查询业务,例如根据id查询商品
- 删除业务,例如根据id删除商品
非幂等:
- 用户下单业务,需要扣减库存用户
- 退款业务,需要恢复余额
方案一,是给每个消息都设置一个唯一id
利用id区分是否是重复消息:每一条消息都生成一个唯一的id,与消息一起投递给消费者。消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
配置生成id可以通过配置消息转化器实现:
//配置消息转化器
@Configuration
public class MessageConfiguration {
//引入消息转化器
@Bean
public MessageConverter JsonMessageConverter() {
Jackson2JsonMessageConverter jjc=new Jackson2JsonMessageConverter();
jjc.setCreateMessageIds(true);
return jjc;
}
}
在源码中默认以UUID的形式生成id
方案二,是结合业务逻辑,基于业务本身做判断。
以我们的业务为例:
我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理:
4.补救措施
在上述可靠性措施都没有生效时,就需要一些兜底措施:
1.即上述介绍的消费者重试策略中的RepublishMessageRecoverer
,将错误信息传到一个错误消息队列中进行人工介入
2.配置定时任务,例如在订单消息未更新成功时,配置定时任务主动查看是否需要更新。
配置定时任务可以看这篇文章:Spring定时任务的几种实现
7.延迟消息
延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。
延迟任务:设置在一定时间之后才执行的任务
那么该怎么实现延迟消息呢?
<1>死信交换机
当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter ) :
-
消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
-
消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
-
要投递的队列消息堆积满了,最早的消息可能成为死信
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机 ( Dead Letter Exchange,简称DLX)。
那么从结果上来看,就是发起者在30秒前发送了一个消息,消费者30秒后才接收到了消息
所以这个方法就可以用来实现延迟消息
在代码中只需要声明队列和交换机,指定死信交换机,连接上图的各个连接(注意:死信交换机和队列也要进行连接)
和消息的过期时间就可以实现延时消息.
但是显然,这个设置方法流程复杂,并且需要声明多个队列和交换机,一般不采用这个方案
<2>延时插件
该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列
可以通过配置类声明:
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder.directExchange("delay.exchange")
//配置延迟属性为true
.delayed()
.durable(true).build();
}
也可以通过注解指定:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue",durable = "true"),
//配置延迟属性
exchange=@Exchange(name = "delay.exchange",delayed = "true"),
key = {"delay"}
))
public void DelayConsumer(){
log.info("延迟消息已接受");
}
在指定完成后,发送消息就用的是setDelay()函数来指定延迟时间:
public void delayPublishMessage(String exchange,String routeKey,String msg){
rabbitTemplate.convertAndSend(exchange, routeKey, msg, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置延迟时间为10秒
message.getMessageProperties().setDelay(10);
return message;
}
});
}