地址:http://localhost:15672/#/queues
默认账号密码:guest guest
Overview模块可以看到监听端口信息和访问web的端口
Exchanges模块可以看到配置的交换机
Queques and Streams模块可以看到配置的消息队列
Admin模块可以看到用户信息 添加修改用户
引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
配置文件
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
# 虚拟主机 虚拟host 可以不设置,使用server默认host
virtual-host: /
username: guest
password: guest
1.创建DirectRabbitConfig类 创建消息队列和交换机最后将两者绑定
package com.cn.controller;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author : JCccc
* @CreateTime : 2019/9/3
* @Description :
**/
@Configuration
public class DirectRabbitConfig {
//队列 起名:TestDirectQueue
@Bean
public Queue TestDirectQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
//一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue("TestDirectQueue",true);
}
//Direct交换机 起名:TestDirectExchange
@Bean
DirectExchange TestDirectExchange() {
// return new DirectExchange("TestDirectExchange",true,true);
return new DirectExchange("TestDirectExchange",true,false);
}
//绑定 将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
}
@Bean
DirectExchange lonelyDirectExchange() {
return new DirectExchange("lonelyDirectExchange");
}
}
2.创建生产者
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
// 交换机,绑定键,消息
rabbitTemplate.convertAndSend("TestDirectExchange","TestDirectRouting",mess+"111");
return "成功";
}
3.创建消费者
//监听队列
@RabbitListener(queues = "TestDirectQueue")
public void getque(String message){
System.out.println("getque:"+message);
}
1.在管理后台创建一个队列 设置名称:testqueque
2.编写生产者测试类SpringAmqpTest,并利用 RabbitTemplate 实现消息发送
@RestController
public class testController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
rabbitTemplate.convertAndSend( "testqueque",mess);
return "成功";
}
}
3.编写消费者,监听队列消息
@RabbitListener(queues = "testqueque")
public void getque2(String message){
System.out.println("getque2:"+message);
}
1生产者
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
String queueName = "work.queue";
String message = "Hello SpringAQMP-";
for (int i = 0; i < 20; i++) {
template.convertAndSend(queueName,message+i);
}
}
消费者
@RabbitListener(queues = "work.queue")
public void workQueueListener01(String message) throws InterruptedException {
System.out.println("消费者01接收到消息:" + message + " - " + LocalTime.now());
Thread.sleep(20);
}
@SneakyThrows
@RabbitListener(queues = "work.queue")
public void workQueueListener02(String message){
System.out.println("消费者02接收到消息:" + message + " - " + LocalTime.now());
正常情况下每个消费者要消费的消息数量是一样的。消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然不合理,因此修改消费者的配置文件使得每个消费者按照能力顺序处理信息
spring:
rabbitmq:
listener:
simple: # 简单队列和work队列的配置
prefetch: 1 # 每次只能获取一条消息,处理完成才获取下一条消息
在发布订阅模型中 有四个角色
▪**producer:**生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
▪**exchange:**交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于exchange的类型。exchange 有以下3种类型:
▫fanout:广播,将消息交给所有绑定到交换机的队列
▫direct: 定向,把消息交给符合指定 routing key 的队列
▫topic: 通配符,把消息交给符合 routing pattern 的队列
▪**consumer:**消费者,订阅队列
▪**queue:**消息队列,接收消息、缓存消息。
(一条消息发送到交换机后所有与该交换机绑定的队列都可以监听到)
在广播模式下,消息发送流程是这样的:
▫ 可以有多个消费者
▫ 每个消费者有自己的queue(队列)
▫ 每个队列都要绑定到Exchange(交换机)
▫ 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
▫ 交换机把消息发送给绑定过的所有队列
▫ 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
▫ 使用内置的 amq.fanout 交换机
▫ 两个队列不用手动创建
1创建队列fanout.queue01、fanout.queue02 与默认交换机amq.fanout 绑定
或者与创建的FanoutExchange交换机绑定
1生产者
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
rabbitTemplate.convertAndSend("amq.fanout", "", "Hello amq.fanout");
}
2消费者
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue01"),
exchange = @Exchange(name = "amq.fanout", type = ExchangeTypes.FANOUT)
))
public void fanoutQueueListener01(String message) {
System.out.println("消费者01接收到fanout.queue01的消息:" + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue02"),//使用@Queue(name = "fanout.queue01")RabbitMQ自动生成队列
exchange = @Exchange(name = "amq.fanout", type = ExchangeTypes.FANOUT)
))
public void fanoutQueueListener02(String message) {
System.out.println("消费者02接收到fanout.queue02的消息:" + message);
}
}
默认amq.direct交换机 或者创建DirectExchange交换机
在 Fanout 模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到 direct 类型的 exchange。
Direct模型下:
▫ 队列与交换机的绑定要指定一个 RoutingKey
▫ 消息的发送方在向 exchange 发送消息时,也必须指定消息的 RoutingKey。
▫ exchange 不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息
1生产者
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
rabbitTemplate.convertAndSend("amq.direct", "save", "新增通知");
rabbitTemplate.convertAndSend("amq.direct", "delete", "删除通知");
}
2消费者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue01"),
exchange = @Exchange(name = "amq.direct", type = ExchangeTypes.DIRECT),
key = {"save"}
))
// 处理新增的业务
public void directQueueListener01(String message){
System.out.println("消费者01接收到direct.queue01的消息:" + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue02"),
exchange = @Exchange(name = "amq.direct", type = ExchangeTypes.DIRECT),
key = {"delete"}
))
// 处理删除的业务
public void directQueueListener02(String message){
System.out.println("消费者02接收到direct.queue02的消息:" + message);
}
默认amq.topic 或者创建TopicExchange交换机
Topics 类型的 Exchange 与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。
不同的是 Topic类型的 Exchange 可以让队列在绑定 Routingkey 的时候使用通配符
通配符规则:
#:匹配一个或多个单词
*:匹配一个单词
举例:
user.#:能够匹配 user.add 或者 user.detail.add
user.*:只能匹配 user.add
生产者
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
rabbitTemplate.convertAndSend("amq.topic", "user.add", "新增用户通知");
rabbitTemplate.convertAndSend("amq.topic", "user.update", "更新户通知");
rabbitTemplate.convertAndSend("amq.topic", "dept.add", "新增部门通知");
rabbitTemplate.convertAndSend("amq.topic", "dept.update", "更新部门通知");
}
消费者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue01"),
exchange = @Exchange(name = "amq.topic", type = ExchangeTypes.TOPIC),
key = "user.*"
))
public void topicQueueListener01(String message){
System.out.println("消费者01接收到topic.queue01的消息:" + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue02"),
exchange = @Exchange(name = "amq.topic", type = ExchangeTypes.TOPIC),
key = "dept.*"
))
public void topicQueueListener02(String message){
System.out.println("消费者02接收到topic.queue02的消息:" + message);
}
有时由于网络波动,可能出现客户端连接mq失败的情况,通过配置可以开始连接失败的重连机制
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
包括生产者发送消息成功,消费者接收消息成功
server:
port: 8021
spring:
#给项目来个名字
application:
name: rabbitmq-provider
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: root
password: root
#虚拟host 可以不设置,使用server默认host
virtual-host: JCcccHost
#确认消息已发送到交换机(Exchange)
publisher-confirms: true
#publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
如果在配置确认回调,测试发现无法触发回调函数,那么存在原因也许是因为版本导致的配置项不起效,可以把publisher-confirms: true替换为 publisher-confirm-type: correlated
创建RabbitTemplateConfig
package com.cn.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class RabbitTemplateConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(true);
template.setMessageConverter(new Jackson2JsonMessageConverter());
template.setEncoding("utf-8");
//实现消息发送到exchange后接收ack回调,publisher-confirms:true
//如果队列是可持久化的,则在消息成功持久化之后生产者收到确认消息
template.setConfirmCallback(((correlationData, ack, cause) -> {
if(ack) {
log.info("消息成功发送到exchange,id:{}", correlationData.getId());
} else {
/*
* 消息未被投放到对应的消费者队列,可能的原因:
* 1)发送时在未找到exchange,例如exchange参数书写错误
* 2)消息队列已达最大长度限制(声明队列时可配置队列的最大限制),此时
* 返回的cause为null。
*/
log.info("******************************************************");
log.info("11消息发送失败: {}", cause);
}
}));
//消息发送失败返回队列,publisher-returns:true
template.setMandatory(true);
//实现消息发送的exchange,但没有相应的队列于交换机绑定时的回调
template.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String id = message.getMessageProperties().getCorrelationId();
log.info("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", id, replyCode, replyText, exchange, routingKey);
});
return template;
}
}
发送请求时增加一个uuid
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("amq.fanout",null,mess+"111",correlationData);
return "成功";
}
ConfirmCallback
为发送Exchange
(交换器)时回调,成功或者失败都会触发;ReturnCallback
为路由不到队列时触发,成功则不触发;RabbitMQ提供了Publisher Confirm和Publisher Return两种确认机制。开启确机制认后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:
生产者确认需要额外的网络和系统资源开销,尽量不要使用。
如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题。
对于nack消息可以有限次数重试,依然失败则记录异常消息。
在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:
1、一旦MQ宕机,内存中的消息会丢失。
2、内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞。
RabbitMQ实现数据持久化包括3个方面:
交换机持久化(Durable属性,Spring默认设置为Durable);
队列持久化(Durable属性,Spring默认设置为Durable);
消息持久化(发送消息时设置delivery_mode=2persisent,Spring发送的消息默认是持久化的)
In memory | Persistent | Transient, Paged Out |
---|---|---|
内存 | 持久 |
从RabbitMQ的3.6.0版本开始,就增加了Lazy Queue的概念,也就是惰性队列。
惰性队列的特征如下:
1、接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)。
2、消费者要消费消息时才会从磁盘中读取并加载到内存。
3、支持数百万条的消息存储。
4、在3.12版本后,所有队列都是Lazy Queue模式,无法更改 。
(3.12之前)需要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可:
//声明Bean
@Bean
public Queue lazyQueue(){
return QueueBuilder.durable("lazy.queue").lazy().build(); //创建惰性队列
}
//基于注解创建
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode",value = "lazy")
))
public void listenLazyQueue(String msg){
log.info("接收到lazy.queue的消息:{}",msg);
}
生产者发送非持久化消息直接存入Paged Out的消息
@GetMapping("addQue")
public String addQue(@Param("mess") String mess ){
Message message = MessageBuilder.withBody(mess.getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT).build();
for (int i = 0; i < 1000000; i++) {
rabbitTemplate.convertAndSend("test","11",message );
}
return "成功";
}
当消费者处理消息结束后,影响Rabbitmq发送一个回执,告知自己消息处理状态。回执有三种可选值:
SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:
none:不处理。即消息投递给消费者后立刻ack,消息会立刻从mq删除。非常不安全,不建议使用
manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务侵入,但灵活
auto:自动模式。springAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回
ack。当业务出现异常,根据异常判断返回不同的结果:
▫ 如果是业务异常,会自动返回nack
▫ 如果是消息处理或校验异常,会自动返回reject
第一步、开启消费者确认其机制
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #每次只能获取一条消息,处理完才能获取下一条消息
acknowledge-mode: auto #none:关闭ack;manual:手动ack;auto:自动ack
第二步、消费者业务模拟异常
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "twst"),
exchange = @Exchange(name = "test", type = ExchangeTypes.DIRECT)))
public void gettwst(Message message){
System.out.println("twst:"+new String(message.getBody()));
throw new RuntimeException("yihcang");
}
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue无限循环,导致mg的消息处理飙升,带来不必要的压力。
我们可以利用spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到MQ队列
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #每次只能获取一条消息,处理完才能获取下一条消息
acknowledge-mode: auto #none:关闭ack;manual:手动ack;auto:自动ack
retry:
enabled: true #开启消费者失败重试
initial-interval: 1000ms #初始的失败等待时长为1秒
multiplier: 1 #下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 #最大重试次数
stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false
以抛出异常做测试
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "twst"),
exchange = @Exchange(name = "test", type = ExchangeTypes.DIRECT)))
public void gettwst(Message message){
System.out.println("twst:"+new String(message.getBody()));
throw new RuntimeException("yihcang");
}
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:
第三中方式更为可靠
(1)首先,定义接收失败消息的交换机、队列及其绑定关系。
(2)然后,定义RepublishMessageRecoverer。
package com.cn.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DirectRabbitConfig {
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name = "enabled",havingValue = "true") //开启了消费者失败重试机制才会生效
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(Queue errorQueue,DirectExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with("errorKey");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
System.out.println("加载RepublishMessageRecoverer");
return new RepublishMessageRecoverer(rabbitTemplate,"error.Exchange","errorKey");
}
}
}
消费者如何保证消息一定被消费?
幂等是一个数学概念,用函数表达式来描述是这样的: f(x)= f(f(X)。在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。
方案一、是给每个消息都设置一个唯一id,利用id区分是否是重复消息
@Bean
public MessageConverter jacksonMessageConvertor() {
//1.定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
//2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}
方案二、是结合业务逻辑,基于业务本身做判断。以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理。
方案三、使用Token令牌,生成一个token存储在redis中,请求的时候携带这个token一起请求(Token 最好将其放到 Headers 中),后端需要对这个Token作为 Key在redis中进行校验,如果 Key存在就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 就返回重复执行的错误信息,这样来保证幂等操作。
当一个队列中的消息满足下列情况之一时,就会成为死信:
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机被称为死信交换机
队列指定死信交换机后发送一个延迟消息,经过一定时间后死信交换机会接收到消息。
RabbitMQ官方推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后在投递到队列。
需要声明delayed属性