看了很多网上教程,对RabbitMq有了一个初步的了解,但是仍然没有想明白,在生产环境中,如何使用才会稳定、可靠、安全,因此记录从零-1的学习过程,在学习过程中练习和熟悉RabbitMq。
资料:
官网文档
黑马Java就业班2.1
【学相伴】RabbitMQ最新完整教程IDEA版通俗易懂 | KuangStudy | 狂神说 | 学相伴飞哥 推荐
网上很多教程都是SpringBoot集成RabbitMq,对于我这种连Mq本身都不了解的人来说已经跳过了一部分,所以先使用RabbitMq本身尝试一下Mq的概念,然后再通过SpringBoot集成RabbitMq,对于可靠的rabbitMq集群,后序练习,熟悉概念的过程是通过RabbitMq的web页面直接去查看的。
1. 页面头信息
可以很清晰的看到,头部包含RabbitMq的版本、ErLang语言的版本、上次数据刷新的时间、virtualHost、当前登录用户、集群名称,下面重点看一下集群名称和virtualHost。
**集群名称**:单机部署的RabbitMq也会有集群名称,为节点的名称,本案例为docker启动,设置的*hostname=my-rabbit*,因此此处显示的是集群名称,点击集群名称,也是可以修改集群名称的。
virtualHost:虚拟主机,和虚拟机类似,每个virtualHost 是相互隔离的 exchange 、message、queue 互不相通,因此可以认为一个virtualHost为一个rabbitMq实例,虚拟主机可以通过下图添加,Tags标签的作用暂时还未了解,如果哪位大神熟悉,还请帮忙留言指导。
user:可以自己控制用户,控制权限,包括对哪个virtualhost进行权限控制、对topic交换机进行权限控制。权限的设置类型如下
Configure regexp:对queue或exchange新建和配置的权限。
Write regexp:对一个queue或exchange写消息的权限。
Read regexp:对一个queue或exchange读消息的权限。
.* 表示对所有queue和exchange有此权限。
^$ 表示对所有的queue和exchage没有此权限。
^(hello.*)$ 表示只有以hello开头的queue或exchage的权限
^(hello.*|team.*)$ 表示有以hello和team开头的queue或exchange的权限。
2. Limits
和VirtualHost、User一起的功能还包含Feature Flags、policy、Limits,Feature Flags和policy还未看到,不太懂,先跳过,Limits就很明显了,和Linux的ulimit类似,可以控制队列和连接的最大值。
3. Connection、Channel 、queues
这三个概念是比较好理解的,找一个网上大神的图,可以很清楚的说明,生产消息(生产者)连接RabbitMq,为一个Connection可以包含多个Channel,RabbitMq通过exchange(交换机)对消息进行路由到Queue(队列)。
参考黑马部分代码如下,可以明确看到ConnectionFactory获取Connection,Connection获取channel。
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.8.102");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
实践6种消息模式之前,先搭建集成RabbitMq的SpringBoot工程,方便验证。
(1)引入下面的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
(2)在SpringBoot工程中创建exchange和queue等有两种方式,一种是注解方式,一种是xml方式,注解方式更加简便,此处使用注解形式,application.yml配置如下:
spring:
application:
name: provider
rabbitmq:
username: guest
password: guest
host: 192.168.8.102
port: 5672
virtual-host: mall
注:对于virtualHost设置的名称问题,是没有必要设置/mall的,具体可以参考大神的博客《rabbitmq 虚拟主机vhost名称设置问题》,virtualhost是需要提前创建好的,否则会报错。
(3)定义一个User类作为要发送的对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String userName;
private String sex;
private String lover;
private Integer age;
private Integer height;
}
(4)创建MqConfig,即注解方式生成交换机,队列等
@Configuration
public class HelloMqConfig {
@Value("${rabbitconfig.helloqueue.name}")
String queueName;
/**
* 定制JSON格式的消息转换器
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
//1、定义一个消息队列
@Bean
public Queue direct_queue() {
return QueueBuilder.durable(queueName).build();
}
}
注:测试时发现,当发送消息时,才会真正的创建,不发送消息不会创建。
1. HelloWorld模式
helloworld模式没有指定交换机,其实使用的是默认交换机,在创建后的界面也可以很明确的看到,默认交换机其实就是一个direct交换机,routekey为队列名称即可。
(1)手动验证发送
选择virtualHost为mall,然后添加queue,在exchanges中选择defaultExchange,publish消息的时候添加routekey为queue名称即可。
(2)注解代码
发送端代码为:
@Configuration
public class HelloMqConfig {
@Value("${rabbitconfig.helloqueue.name}")
String queueName;
/**
* 定制JSON格式的消息转换器
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
//1、定义一个消息队列
@Bean
public Queue direct_queue() {
return QueueBuilder.durable(queueName).build();
}
}
发送的代码为:
rabbitTemplate.convertAndSend("hello", new User("hhh", "male", "zzz", 18, 20));
消费端代码为:
只要消费端一直执行着,就能消费掉消息
@Component
public class HelloListener {
@RabbitListener(queues = "hello")
public void reviceMessage(String message) {
System.out.println("接收了消息" + message);
}
}
2. WorkQueue模式
workQueue模式和简单模式并没有太大的区别,只是增加了一个消费端,我们直接消费端复制一份或者多起一个实例来尝试一下,两个消费端默认的执行方式是轮询模式。
消费端1的回显为:
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":20,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":22,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":24,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":26,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":28,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":30,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":32,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":34,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":36,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":38,"height":20}
消费端2的回显为:
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":21,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":23,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":25,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":27,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":29,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":31,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":33,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":35,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":37,"height":20}
接收了消息{"userName":"hhh","sex":"male","lover":"zzz","age":39,"height":20}
参考大神博客《Springboot中RabbitMQ公平模式》,即使增加了不同的延时,仍然消费如上,处理能力强的消费端并没有处理更多的消息,可以在消费端增加下面的配置,改变为公平模式。
rabbitmq:
username: guest
password: guest
host: 192.168.8.102
port: 5672
virtual-host: mall
listener:
下面的两个配置都需要有
simple:
prefetch: 0
acknowledge-mode: manual
此时需要将消费端修改为手动确认,Listener修改如下
@RabbitListener(queues = "hello")
public void reviceMessage(Message message, Channel channel) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收了消息" + new String(message.getBody()));
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException e) {
e.printStackTrace();
}
}
3. Publish/subscribe 模式
发布订阅模式和设计模式的观察者实现的功能类似,需要定义一个交换机,多个队列,不同的消费者消费不同的队列消息即可。
(1)手动尝试
添加fanoutExchange,然后修改bind即可,注意routekey为空。
查看queue的绑定交换机会显示交换机名称,不会显示类似,看着像未绑定成功,其实绑定成功了。
(2)代码实现
provider实现队列创建:
@Configuration
public class FanOutMqConfig {
@Value("${rabbitconfig.fanoutqueue.name}")
String queueName;
/**
* 定制JSON格式的消息转换器
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public FanoutExchange fanoutExchange() {
return ExchangeBuilder.fanoutExchange(queueName).durable(true).build();
}
//1、定义一个消息队列
@Bean
public Queue fanoutQueue1() {
return QueueBuilder.durable(queueName + "1").build();
}
//1、定义一个消息队列
@Bean
public Queue fanoutQueue2() {
return QueueBuilder.durable(queueName + "2").build();
}
@Bean
public Binding fanoutQueue1Bind() {
return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
}
@Bean
public Binding fanoutQueue2Bind() {
return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
}
}
Consumer指定消费的队列,直接改代码中的指定队列即可,运行后可以看到两者打印一直。
4. Routing 路由模式
路由模式是交换机指定路由,匹配方法为全匹配,创建完成后,查看一下绑定关系如下:
Provider代码:
路由模式和发布订阅模式交换机类型改变,绑定时,增加路由key即可。
@Configuration
public class RouteMqConfig {
String queueName = "directQueue";
@Bean
public DirectExchange routeExchange() {
return ExchangeBuilder.directExchange(queueName).durable(true).build();
}
//1、定义一个消息队列
@Bean
public Queue directQueue1() {
return QueueBuilder.durable(queueName + "1").build();
}
//1、定义一个消息队列
@Bean
public Queue directQueue2() {
return QueueBuilder.durable(queueName + "2").build();
}
@Bean
public Binding queue1Binding(){
return BindingBuilder.bind(directQueue1()).to(routeExchange()).with("info");
}
@Bean
public Binding queue2Binding(){
return BindingBuilder.bind(directQueue2()).to(routeExchange()).with("error");
}
}
消费者正常绑定到队列消费即可,按照年龄奇数和偶数的方式增加到了两个队列,消费的时候可以明显看到已分开。
5. Topics 模式
主体模式其实就是在路由模式的基础上,支持了对key的通配符匹配(星号以及井号),以满足更加复杂的消息分发场景。
“#” : 匹配一个或者多个
“*”:匹配一个
只是key不同,不做尝试
1. 消息可靠性
消息可靠性即MQ接收到消息之后的应答和消费端消费后的应答,
@Configuration
@Slf4j
public class RouteMqConfig {
String queueName = "directQueue";
@Bean
public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String correlationId = message.getMessageProperties().getCorrelationId();
log.debug("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", correlationId, replyCode, replyText, exchange, routingKey);
});
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("消息发送到exchange成功,id: {}", correlationData.getId());
} else {
log.debug("消息发送到exchange失败,原因: {}", cause);
}
});
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
return rabbitTemplate;
}
@Bean
public DirectExchange routeExchange() {
return ExchangeBuilder.directExchange(queueName).durable(true).build();
}
@Bean
public DirectExchange deadExchange() {
return ExchangeBuilder.directExchange("deadexchange").durable(true).build();
}
//1、定义一个消息队列
@Bean
public Queue directQueue1() {
return QueueBuilder.durable(queueName + "1").build();
}
//1、定义一个消息队列
@Bean
public Queue deadQueue1() {
return QueueBuilder.durable("deadQueue").build();
}
//1、定义一个消息队列
@Bean
public Queue directQueue2() {
return QueueBuilder.durable(queueName + "2").deadLetterExchange("deadexchange").deadLetterRoutingKey("dead").ttl(5000).build();
}
@Bean
public Binding queue1Binding() {
return BindingBuilder.bind(directQueue1()).to(routeExchange()).with("info");
}
@Bean
public Binding queue2Binding() {
return BindingBuilder.bind(directQueue2()).to(routeExchange()).with("error");
}
@Bean
public Binding deadQueueBinding() {
return BindingBuilder.bind(deadQueue1()).to(deadExchange()).with("dead");
}
}
yml增加下面的两个参数
publisher-confirm-type: correlated
publisher-returns: true
消费端确认方式:
yml配置
手动应答,应答ACK时消息会背消费掉,应答Nack时,消息会进入死信队列
listener:
simple:
prefetch: 5
acknowledge-mode: manual
@Component
public class HelloListener {
@RabbitListener(queues = "directQueue1")
public void reviceMessage(Message message, Channel channel) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
try {
// 异常时,应答Nack,不重发,否则会使得上面的重试失效,造成死循环
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
System.out.println("接收了消息" + new String(message.getBody()));
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 延迟队列
是通过死信队列实现的,设置TTL,如果30分钟还未被消费,则进入死信队列。
3. 死信队列
死信队列即无法处理的消息重新找一个交换机发送到一个队列,可以右专门的消费端处理,出现的原因包含下面几种情况:
在上面的基础上构造死信队列也很简单,加一个死信交换机并且绑定队列和routekey,在队列设置私信队列“x-dead-letter-exchange”和“x-dead-letter-routing-key”的时候设置成死信队列即可。
@Configuration
public class RouteMqConfig {
String queueName = "directQueue";
@Bean
public DirectExchange routeExchange() {
return ExchangeBuilder.directExchange(queueName).durable(true).build();
}
@Bean
public DirectExchange deadExchange() {
return ExchangeBuilder.directExchange("deadexchange").durable(true).build();
}
//1、定义一个消息队列
@Bean
public Queue directQueue1() {
return QueueBuilder.durable(queueName + "1").build();
}
//1、定义一个消息队列
@Bean
public Queue deadQueue1() {
return QueueBuilder.durable("deadQueue").build();
}
//1、定义一个消息队列
@Bean
public Queue directQueue2() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
args.put("x-dead-letter-exchange","deadexchange");
args.put("x-dead-letter-routing-key","dead");
return QueueBuilder.durable(queueName + "2").withArguments(args).build();
}
@Bean
public Binding queue1Binding() {
return BindingBuilder.bind(directQueue1()).to(routeExchange()).with("info");
}
@Bean
public Binding queue2Binding() {
return BindingBuilder.bind(directQueue2()).to(routeExchange()).with("error");
}
@Bean
public Binding deadQueueBinding() {
return BindingBuilder.bind(deadQueue1()).to(deadExchange()).with("dead");
}
}
4. 消费端限流
消费端限流即消费端修改成手动确认,然后设置每次处理5个情况
listener:
simple:
prefetch: 5
acknowledge-mode: manual
5. TTL
有两种方式,一种是消息添加TTL,一种是队列添加TTL,单独消息的未想明白什么场景使用,练习队列添加TTL,
添加队列的时候,添加一个args即可,单位ms,超过TTL时间的消息会自动删除,和死信队列一起使用,防止消息丢失
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
return QueueBuilder.durable(queueName + "2").withArguments(args).build();
6. 消息追踪
开启后性能会下降,但是很好用
7. 消息补偿
8. 幂等性保障
案例以后遇到的时候再维护