RabbitMQ工作队列之竞争消费者模式(二)

RabbitMQ工作队列之竞争消费者模式(二)

本篇文章基于之前构建的项目中,详细讲解竞争消费者模式:

  • 基于客户端模式使用竞争消费者模式
  • 基于spring集成使用竞争消费者模式
  • 基于spring boot集成使用竞争消费者模式

RabbitMQ官方竞争消费者模式模型图

这里写图片描述

竞争消费者模式听起来比较拗口,说白了就是一个生产者,一个队列,多个消费者。
同样是点对点模式,但是在消费者之间,对消费队列是有一些规则策略的,如:公平分发策略,轮询分发策略等等。

目录

[TOC]来生成目录:

1、基于客户端模式使用竞争消费者模式

1.1获取连接工具类
package com.util;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author : alex
 * @version :1.0.0
 * @Date : create by 2018/7/19 22:01
 * @description :获取连接
 * @note 注意事项
 */
public class ConnectionUtils {

    //获取接连
    public static Connection getConnection() throws IOException, TimeoutException {

        ConnectionFactory factory = new ConnectionFactory();

        //设置连接MQ的IP地址
        factory.setHost("192.168.153.128");
        //设置连接端口号
        factory.setPort(5672);
        //设置要接连MQ的库(域)
        factory.setVirtualHost("/test_vh");
        //连接帐号
        factory.setUsername("test_mmr");
        //连接密码
        factory.setPassword("123456");
        return factory.newConnection();
    }

}
1.2生产者
package com.workqueue;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.util.ConnectionUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author : alex
 * @version :1.0.0
 * @Date : create by 2018/7/30 21:07
 * @description :消息发送者
 * @note 注意事项
 */
public class Sender {

    //定义队列名称
    public static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {

        //获取连接
        Connection connection = ConnectionUtils.getConnection();

        //创建channel通道
        Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //循环发送30条消息
        for (int i = 0; i < 30 ; i++) {

            //声明消息
            String msg = "this is msg [" + i + "]";
            System.out.println("发送消息:"+msg);
            //发送消息
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
            Thread.sleep(500);
        }

        //关闭通道
        channel.close();
        //关闭连接
        connection.close();
    }

}
1.3消费者1号
package com.workqueue;

import com.rabbitmq.client.*;
import com.util.ConnectionUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author : alex
 * @version :1.0.0
 * @Date : create by 2018/7/30 21:28
 * @description :
 * @note 注意事项
 */
public class Customer1 {

    public static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //定义消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                //获取并转成String
                String message = new String(body, "UTF-8");
                System.out.println("-->消费者1号,收到消息,msg :"+message);
            }
        };

        //监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);

    }

}
1.4消费者2号和消费者3号,复制消费者1号,代码都一样(记得把输出的代号改一下)

运行一下查看结果

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

可以看到按照取模的方式进行分发消息给消费者,每个消费者拿到的消息数量都是一样的。
默认RabbitMQ采用的是轮询分发策略。

1.5接口分析阶段

首先,我们找到找一下RabbitMQ Client的官方文档地址:
版本5.3.0:https://rabbitmq.github.io/rabbitmq-java-client/api/current/overview-summary.html
版本4.7.0:https://rabbitmq.github.io/rabbitmq-java-client/api/4.x.x/overview-summary.html

当前版本为5.3.0

打开到channel接口文档:https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/Channel.html

queueDeclare()接口方法,该方法为声明一个队列

  • queue:表示队列的名称
  • durable:参数为true表示声明一个持久队列(队列将在服务器重启后继续存在)
  • exclusive:参数为true表示声明一个独占队列(仅限于此连接)
  • autoDelete:参数为true表示声明一个自动删除队列(服务器将在不再使用时将其删除)
  • arguments:队列的其他属性(构造参数)
    这里写图片描述

basicPublish()接口方法,该方法有3个重载,该方法为发布消息

  • exchange:将消息发布到的交换器exchange
  • routingKey:路由密钥
  • mandatory:如果要设置“强制”标志,则为true
  • immediate:如果要设置’立即’标志,则为true
  • props:消息的其他属性 - 路由头等
  • body:消息体

这里写图片描述

DefaultConsumer类,定义消费者,用于后续的消费

  • getChannel():获得使用到的的Channel,定义的搜索频道
  • getConsumerTag():获得检索使用者标记。
  • handleCancel():取消处理
  • handleCancleOk():取消处理ok
  • handleConsumeOk():消费处理ok
  • handleDelivery():输送处理
  • handleRecoverOk():收回ok
  • handleShutdownSignal():关闭信号

这里写图片描述

文档地址:https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/DefaultConsumer.html

消费操作的方法basicConsume(),该方法用于消费消息

该方法重载比较多,就不一一全部讲解了

  • queue:队列名称
  • autoAck:是否自动确认消息
  • callback:之前的定义消费者类
  • deliverCallback:交付回调
  • cancelCallback:取消回调
  • shutdownSignalCallback:关闭信号的回调
  • consumerTag:使用者标记
  • exclusive:是否独占队列

