RabbitMQ 详细到落泪

RabbitMQ笔记

高级部分点击
演示代码下载

RabbitMQ是一款Message Queue(消息队列)的产品,是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 RabbitMQ官方地址:http://www.rabbitmq.com/

1.基础架构

请添加图片描述

RabbitMQ 中的相关概念:

  1. Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

  2. Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等

    AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,遵循此协议,不收客户端和中间件产品和开发语言限制

  3. Connection:publisher/consumer 和 broker 之间的 TCP 连接

  4. Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCPConnection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

  5. Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) andfanout (multicast)

  6. Queue:消息最终被送到这里等待 consumer 取走

  7. Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

2. 相关配置

开启管理界⾯
 rabbitmq-plugins enable rabbitmq_management
修改默认配置信息(⽐如修改密码、配置等等,例如: loopback_users 中的 <<“guest”>>,只保留guest)
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
service rabbitmq-server start # 启动服务
service rabbitmq-server stop # 停⽌服务
service rabbitmq-server restart # 重启服务 

访问管理页面 http://ip地址:15672 账号密码都是guest

如果出现打不开管理页面可以考虑Linux防火墙

# 关闭防火墙服务
systemctl disable firewalld
systemctl stop firewalld  

有6种工作模式

我们使用java来学习RabbitMQ首先我们先做一个连接的准备工作

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.6.0</version>
</dependency>
public class ConnectionUtils {
	//消费者和生产者共用
    public static Connection getConnection() throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //主机地址;默认为 localhost
        connectionFactory.setHost("192.168.220.128");
        //连接端口;默认为 5672
        connectionFactory.setPort(5672);
        //虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/demo");
        //连接用户名;默认为guest
        connectionFactory.setUsername("sgp");
        //连接密码;默认为guest
        connectionFactory.setPassword("sgp");
        //创建连接
        Connection connection = connectionFactory.newConnection();
        return connection;
    }
}

下面我们开始介绍

1.最简单的模式

只有一个消费者 一个生产者和一个队列

(P) -> [|||] -> (C)

生产者

1. 获取连接
2. 创建频道 Channel
3. 声明或者创建队列
4. 向队列中发送消息

=== 需要注意的是队列只需要在生产者或者消费者创建一次 另一个不写也可 ===

public class Producer {
	//定义队列名字
    public static String QUEUE_NAME = "work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        //创建频道
        Channel channel = connection.createChannel();
        //声明(创建)队列
        /**
         * 参数1:队列名称
         * 参数2:是否定义持久化队列
         * 参数3:是否独占本次连接,只能有一个Consumer监听这个队列
         * 参数4:是否在不使用的时候自动删除队列,当没有Consumer时,自动删除
         * 参数5:队列其它参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        for (int i = 1; i <= 30; i++) {
            //发送消息
            String message = "你好:小兔子~ work queue模式---" + i;
            /**
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage
             * 参数2:路由key,简单模式可以传递队列名称
             * 参数3:消息其它属性
             * 参数4:消息内容
             */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("已发送消息:" + message);
        }
        //释放资源
        channel.close();
        connection.close();
    }
}	

消费者

  1. 获取连接
  2. 创建频道 Channel
  3. 声明或者创建队列
  4. 接收消息
public class Consumer1 {


    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        //创建频道
        Channel channel = connection.createChannel();
        //声明(创建)队列
        /**
         * 参数1:队列名称
         * 参数2:是否定义持久化队列
         * 参数3:是否独占本次连接,只能有一个Consumer监听这个队列
         * 参数4:是否在不使用的时候自动删除队列,当没有Consumer时,自动删除
         * 参数5:队列其它参数
         */
        channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);
        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 接收到消息执行的的回调
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1 - 接收到的消息为:" + new String(body, "utf-8"));
            }
        };

        /**
         * 参数1:队列名称
         * 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
         * 参数3:消息接收到后回调
         */
        channel.basicConsume(Producer.QUEUE_NAME, true, consumer);
        //释放资源
