SpringCloud 之 RabbitMQ(二)

119 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

前情回顾:juejin.cn/post/715319… 上次我们讲解了rabbitmq的基本理论和要学习的集中模型,这次我们来学学这写模型及其应用场景

基本消息队列模型--HelloWorld案例

最简单的案例入手:

image-20221011150341553.png

  • Publisher:消息发布者
  • Queue:消息队列,负责接收并且缓存消息
  • Consumer:订阅队列,处理队列中的信息

和上面说的一样,消息提供者发布消息,到队列中,服务消费者从队列中拿去,慢慢消化

我们创建一个多模块项目

image-20221011150918346.png

父模块的pom的parent是springboot,我们不用再引入springboot依赖

image-20221011151042537.png

引入一个lombok,单元测试,AMQP的依赖

image-20221011194703175

在publisher模块里,我们写了一个test类

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("你的主机名");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("你的账户");
        factory.setPassword("你的密码");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
​
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
​
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
​
        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");
​
        // 5.关闭通道和连接
        channel.close();
        connection.close();
​
    }
}

第一步,建立连接,填上你的账户密码和你对应的虚拟主机

第二步,创建通道

第三步,创建队列名称

第四步,发送消息,往你的队列中发

第五步,关闭通道和连接

image-20221012123254669

image-20221012123301567

image-20221012123308649

我们在对应的可视化界面也都可以看到对应代码的作用结果

同时,我们也可以看到发送的消息

image-20221012123424325

当我们执行完第五步,都断开链接了,消息却还在,说明它已经完成了消息发送的功能却没有

下面是接收消息的消费者

public class ConsumerTest {
​
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("主机名);
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("账户");
        factory.setPassword("密码");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
​
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
​
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
​
        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}
​

前面几步和消息发送的代码类似,我们不做赘述

到订阅消息这一步的时候,用到了回调函数的思想,我们将这个回调函数的逻辑绑定到队列上,但是此时消息还没有过来,因此我们会先执行等待接收消息。。。然后等rabbitmq把消息传递过来了,再去执行回调函数,也就是接收到消息【】这时候正是说明了这种异步的机制

在执行完我们发现

image-20221012124311950

队列里的ready的数目已经变成了0,说明消息已经被消费了

SpringAMOP

AMQP:Advanced Message Queue Protocal ,是在应用程序或之间传递业务信息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求

Spring AMQP:Spring AMQP 是基于AMQP协议定义的一套API规范,提供模板来发送和接收消息,包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层默认实现

spring.io/projects/sp…

这个东西是帮助我们便捷的消息接收发送和队列的声明,之后我们几个模板都用它来实现

基本消息队列模型--HelloWorld案例--AMQP实现

1.引入依赖

        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

2.在publisher服务编写application.yml,添加mq连接信息

spring:
  rabbitmq:
    host: 主机名
    port: 5672
    virtual-host: /
    username: nika
    password: 密码

3.在test类测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmpqTest {
​
    @Autowired
    private RabbitTemplate rabbitTemplate;
​
    @Test
    public void testSimpleQueue(){
        String queueName = "simple.queue";
        String message = "hello,Spring amqp!";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

你看,用ampq方式,只需要写一个队列名和要发送的信息就好,是不是很方便?

image-20221012134001714

在启动完成测试类以后,由于我们之前的服务消费者服务没关,自动就收到了,说明发送消息成功!

我们再来试着用amqp的方式去写一个监听消息的方法

1.配置文件书写,和publisher一样

2.写一个listener

@Component
public class SpringRabbitListener {
​
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
    }
}

我们先把它注册成一个component,让spring帮助识别

之后rabbit的监听器,标注要监听的队列名,再用一个string接收传递来的队列消息,打印试试

然后我们启动main函数,等待接收消息

我们再启动运行这个函数,发送消息

image-20221012135159842

然后我们再cosumer的日志里发现:

image-20221012135216998

接收消息成功!

WorkQueue模型--AMQP

image-20221012135612542

workqueue从helloworld案例中进化了,从一个消费者变成多个,防止队列消息堆积,共同处理队列消息

下面我们来模拟一下workqueue,实现一个队列绑定多个消费者

1.再Publisher服务测试定义方法,每秒产生50条消息,发送到simple.queue

2.在consumer服务中定义俩消息监听者,都监听simple.queue队列

3.消费者1号每秒处理50条消息,消费者二号每秒处理10条消息

我们在SpringRabbitListener类中这么写:

@Component
public class SpringRabbitListener {
​
//    @RabbitListener(queues = "simple.queue")
//    public void listenSimpleQueue(String msg){
//        System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
//    }
    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1号接收到的消息:【" + msg + "】" + LocalTime.now());
        Thread.sleep(20);
    }
​
    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue2(String msg) throws InterruptedException {
        System.err.println("消费者2号接收到的消息:【" + msg + "】" + LocalTime.now());
        Thread.sleep(100);
    }
}

用线程休眠来模拟一分钟的处理效率

消息发送者这么定义:

