RabbitMQ学习

        参考大神文档:https://blog.csdn.net/kavito/article/details/91403659?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162795376016780265433132%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=162795376016780265433132&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-3-91403659.pc_search_result_cache&utm_term=rabbitmq&spm=1018.2226.3001.4187

一、为什么需要MQ

        学习一个工具,习惯思考其使用的场景,没有使用价值,也没有学习的必要。要了解MQ的适用场景,可以先看一个业务场景。

        商品的原始数据都存储在数据库中,增删改查都在数据库中完成,搜索索引为ES,前端页面为静态页面,不会随着数据库的数据变化而自动变化。

        当修改数据库中数据时,比如价格,需要及时的反馈到ES和前端页面。怎么来设计这个框架?比较差的

        (1)数据库端开放接口,供ES和前端页面不停的查询——延迟先不讨论,前端、ES和数据库模块强耦合,还会造成接口查询的压力和资源的浪费。

        (2)前端页面和ES开放接口,供数据库处理模块每次需改数据后的调用——模块间耦合。

        引入MQ消息队列,数据库端处理完,只需向MQ发送一个消息,前端页面和ES消费订阅消费消息,根据消息内容,自主处理,解耦,并且消息队列的强大,延迟基本忽略。

二、MQ消息队列

        MQ全称Message Queue,即消息队列,是在消息传输过程中存储消息的容器。生产者(上例中的数据库端)和消费者(上例中的ES和前端页面)只关注消息的发送和接收,没有业务逻辑的入侵,实现生产者和消费者的解耦。

三、MQ使用场景

3.1 异步处理任务

        高并发环境使用的比较多,由于来不及处理,请求往往会阻塞,响应延迟。通过消息队列,将不需要立即处理的请求,可以异步处理请求,请求达到MQ,就可以给出响应。然后MQ在通知消息接收模块,达到异步处理,减少系统响应时间。

3.2 应用程序解耦

        MQ充当中介角色,应用程序通过中介与需要交互的程序通信。

四、MQ分类

        实现消息队列,现在主流的方式有:AMQP和JMS。

  • JMS定义了统一接口,来对消息进行统一操作;AMQP是通过规定协议来统一数据交换格式
  • JMS必须使用JAVA语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
  • JMS规定两种消息模型;AMQP消息模型更丰富。

        主流的MQ产品有:

  • ActiveMQ:基于JMS
  • RabbitMQ:基于AMQP,erlang语言开发,稳定性好
  • RocketMQ:基于JMS,阿里巴巴产品
  • Kafka:分布式消息系统,高吞吐量。

五、RabbitMQ的工作原理

5.1 RabbitMQ工作原理结构图

  • Broker:消息队列服务进程,此进程包括Exchange(交换机)和Queue(队列)
  • Exchange:消息队列交换机,按一定的规则,将消息转发到某个队列,对消息进行过滤。 
  • Queue:消息队列,存储消息的队
  • Producer:消息生产者,生产方客户端,将消息发送到Exchange
  • Consumer:消费者,消费者客户端,接收MQ转发的消息。

5.2 Exchange类型

        交换机,生产者所有发出的消息都是直接发给交换机,交换机转发到相应的队列。RabbitMQ提供默认的交换机,只需在publish发布消息时,交换机参数填写空,就可以使用RabbitMQ提供的默认交换机。

        当然也可以根据自己业务的需要,自己定义交换机,交换机类型如下:

  • Fanout:广播,将消息交给所有绑定到交换机的队列
  • Direct:定向,把消息交给符合routing key的队列
  • Topic:通配符,把消息交给符合routing pattern(路由模式)队列

5.3 生产者发送消息流程

  1. 生产者和Broker建立TCP连接
  2. 生产者和Broker建立通道Channel
  3. 生产者通过通道发送消息给Broker
  4. Exchange转发消息给指定的Queue

5.4 消费者接收消息流程

  1. 消费者和Broker建立TCP连接
  2. 消费者和Broker建立通道Channel
  3. 消费者监听队列Queue
  4. 当有消息到达队列Queue时,Broker默认将消息推送给消费者
  5. 消费者接收到消息
  6. ack回复

六、代码案例实现

 6.1 基本消息模型

         

        一个生产者一个队列,一个消费者。

        首先,使用maven项目,手动写创建Broker的Connection(Spring boot集成RabbitMQ后面再说),新建两个maven工程,producer和consumer,引入RabbitMQ的Maven依赖。

<dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.7.1</version>
        </dependency>
    </dependencies>

        连接工具类,调用rabbitMQ的ConnectionFactory类,工厂方式创建连接

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


public class ConnectionUtil {

    public static Connection getConnection() throws Exception{
        //定义工厂类
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("10.21.70.43");
        //端口
        factory.setPort(5672);
        //设置账号信息
        factory.setUsername("guest");
        factory.setPassword("guest");

        return factory.newConnection();
    }
}

        在工程producer中,生产者端代码

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

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