//        channel.close();
//        connection.close();
    }
}	

2.工作模式

与简单模式相比消费者可以有多个 彼此之间形成竞争者 默认按轮询的方式去消费消息

img

​ 与简单模式就是多了一个消费者

3.发布订阅模式Publish/Subscribe

生产者发送消息到交换机,交换机在转发给绑定该交换机队列,同时消费者一直监听与他绑定的队列一旦队列中有消息会立即收到;发布与订阅使用的交换机类型为:fanout

RabbitMQ 中消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,生产者通常根本不知道消息是否会被传递到任何队列。生产者只能向交换器发送消息。交换是一件非常简单的事情。一方面它接收来自生产者的消息,另一方面它将它们推送到队列中。那么交换机怎么知道应该吧消息发送到那个队列呢?其规则由 交换类型定义。有几种可用的交换类型:direct、topic、headers 和fanout。

img

生产者

  1. 获取连接
  2. 创建频道 Channel
  3. 声明交换机类型为fanout
  4. 声明或者创建队列
  5. 绑定交换机和对应队列
  6. 向交换机发送消息
public class Producer {
	
    //定义交换机
    public static String FANOUT_EXCHAGE = "fanout_exchage";
    //定义队列
    public static String FANOUT_QUEUE_1 = "fanout_queue_1";
    public static String FANOUT_QUEUE_2 = "fanout_queue_2";


    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        //创建频道
        Channel channel = connection.createChannel();

        /**声明交换机
         * 参数1:交换机名称
         * 参数2:交换机类型,fanout, topic, direct, headers
         */
        channel.exchangeDeclare(FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);

        //声明(创建)队列
        /**
         * 参数1:队列名称
         * 参数2:是否定义持久化队列
         * 参数3:是否独占本次连接,只能有一个Consumer监听这个队列
         * 参数4:是否在不使用的时候自动删除队列,当没有Consumer时,自动删除
         * 参数5:队列其它参数
         */
        channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
        channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null);

        //队列绑定交换机
        channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, "");
        channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHAGE, "");

        for (int i = 1; i <= 10; i++) {
            //发送消息
            String message = "你好:小兔子~ fanout 模式---" + i;
            /**
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage
             * 参数2:路由key,简单模式可以传递队列名称
             * 参数3:消息其它属性
             * 参数4:消息内容
             */
            channel.basicPublish(FANOUT_EXCHAGE, "", null, message.getBytes());
            System.out.println("已发送消息:" + message);
        }
        //释放资源
        channel.close();
        connection.close();
    }
}

消费者 可以有多个

  1. 获取连接
  2. 创建频道 Channel
  3. 声明或者创建队列
  4. 绑定交换机
  5. 接收队列中的消息
public class Consumer1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        //创建频道
        Channel channel = connection.createChannel();
        //声明(创建)队列
        /**
         * 参数1:队列名称
         * 参数2:是否定义持久化队列
         * 参数3:是否独占本次连接,只能有一个Consumer监听这个队列
         * 参数4:是否在不使用的时候自动删除队列,当没有Consumer时,自动删除
         * 参数5:队列其它参数
         */
        channel.queueDeclare(Producer.FANOUT_QUEUE_1, true, false, false, null);

        //队列绑定交换机
        channel.queueBind(Producer.FANOUT_QUEUE_1, Producer.FANOUT_EXCHAGE, "");

        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 接收到消息执行的的回调
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1 - 接收到的消息为:" + new String(body, "utf-8"));
            }
        };

        /**
         * 参数1:队列名称
         * 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
         * 参数3:消息接收到后回调
         */
        channel.basicConsume(Producer.FANOUT_QUEUE_1, true, consumer);
        //释放资源 注释是为了持续监听
//        channel.close();
//        connection.close();
    }
}

如果创建多个消费者来监听多个队列 那么当生产者发送消息时 所有的消费者都会接收到信息