  @Test
    public void testWorkQueue() throws InterruptedException {
        String queueName = "simple.queue";
        String message = "hello,Spring amqp!___";
        for (int i = 0; i <= 50; i++) {
            rabbitTemplate.convertAndSend(queueName,message+i);
            Thread.sleep(20);
        }
    }

一分钟发50条消息。

我们先来做个预测吧,因为消息发送者一秒发送50条消息,1号消费者每秒处理50条,2号每秒处理10条,理应处理速度小于1s,按照能者多劳的理论,1号肯定做的事情比2号多,然而事实真的是这样吗?

我们来试试运行代码。

image-20221012142024632

我们看下日志,不难发现,1号消费者处理的量和2号一样,并没有我们预想的能者多劳,这是为什么呢?

原来,有个问题叫消息预取,消费者会提前去队列里取好任务,管他能力够不够,先一人拿一半再说,所以会出现这种情况

我们可以进行配置取消消息预取,在properties文件中,设置prefetch值

image-20221012142730126

这样消费者就会拿一条消息,处理一条消息

我们重启看看配置是否生效

image-20221012142630096

生效!能者多劳!!

发布订阅模型介绍 -- AMQP

发布,订阅模式与之前的案例的区别是允许将同一消息发送给多个消费者。实现方式是加入exchange(交换机)。

image-20221012143126909

发布订阅模型--Fanout Exchange

image-20221012143611170

Fanout Exchange 会将收到的消息路由到每个跟其绑定的queue

我们来实现一下,上案例!

1.在consumer服务中,利用代码声明队列和交换机,并且将两者绑定

2.在consumer服务中,编写俩消费者方法,分别监听fanout.queue1和fanout.queue2

3.在publisher中编写测试方法,向itcast.fanout发送消息

@Configuration
public class FanoutConfig {
    //itcast.fanout
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }
​
    //fanout.queue1
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }
    //绑定队列1到交换机
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
        return BindingBuilder
                .bind(fanoutQueue1)
                .to(fanoutExchange);
    }
    //fanout.queue2
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }
    //绑定队列2到交换机
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
        return BindingBuilder
                .bind(fanoutQueue2)
                .to(fanoutExchange);
    }
​
}

我们启动代码,看看网站上

image-20221012145249277

exchange里面有itcast.fanout交换机

image-20221012145315653

queue有三,正确!

image-20221012145401386

也确实绑定上了

我们现在开始写消息的接收

   @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg){
        System.out.println("消费者接收到fanoutQueue1的消息:【" + msg + "】" );
    }
​
    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg){
        System.out.println("消费者接收到fanoutQueue2的消息:【" + msg + "】" );
    }

和发送

    @Test
    public void testSendFanoutExchange()  {
        //交换机名称
        String exchangeName = "itcast.fanout";
        //消息
        String message = "hello,everyone";
        //发送消息
        rabbitTemplate.convertAndSend(exchangeName,"",message);
    }

image-20221012150006941

这样就实现了一次发送,多个队列都可以收到消息

经过上面的案例,我们来总结一下交换机的作用吧

1.接收publisher发送的消息

2.按照将消息按照规则路由到与之绑定的队列

3.不能缓存消息,路由失败,消息丢失

4.fanoutexchange 会将消息路由到每一个绑定的队列当中

发布订阅模型--Direct Exchange

Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此被成为路由模式,而我们上述说的fanoutExchange则是把消息发送到每一个Queue中

  • 每一个Queue都会和Exchange设置一个BindingKey
  • 发送者发送消息的时候指定一个RoutingKey
  • Exchange将消息路由到BindingKey和RoutingKey一致的队列

看图片理解可能更加清晰

image-20221012190448128

比方说,现在我们已经将队列和交换机绑定好了,就像上图一样。

如果发布者发布的信息里的RoutingKey携带了red,那么消息会进入这两个队列,因为BindingKey里面都有red

如果发布者发布的信息里的RoutingKey携带了yellow,那么消息只会进入下面那个队列

好了接下来我们来对上述逻辑进行代码实现

思路:

1.利用@RabbitListener去声明交换机,队列,routingkey(这里就不用bean去声明交换机队列和它们的绑定关系了,因为实在太麻烦,要写好多好多bean)

2.在consumer服务中,编写俩消费者方法,分别监听direct.queue1,和direct.queue2(自定义的俩队列)