public class ProducerSend {

    private static final String QUEUE_NAME="simple_queue";

    public static void main(String[] args) {

        //获取连接
        Connection connection = null;
        
        //2.从连接中创建通道
        Channel channel = null;
        
        try {
            
            connection = ConnectionUtil.getConnection();
            
            channel = connection.createChannel();
            
            // 3、声明(创建)队列
            //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);

            //4.消息
            String msg = "Hello my friend!";

            //向指定队列中发送消息
            //参数:String exchange, String routingKey, BasicProperties props, byte[] body
            /**
             * 参数明细:
             * 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
             * 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
             * 3、props,消息的属性
             * 4、body,消息内容
             */

            channel.basicPublish("", QUEUE_NAME,null,msg.getBytes());
            System.out.println("send '"+msg+"'");
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            
            if (connection != null) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        
        

    }
}

        consumer工程中消费者接收消息代码,连接工具类使用producer工程中的。

import com.cc.consumer.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.UnsupportedEncodingException;

public class ConsumerRecv {

    private static final String QUEUE_NAME = "simple_queue";

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


        //1.获取connection
        Connection connection = ConnectionUtil.getConnection();

        //2.获取通道Channel,生产者和MQ的通信都在channel中完成
        Channel channel = connection.createChannel();

        //3.声明队列
        //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        /**
         * 参数明细
         * 1、queue 队列名称
         * 2、durable 是否持久化,如果持久化,mq重启后队列还在
         * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
         * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
         * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
         */
        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 UnsupportedEncodingException {
                //交换机
                String exchange = envelope.getExchange();

                //消息id,消息在channel中传输用来标识消息的id,可用于消息的确认接收
                long deliveryTag = envelope.getDeliveryTag();

                //body
                String msg = new String(body,"UTF-8");

                System.out.println("receive msg '"+msg+"'");
            }
        };

        //监听队列,第二个参数是否自动进行消息确认
        channel.basicConsume(QUEUE_NAME,true,consumer);

    }
}

手动确认消息模式 

        自动确认模式,Broker会在将消息发送到Channel时,就确认消息成功消费,如果消费者端因为代码原因,消息没有正常处理,将会丢失该消息。所以需要手动确认消息,如果业务逻辑失败,会将消息重新放回Broker队列,等待下次消费。

        消费者端代码稍作改动即可。

6.2 work消息模型 

        一个队列对应两个消费者,队列中消息只能被一个消费者消费,竞争消费者模式。

        这种模式比较适合分压的场景,在不设置通道Channel的BasicQos对象的prefetchCount属性时, RabbitMQ会平分所有任务给两个消费者,示例代码如下,在6.1示例上修改,不贴出全部代码,只贴出核心代码的截图

        生产者循环发送50个消息到队列

        消费者C1,实现和6.1中一样,C2中在手动确认前休眠1秒,模拟处理比较慢的场景。

        两个队列的输出如下

        发现两个Consumer消费消息的数量是一致的,所以可以知道,默认情况,RabbitMQ是平分两个消费者的工作量的。

        需要处理快的Consumer多处理消息,可以设置channel的BasicQos对象的prefetchCount属性,设置成1,即表示一个消费者在同一个时间点只处理一个消息,也就是说,必须收到消息确认后,才会接收处理第二条消息。

        prefetchCount只在手动确认模式下生效,其实也很好理解,自动确认,刚到Channel,就确认了,跟业务处理快慢没有关系了。 

         可以看到输出结果的差别,快的Consumer消费了更多的msg

        以上都是使用了RabbitMQ默认的交换机,下面将从交换机三种类型来说明,交换机类型在5.2小节已经说明

6.3 publish/subscribe(广播)

        实现生产者消息被多个消费者同时消费,模型示意图如下:

        Producer生产者,不再声明队列,而是声明交换机,转发所有Consumer消费者绑定到声明的交换机上的队列,示例代码如下:

        Producer代码:

import com.cc.producer.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;


public class ProducerBroadcast {

    private static final String EXCHANGE_NAME = "test_fanout_exchange";

    public static void main(String[] args)  {

        Connection connection = null;

        Channel channel = null;
        try {
            connection = ConnectionUtil.getConnection();

            //获取通道
            channel = connection.createChannel();

            //声明交换机,
            /**
             * 广播模式,不在绑定指定队列,转发所有队列
             */
            channel.exchangeDeclare(EXCHANGE_NAME,"fanout");

            //消息内容
            String msg = "注册成功";

            //发布消息到exchange
            channel.basicPublish(EXCHANGE_NAME,"",null,msg.getBytes(StandardCharsets.UTF_8));

            System.out.println("[生产者] send '"+msg+"'");
        } catch (Exception e){
            e.printStackTrace();
        } finally {

            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException | TimeoutException e) {
                    e.printStackTrace();
                }
            }