4.Routing路由模式
  1. 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey (路由key)
    消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey 。
  2. Exchange不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的 Routingkey 与消息的 Routing key 完全一致,才会接收到消息

img

生产者

  1. 获取连接
  2. 创建频道 Channel
  3. 声明交换机的类型为direct
  4. 声明或者创建队列
  5. 通过RoutingKey绑定交换机和列队
  6. 向交换机发送消息
public class Producer {

    public static String DIRECT_EXCHAGE = "direct_exchage";
    public static String DIRECT_QUEUE_INSERT = "direct_queue_insert";
    public static String DIRECT_QUEUE_UPDATE = "direct_queue_update";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        //创建频道
        Channel channel = connection.createChannel();

        /**声明交换机
         * 参数1:交换机名称
         * 参数2:交换机类型,fanout, topic, direct, headers
         */
        channel.exchangeDeclare(DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);

        //声明(创建)队列
        /**
         * 参数1:队列名称
         * 参数2:是否定义持久化队列
         * 参数3:是否独占本次连接,只能有一个Consumer监听这个队列
         * 参数4:是否在不使用的时候自动删除队列,当没有Consumer时,自动删除
         * 参数5:队列其它参数
         */
        channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false, null);
        channel.queueDeclare(DIRECT_QUEUE_UPDATE, true, false, false, null);

        //队列绑定交换机
        channel.queueBind(DIRECT_QUEUE_INSERT, DIRECT_EXCHAGE, "insert");
        channel.queueBind(DIRECT_QUEUE_UPDATE, DIRECT_EXCHAGE, "update");

        //发送消息
        String message = "新增商品。 路由模式:routing key insert";
        /**
         * 参数1:交换机名称,如果没有指定则使用默认Default Exchage
         * 参数2:路由key,简单模式可以传递队列名称
         * 参数3:消息其它属性
         * 参数4:消息内容
         */
        channel.basicPublish(DIRECT_EXCHAGE, "insert", null, message.getBytes());
        System.out.println("已发送消息:" + message);

        //发送消息
        message = "修改商品。 路由模式:routing key update";
        /**
         * 参数1:交换机名称,如果没有指定则使用默认Default Exchage
         * 参数2:路由key,简单模式可以传递队列名称
         * 参数3:消息其它属性
         * 参数4:消息内容
         */
        channel.basicPublish(DIRECT_EXCHAGE, "update", null, message.getBytes());
        System.out.println("已发送消息:" + message);
        //释放资源
        channel.close();
        connection.close();
    }
}

消费者

  1. 获取连接
  2. 创建频道 Channel
  3. 声明交换机的类型为direct
  4. 声明或者创建队列
  5. 通过RoutingKey绑定交换机和列队
  6. 向交换机发送消息
public class Consumer1 {

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        //创建频道
        Channel channel = connection.createChannel();
        //声明(创建)队列
        /**
         * 参数1:队列名称
         * 参数2:是否定义持久化队列
         * 参数3:是否独占本次连接,只能有一个Consumer监听这个队列
         * 参数4:是否在不使用的时候自动删除队列,当没有Consumer时,自动删除
         * 参数5:队列其它参数
         */
        channel.queueDeclare(Producer.DIRECT_QUEUE_INSERT, true, false, false, null);

        //队列绑定交换机
        channel.queueBind(Producer.DIRECT_QUEUE_INSERT, Producer.DIRECT_EXCHAGE, "insert");

        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 接收到消息执行的的回调
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1 - 接收到的消息为:" + new String(body, "utf-8"));
            }
        };

        /**
         * 参数1:队列名称
         * 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
         * 参数3:消息接收到后回调
         */
        channel.basicConsume(Producer.DIRECT_QUEUE_INSERT, true, consumer);

        //释放资源
//        channel.close();
//        connection.close();
    }
}

将接收到routingkey为insert的消息 同理可写出routingkey为update的消费者