这里写图片描述

如果想深入了解,文档地址:https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/Channel.html

1.6、扩展默认的轮询分发机制

首先抛出一个问题,假设消费者分布于不同的服务器上,每个消费者的处理速率不一,如:
消费者1需要执行2秒才结束
消费者2需要执行5秒
消费者3需要执行1秒

那么就会造成服务器的使用率不高,有一些服务器很忙,有一些服务器很闲。我们如何优化呢?

让我们模拟耗时操作:

分别在消费者1,2,3中的定义消费者中,加入以下代码,分别睡眠2、5、1秒

        try {
                    Thread.sleep(2000);//模拟耗时操作,2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

这里写图片描述

再执行一遍,发现虽然每台服务器执行数量都一样,但是有一些服务器很忙,有一些服务器很闲。

改造成公平分发策略,直接上修改后的代码,再解释:

生产者不需要改动,只需要改动消费者

以消费者1为例:

package com.workqueue;

import com.rabbitmq.client.*;
import com.util.ConnectionUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author : alex
 * @version :1.0.0
 * @Date : create by 2018/7/30 21:28
 * @description :消费者1
 * @note 注意事项
 */
public class Customer1 {

    public static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        Connection connection = ConnectionUtils.getConnection();

        final Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        channel.basicQos(1);//添加设置,每次处理1个

        //定义消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                //获取并转成String
                String message = new String(body, "UTF-8");
                System.out.println("-->消费者1号,收到消息,msg :"+message);

                try {
                    Thread.sleep(2000);//模拟耗时操作,2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    channel.basicAck(envelope.getDeliveryTag(),false);//手动确认
                }
            }
        };


        //监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);//这里改为false关闭自动确认

    }

}

上述代码改动4处:

  • channel.basicQos(1);//添加设置,每次处理1个
  • Channel设置为final
  • finally 模块添加手动确认:channel.basicAck(envelope.getDeliveryTag(),false);//手动确认
  • 最后消费时改为false关闭自动确认

依次修改3个消费者,结束

讲一下思路,在前面轮询分发时,消息是自动确认的,自动确认消息的同时,会一直监听队列获取新任务,哪怕设置当前接收任务数为1,也无法实现。(感兴趣的可以试一下,设置接收任务数为1,开启消息自动确认,移除手动确认)

改造思路:关闭消息自动确认,并且设置当前接收任务处理任务数为1,消费者手动确认消息,在收到确认消息之后,才可以继续监听队列获取新的消息任务。

2、基于spring集成使用竞争消费者模式

2、1轮询分发策略

spring集成后比较简单

这里写图片描述