在listner类里面定义这些

    @RabbitListener( bindings = @QueueBinding(
            value = @Queue(name = "directQueue1"),
            exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void listenDirectQueue1(String msg){
        System.out.println("消费者接收到DirectQueue1的消息:【" + msg + "】" );
    }
​
    @RabbitListener( bindings = @QueueBinding(
            value = @Queue(name = "directQueue2"),
            exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
            key = {"red","yellow"}
    ))
    public void listenDirectQueue2(String msg){
        System.out.println("消费者接收到DirectQueue2的消息:【" + msg + "】" );
    }

然后重启代码

image-20221012192610020

image-20221012192617337

交换机,队列,都被注册上去了,再看看绑定关系

image-20221012192702321

OK,完美!

我们再定义发布者,先发送red吧

   @Test
    public void testSendDirectExchange()  {
        //交换机名称
        String exchangeName = "itcast.fanout";
        //消息
        String message = "hello";
        //发送消息
        rabbitTemplate.convertAndSend(exchangeName,"red",message);
    }

我们之前的预测是俩都会收到,看看结果!

image-20221012193200286

没错!那接下来我们发送yellow,我们的预测是队列2会收到

image-20221012193205565

测试成功!

我们来做个小总结吧,无论是用bean还是用注解,都是要申明交换机,queue和两者之间的联系,如果,你要精准发送,就得用一些key来路由

发布订阅--TopicExchange

想象一个场景,如果说这个key很多很多,那是不是这个key在方法上面越写越多?这很麻烦。

TopicExchange和DirectExchange十分类似,区别在于routingkey必须是多个单词的列表,并且要以 .分割

我们还是看一张图:

image-20221012195518063

Queue与Exchange指定BindingKey的时候,可以使用通配符:

#:代表0个或多个单词

*:代表一个单词

举个例子:

如果queue1的bindingkey是china.#,那么发布者的routingkey如果写china.abasbdbabs.asdasdasd.asdasdaaasdchina后面跟好多内容,是不是都可以匹配到queue1啊?

再举个例子:

如果queue2的bindingkey是#.news,那么发布者的routingkey如果写

asdasdasdasdasdasd.asdasdasdasdasd.news也都可以匹配到queue2啊?

来,思路讲完了,上代码!

思路:

1.还是利用@RabbitListener注解定义交换机队列,bindingkey

2.在consumer服务里面,编写俩消费者方法,分别监听topic.queue1和topic.queue2

3.在publisher里面编写测试方法,发送消息

    @RabbitListener( bindings = @QueueBinding(
            value = @Queue(name = "topicQueue1"),
            exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
            key = {"china.#"}
    ))
    public void listenTopicQueue1(String msg){
        System.out.println("消费者接收到TopicQueue1的消息:【" + msg + "】" );
    }
​
    @RabbitListener( bindings = @QueueBinding(
            value = @Queue(name = "topicQueue2"),
            exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
            key = {"#.news"}
    ))
    public void listenTopicQueue2(String msg){
        System.out.println("消费者接收到TopicQueue2的消息:【" + msg + "】" );
    }

重启,在可视化网站上看看有没有被注册。

    @Test
    public void testSendTopicExchange()  {
        //交换机名称
        String exchangeName = "itcast.topic";
        //消息
        String message = "hello";
        //发送消息
        rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
    }

来,经过我们上述的分析,这段代码是会被哪个队列收到呢?

image-20221012201032146

没错!是两个都收到了,因为i都符合匹配规则!

    @Test
    public void testSendTopicExchange()  {
        //交换机名称
        String exchangeName = "itcast.topic";
        //消息
        String message = "hello";
        //发送消息
        rabbitTemplate.convertAndSend(exchangeName,"asdasdasdasdasdasdas.news",message);
    }

这段代码由于只匹配队列2的规则,所以:

image-20221012201201654

完美!

消息转换器

之前我们的案例传送的数据都是string,但是rabbitTemplate能否传对象呢?

我们来试一试

先在发布者这边写下发布内容和要发布的队列

​
    @Test
    public void  testSendObject()  {
        String exchangeName = "object.queue";
        HashMap<String, String> map = new HashMap<>();
        map.put("nika","菜鸡");
        map.put("南信大","不行");
        //发送消息
        rabbitTemplate.convertAndSend(exchangeName,map);
    }

由于我们不想让队列里的消息被消费,我们想看看在rabbitmq可视化里面的消息嘛,所以这里我们只配置了队列的bean,这样,消息发送以后就只会停留在队列中,不会被消费

    @Bean
    public Queue objectQueue(){
        return new Queue("object.queue");
    }

在可视化里面,我们看到了之前放进去的map:

image-20221012202536614

什么?这是啥?为什么那么长?

原来这个东西spring内部帮我们做了序列化,并且用的是jdk的序列化(object。outputstream)!这种序列化安全性差,且效率不过,你看看消息体那么大,占用内存空间,传输速度也会变慢!

因此我推荐大家,不要用这个序列化方式!建议修改!

那么怎么样去修改呢?

我们推荐用JSON序列化,步骤如下:

  • 在父模块引入依赖

         <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
            </dependency>
    
  • 配置bean

        @Bean
        public MessageConverter messageConverter(){
            return new Jackson2JsonMessageConverter();
            //这也是springmvc里面经常用到的工具转化类
        }
    

    这里我们是在启动类里进行的配置,为了方便,嗯

    好了重启服务!

    发送!

    image-20221012203728008

ok,短小精悍!可读性强的一!

我们完成了消息的发送的序列化,而消息接收肯定得反序列化

]同样在consumer模块配置相同代码即可