每天进步一点点---------RabbitMq

题记

看了很多网上教程,对RabbitMq有了一个初步的了解,但是仍然没有想明白,在生产环境中,如何使用才会稳定、可靠、安全,因此记录从零-1的学习过程,在学习过程中练习和熟悉RabbitMq。
资料
官网文档
黑马Java就业班2.1
【学相伴】RabbitMQ最新完整教程IDEA版通俗易懂 | KuangStudy | 狂神说 | 学相伴飞哥 推荐

一、RabbitMq概念熟悉

网上很多教程都是SpringBoot集成RabbitMq,对于我这种连Mq本身都不了解的人来说已经跳过了一部分,所以先使用RabbitMq本身尝试一下Mq的概念,然后再通过SpringBoot集成RabbitMq,对于可靠的rabbitMq集群,后序练习,熟悉概念的过程是通过RabbitMq的web页面直接去查看的。

1. 页面头信息
可以很清晰的看到,头部包含RabbitMq的版本、ErLang语言的版本、上次数据刷新的时间、virtualHost、当前登录用户、集群名称,下面重点看一下集群名称和virtualHost。
在这里插入图片描述

**集群名称**:单机部署的RabbitMq也会有集群名称,为节点的名称,本案例为docker启动,设置的*hostname=my-rabbit*,因此此处显示的是集群名称,点击集群名称,也是可以修改集群名称的。

每天进步一点点---------RabbitMq_第1张图片
virtualHost:虚拟主机,和虚拟机类似,每个virtualHost 是相互隔离的 exchange 、message、queue 互不相通,因此可以认为一个virtualHost为一个rabbitMq实例,虚拟主机可以通过下图添加,Tags标签的作用暂时还未了解,如果哪位大神熟悉,还请帮忙留言指导。
每天进步一点点---------RabbitMq_第2张图片
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的权限。

每天进步一点点---------RabbitMq_第3张图片

2. Limits
和VirtualHost、User一起的功能还包含Feature Flags、policy、Limits,Feature Flags和policy还未看到,不太懂,先跳过,Limits就很明显了,和Linux的ulimit类似,可以控制队列和连接的最大值。
每天进步一点点---------RabbitMq_第4张图片

3. Connection、Channel 、queues

这三个概念是比较好理解的,找一个网上大神的图,可以很清楚的说明,生产消息(生产者)连接RabbitMq,为一个Connection可以包含多个Channel,RabbitMq通过exchange(交换机)对消息进行路由到Queue(队列)。
每天进步一点点---------RabbitMq_第5张图片
每天进步一点点---------RabbitMq_第6张图片
参考黑马部分代码如下,可以明确看到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(); 

二、RabbitMq主要消息模式

实践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为队列名称即可。
在这里插入图片描述
每天进步一点点---------RabbitMq_第7张图片
(1)手动验证发送
选择virtualHost为mall,然后添加queue,在exchanges中选择defaultExchange,publish消息的时候添加routekey为queue名称即可。
每天进步一点点---------RabbitMq_第8张图片
(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模式和简单模式并没有太大的区别,只是增加了一个消费端,我们直接消费端复制一份或者多起一个实例来尝试一下,两个消费端默认的执行方式是轮询模式。
每天进步一点点---------RabbitMq_第9张图片
消费端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 模式
发布订阅模式和设计模式的观察者实现的功能类似,需要定义一个交换机,多个队列,不同的消费者消费不同的队列消息即可。
每天进步一点点---------RabbitMq_第10张图片
(1)手动尝试
添加fanoutExchange,然后修改bind即可,注意routekey为空。
每天进步一点点---------RabbitMq_第11张图片
查看queue的绑定交换机会显示交换机名称,不会显示类似,看着像未绑定成功,其实绑定成功了。
每天进步一点点---------RabbitMq_第12张图片
(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指定消费的队列,直接改代码中的指定队列即可,运行后可以看到两者打印一直。
每天进步一点点---------RabbitMq_第13张图片
4. Routing 路由模式
路由模式是交换机指定路由,匹配方法为全匹配,创建完成后,查看一下绑定关系如下:
每天进步一点点---------RabbitMq_第14张图片
每天进步一点点---------RabbitMq_第15张图片
每天进步一点点---------RabbitMq_第16张图片

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");
    }

}

消费者正常绑定到队列消费即可,按照年龄奇数和偶数的方式增加到了两个队列,消费的时候可以明显看到已分开。
每天进步一点点---------RabbitMq_第17张图片
5. Topics 模式
每天进步一点点---------RabbitMq_第18张图片
主体模式其实就是在路由模式的基础上,支持了对key的通配符匹配(星号以及井号),以满足更加复杂的消息分发场景。
“#” : 匹配一个或者多个
“*”:匹配一个
只是key不同,不做尝试

三、细节技术点

1. 消息可靠性
消息可靠性即MQ接收到消息之后的应答和消费端消费后的应答,

  • Confirm确认模式:从Producer到exchange,则会返回一个confirmACK
  • return回退模式:从exchange到消费端失败则会返回一个returnback
    生产端消息确认机制:
@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. 死信队列
死信队列即无法处理的消息重新找一个交换机发送到一个队列,可以右专门的消费端处理,出现的原因包含下面几种情况:

  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度

在上面的基础上构造死信队列也很简单,加一个死信交换机并且绑定队列和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();

每天进步一点点---------RabbitMq_第19张图片
6. 消息追踪
开启后性能会下降,但是很好用
7. 消息补偿

8. 幂等性保障

应用案例

案例以后遇到的时候再维护

你可能感兴趣的:(畅购商城,rabbitmq,java,分布式)