            if (connection != null) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }


        }


    }
}

          示例中设定两个Consumer,代码基本一致,就是队列 名不一样,所以这边就贴出一份代码

import com.cc.consumer.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;


public class ConsumerBroadcast1 {

    private static final String EXCHANGE_NAME = "test_fanout_exchange";

    private static final String QUEUE_NAME = "fanout_exchange_queue_sms1";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = ConnectionUtil.getConnection();

        //获取通道
        Channel channel = connection.createChannel();

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

        //绑定队列到交换机
        //routingKey为空
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");

        //消费者
        DefaultConsumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //交换机
                String exchange = envelope.getExchange();

                //消息id,消息在channel中传输用来标识消息的id,可用于消息的确认接收
                long deliveryTag = envelope.getDeliveryTag();

                //body
                String msg = new String(body, StandardCharsets.UTF_8);

                System.out.println("[消费者1] received '"+msg+"'");

                //手动进行消息确认
                /*
                 *  void basicAck(long deliveryTag, boolean multiple) throws IOException;
                 *  deliveryTag:用来标识消息的id
                 *  multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
                 */
                channel.basicAck(deliveryTag,false);
            }
        };

        channel.basicConsume(QUEUE_NAME,false,consumer);

    }
}

       可以发现Producer执行发送消息后,两个Consumer都能消费到信息。

Tips:

        publish/subscribe模式和work queues模式区别?

        区别:

  • work queues不用定义交换机,用RabbitMQ默认交换机,而publish/subscribe模式需要定义交换机
  • publish/subscribe时生产者向交换机发送消息,work queues模式,看起来是直接发送到queue,其实也是发送到默认交换机,有交换机直接转发到指定队列。
  • publish/subscribe模式,在消费者端需要设置队列与交换机的绑定,work queue将队列绑定到默认的交换机上。

        相同点:

        其实从底层实现来看,publish/subscribe模式和work queues模式是一样的,都是将消息发送给交换机,由交换机转发队列,publish/subscribe模式的示例,将两个消费者的队列名设置成一样,就是work queues模式,只不过交换机由默认换成自定义的一个交换机。

6.4 Routing路由模式(Direct定向交换机)

        队列和路由绑定,根据routing key直接找到指定队列的模式,模型示意图如下:

  • 生产者向Exchange发送消息时,会指定一个routing key
  • Exchange将消息转发给和routing key完全匹配的队列,和Routing pattern模式的主要区别
  • 消费者声明队列时,指定了需要消费的队列routing key
import com.cc.producer.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;


public class ProducerDirect {

    private static final String EXCHANGE_NAME = "test_direct_exchange";

    public static void main(String[] args){

        Connection connection = null;

        Channel channel = null;

        try {
            connection = ConnectionUtil.getConnection();

            channel = connection.createChannel();

            //声明交换机exchange,指定为direct类型
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

            String msg = "注册成功,请回复短信[T]退订";

            //发送消息,并且指定routing key为"sms"
            channel.basicPublish(EXCHANGE_NAME,"sms",null,msg.getBytes(StandardCharsets.UTF_8));

            System.out.println("Send '"+msg+"'");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (channel == null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }

            if (connection != null) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

        消费者端代码,关注绑定Exchange部分

import com.cc.consumer.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;


public class ConsumerDirect {

    private static final String EXCHANGE_NAME = "test_direct_exchange";

    private static final String QUEUE_NAME = "direct_exchange_queue_sms";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();

        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //绑定到交换机,并且指定routing key为"sms"
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"sms");

        //消费者
        DefaultConsumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //交换机
                String exchange = envelope.getExchange();

                //消息id,消息在channel中传输用来标识消息的id,可用于消息的确认接收
                long deliveryTag = envelope.getDeliveryTag();

                //body
                String msg = new String(body, StandardCharsets.UTF_8);

                System.out.println("[消费者] received '"+msg+"'");

                //手动进行消息确认
                /*
                 *  void basicAck(long deliveryTag, boolean multiple) throws IOException;
                 *  deliveryTag:用来标识消息的id
                 *  multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
                 */
                channel.basicAck(deliveryTag,false);
            }
        };

        channel.basicConsume(QUEUE_NAME,false,consumer);
    }
}

6.5 通配符模式(Topics类型交换机)

        routing key为通配符方式,可对比6.4小节,direct模式,原理示意图如下:

        通配符规则: 

        #:匹配一个或者多个单词

        *:只能匹配一个单词

        样例说明通配规则,

        audit.#:能够匹配audit.irs.corporate或者audit.irs

        audit.*:只能匹配audit.irs

        示例代码,跟direct类似,只粘贴出重要部分

        生产者代码和direct基本类似,直接贴消费者端代码

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值