5.Topics通配符模式

Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,

通配符规则:# 匹配一个或多个词, * 匹配不多不少恰好1个词举例:

item.# :能够匹配 item.insert.abc 或者 item…* :只能匹配 item.insert

img

和路由模式基本相同 只不过采用通配符的RoutingKey 来绑定交换机和队列 代码就不写啦

6.远程过程调用 (RPC)不常用

img

RabbitMQ工作模式:

  1. 简单模式 HelloWorld 一个生产者、一个消费者,不需要设置交换机(使用

默认的交换机)

  1. 工作队列模式 Work Queue 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)
  2. 发布订阅模式 Publish/subscribe 需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列
  3. 路由模式 Routing 需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routingkey,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
  4. 通配符模式 Topic 需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

高级特性

1.消息的可靠性

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。

rabbitmq 整个消息投递的路径为:
producer—>rabbitmq broker—>exchange—>queue—>consumer

1.confirm 确认模式

消息从 producer 到 exchange 则会返回一个 ConfirmCallback。

需要开启

channel.confirmSelect();//开启确认模式
channel.waitForConfirmsOrDie(3000); //设置最晚响应时间

可以通过addConfirmListener方法来接收

//lambda表达式传参
channel.addConfirmListener((sequenceNumber, multiple) -> {
    System.out.println(sequenceNumber+"  送到exchange了 ");
    // code when message is confirmed
}, (sequenceNumber, multiple) -> {
    System.out.println(sequenceNumber+"  送没了");
    // code when message is nack-ed
});
2.return 退回模式

​ 消息从 exchange–>queue 投递失败则会返回一个 returnCallback 或者ReturnListener (对参数不同)。 都可以通过参数的形式使用addReturnListener方法接收

public interface ReturnCallback {
    void handle(Return returnMessage);
}
public interface ReturnListener {
    void handleReturn(int replyCode,
            String replyText,
            String exchange,
            String routingKey,
            AMQP.BasicProperties properties,
            byte[] body)
        throws IOException;
}
channel.addReturnListener(new ReturnListener() {
    @Override
    public void handleReturn(int replyCode, String replyText, String exchange,
                             String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {

        System.err.println("---------handle  return----------");
        System.err.println("replyCode: " + replyCode);
        System.err.println("replyText: " + replyText);
        System.err.println("exchange: " + exchange);
        System.err.println("routingKey: " + routingKey);
        System.err.println("properties: " + properties);
        System.err.println("body: " + new String(body));
    }
});	

我们将利用这两个 callback 控制消息的可靠性投递

ack应答

是保证消费者在接收到消息做出的反应

ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
通过设置basicConsume方法的autoAck参数来设置自动确认还是手动 (自动的性能高)

手动提交相关的有三个方法

  1. 拒绝 用于程序出错 第一个参数是消息的序列号 第二个参数是是否可以多拒绝 第三个参数是 是否重新排队
long deliveryTag = envelope.getDeliveryTag();
channel.basicNack(deliveryTag, true,true);
  1. 确定接收 第二个参数是 确定所有消息接收

    channel.basicAck(deliveryTag,true);
    
  2. 拒绝接收 第二个参数表示是否重新排队

    channel.basicReject(deliveryTag,true);
    

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从
RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

Acknowledge,确认。 表示消费端收到消息后的确认方式。
通过设置basicConsume方法的autoAck参数来设置自动确认还是手动 (自动的性能高)

手动提交相关的有三个方法

  1. 拒绝 用于程序出错 第一个参数是消息的序列号 第二个参数是是否可以多拒绝 第三个参数是 是否重新排队
long deliveryTag = envelope.getDeliveryTag();
channel.basicNack(deliveryTag, true,true);
  1. 确定接收 第二个参数是 确定所有消息接收

    channel.basicAck(deliveryTag,true);
    
  2. 拒绝接收 第二个参数表示是否重新排队

    channel.basicReject(deliveryTag,true);
    

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从
RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值