在原有的项目基础上(参考RabbitMQ工作队列模式简介,以及简单使用、简单集成spring使用(一) ,地址:https://blog.csdn.net/u011709128/article/details/81263309),添加复制MyCustomer类,粘帖成MyCustomer2和MyCustomer3两个类,print输出带上消费者代号
然后修改rabbitmq.xml文件,定义两个消费者,队列监听添加两个消费者。就可以了

我们运行一下看看
这里写图片描述

可以看到按照轮询的方式进行分发消息。

2、2改造成公平分发策略

main类

package com.rabbitmq.handle;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author : alex
 * @version :1.0.0
 * @Date : create by 2018/7/19 23:02
 * @description :
 * @note 注意事项
 */
public class Main {

    public static void main(String[] args) throws InterruptedException {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:context.xml");

        //获取rabbit模版(等价于@Autowired)
        RabbitTemplate bean = context.getBean(RabbitTemplate.class);

        //循环发送30条消息
        for (int i =0;i<30;i++) {
            bean.convertAndSend("test_mmr","hello word"+i);
            Thread.sleep(500);//休眠0.5秒
        }

        Thread.sleep(10000);//休眠2秒后,关闭spring容器
        context.close();

    }
}

消费者1、消费者2、消费者3,代码其实都一样,但是要把耗时操作改为1秒、3秒、5秒。
实现与ChannelAwareMessageListener后,自定义的监听方法listen()就不起作用了。使用onMessage()消费

package com.rabbitmq.handle;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;

/**
 * @author : alex
 * @version :1.0.0
 * @Date : create by 2018/7/19 23:39
 * @description :我的消费者
 * @note 注意事项
 */
public class MyCustomer implements ChannelAwareMessageListener {

    public void listen(String foo){
        System.out.println("消费者消费1,获取消息msg:"+foo);
    }

    public void onMessage(Message message, Channel channel) throws Exception {
        try{
            //message.getMessageProperties()+
            Thread.sleep(1000);//模拟耗时操作1秒
            System.out.println("consumer--:"+":"+new String(message.getBody()));
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }catch(Exception e){
            e.printStackTrace();//TODO 业务处理
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
        }
    }
}

更改rabbitmq.xml中队列监听参数,并且定义多个消费者

    <!--定义消费者-->
    <bean id="myCustomer" class="com.rabbitmq.handle.MyCustomer"/>
    <bean id="myCustomer2" class="com.rabbitmq.handle.MyCustomer2"/>
    <bean id="myCustomer3" class="com.rabbitmq.handle.MyCustomer3"/>

    <!--队列监听 acknowledge应答方式:auto,manual,none   prefetch等价于channel.basicQos(1) -->
    <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1" >
        <rabbit:listener ref="myCustomer" method="listen" queue-names="test_mmr"  />
        <rabbit:listener ref="myCustomer2" method="listen" queue-names="test_mmr"/>
        <rabbit:listener ref="myCustomer3" method="listen" queue-names="test_mmr" />
    </rabbit:listener-container>

运行结果:可以看到,消费者1、消费者2、消费者3一开始的时候,就领走了编号0、1、2的消息,但是由于每个消费者中的耗时操作不同,机器的空闲情况不同,后续消费者1消费了:0、3、4之后,消费者2才开始消费第二个消息,而消费者3耗时操作最久,第二次领取消息时,已经是编号9了
这里写图片描述

3、基于spring boot集成使用竞争消费者模式

3、1默认的多消费者模式最简单。

在原有的项目基础上(参考RabbitMQ工作队列模式简介,以及简单使用、简单集成spring使用(一) ,地址:https://blog.csdn.net/u011709128/article/details/81263309

基于spring boot实现中,多复制几份CustomerMsg,也就是消费者类出来,就满足了

目录结构图:

这里写图片描述

运行效果图:

这里写图片描述

3、2改造成公平分发策略

maven构建spring boot项目

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- rabbitmq -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

主程序入口:DemoApplication

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

配置文件:application.yml

spring:
  rabbitmq:
    username: root
    password: 123456
    host: 192.168.199.128
    port: 5672
    virtual-host: /test_vh
    listener:
      simple:
        acknowledge-mode: manual #设置确认方式
        prefetch: 1 #每次处理1条消息

以web的方式访问:IndexController.java

package com.example.demo.controller;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: Alex
 * @DateTime: 2018/8/17 16:36
 * @Description: 模拟生产者
 * @Version: 1.0.0
 **/
@RestController
public class IndexController {


    @Autowired
    private AmqpTemplate amqpTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;


    /**
     * 使用AmqpTemplate
     * @return
     * @throws Exception
     */
    @PostMapping("/amqpSend")
    public String amqpSend() throws Exception{
        String msg = "amqp";
        for (int i=0;i<20;i++){
            amqpTemplate.convertAndSend("test_mmr",msg);//指定发送的队列名称,和数据
            System.out.println("序号:"+i+",发送时间:"+System.currentTimeMillis()+",发送消息:"+msg);
            Thread.sleep(1000);//1秒
        }
        return msg;
    }

    /**
     * 使用RabbitTemplate
     * @return
     * @throws Exception
     */
    @PostMapping("/rabbitSend")
    public String rabbitSend() throws Exception{
        String msg = "rabbit";
        for (int i=0;i<20;i++) {
            rabbitTemplate.convertAndSend("test_mmr", msg+i);
//            System.out.println("生产者,序号:"+i+",发送时间:"+System.currentTimeMillis()+",发送消息:"+msg);
            Thread.sleep(500);//0.5秒
        }
        return msg;
    }
}

消费者1

package com.example.demo.rabbitUtil;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author: Alex
 * @DateTime: 2018/8/17 16:35
 * @Description: 模拟消费者1
 * @Version: 1.0.0
 **/
@Component
//监听的队列
@RabbitListener(queues = "test_mmr")
public class CustomerMsg {

    /**
     * 进行接收处理
     * @param string
     */
    @RabbitHandler
    public void onMessage(String string,Channel channel, Message message) throws IOException, InterruptedException {
        Thread.sleep(1000);
        System.out.println("消费者1,接收时间:"+System.currentTimeMillis()+",收到消息,消息: " + string);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动确认
        //丢弃这条消息
        //channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
    }

}

消费者2

package com.example.demo.rabbitUtil;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author: Alex
 * @DateTime: 2018/8/17 16:42
 * @Description: 模拟消费者2
 * @Version: 1.0.0
 **/
@Component
//监听的队列
@RabbitListener(queues = "test_mmr")
public class CustomerMsg2 {

    /**
     * 进行接收处理
     * @param string
     */
    @RabbitHandler
    public void onMessage(String string,Channel channel, Message message) throws InterruptedException, IOException {
        Thread.sleep(3000);
        System.out.println("消费者2,接收时间:"+System.currentTimeMillis()+",收到消息,消息: " + string);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动确认
        //丢弃这条消息
        //channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
    }

}

项目目录结构图:
这里写图片描述

运行效果图:
这里写图片描述

4、小结

真心感叹spring的强大,一次比一次的方便使用
与此同时,rabbitMQ的手动确认消息机制,必须还是channel干活,spring集成rabbitMQ的使用,基于bean方式的注入各种配置,各种设置,如:queue,RabbitListener之类,但是使用起来,和原生的使用的理念不大,只是使用的方式更便捷了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值