RabbitMQ(一)

简介

RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端(语言),存储转发消息


一、RabbitMQ系统架构

在这里插入图片描述
1.绿色图示为交换机,红色图示为消息队列 在服务端称作Broker,由RabbitMQ实现
2.蓝色为生产者和消费者两种类型,为客户端

二、RabbitMQ概念

RabbitMQ两大核心组Exchange和Queue

1.Queue-消息队列

Queue是一个不重复,唯一,名字随机的的缓冲区,应用程序在其权限之内可以自由地创建、共享使用和消费消息队列
durability:持久化 durable:是 transient:否
auto delete:如果是yes,最后一个监听被移除后,这个队列自动删除

2.Exchange-交换机

1.Exchange称作交换器,它接收消息和路由消息,然后将消息发送给消息队列。每个交换器都有独一无二的名字。
2.交换机属性

  • name:交换机名称
  • type:交换机类型 direct、topic、fanout、headers
  • durability:是否需要持久化,true为持久化
  • auto delete:当最后一个绑定到exchange上的队列删除后,自动删除该exchange
  • internal:当前exchange是否用于rabbitmq内部使用,默认false

3.交换机类型

  • direct(直连式交换机) 所有发送到direct exchange的消息被转发到routekey中指定的queue
    在这里插入图片描述

(1)Producer:

public class RabbitMQProducer_Direct {

    private static final String EXCHANGE_NAME = "RABBITMQ_Direct";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        //指定Exchange的Type = "direct"
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        String routingKey1 = "error";
        String message1 = "error infomations....";
        String routingKey2 = "warning";
        String message2 = "warning infomations....";
        String routingKey3 = "info";
        String message3 = "info infomations....";

        //指定消息的路由参数:routingKey,并发送消息
        channel.basicPublish(EXCHANGE_NAME, routingKey1, null, message1.getBytes());
        channel.basicPublish(EXCHANGE_NAME, routingKey2, null, message2.getBytes());
        channel.basicPublish(EXCHANGE_NAME, routingKey3, null, message3.getBytes());

        //发布消息成功提示信息
        System.out.println("RABBITMQ客户端成功发送信息:" +  message1);
        System.out.println("RABBITMQ客户端成功发送信息:" +  message2);
        System.out.println("RABBITMQ客户端成功发送信息:" +  message3);

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

(2)Consumer:
两个Consumer,一个 bindingKey 是”error“;只能收到routing key 是 “error”的消息;
另一个是{“error”, “info”, “warning”},可以收到routing key 是 {“error”, “info”, “warning”}的消息。

//Consumer1
public class RabbitMQConsumer_Direct {

    private static final String EXCHANGE_NAME = "RABBITMQ_Direct";

    private static String bindingKey = "error";

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        String queueName = channel.queueDeclare().getQueue();
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);

        System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
        //RabbitMQConsumer_Topic consumer = new RabbitMQConsumer_Topic();
        QueueingConsumer consumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, consumer);

        while (true){
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            String routingKey =  delivery.getEnvelope().getRoutingKey();
            System.out.println(" Consumer1接收消息: '" + routingKey + "':'" + message + "'");
        }

    }
}
//-------------------------------------------------------------------------------------
//Consumer2
public class RabbitMQConsumer2_Direct {

    private static final String EXCHANGE_NAME = "RABBITMQ_Direct";

    private static String[] bindingKeys = new String[]{"error", "info", "warning"};

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        String queueName = channel.queueDeclare().getQueue();
        System.out.println(">>>>" + queueName);

        //绑定:Exchange与Queue绑定
        for(String bindingKey : bindingKeys){
            channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
        }

        System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
        //RabbitMQConsumer_Topic consumer = new RabbitMQConsumer_Topic();
        QueueingConsumer consumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, consumer);

        while (true){
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            String routingKey =  delivery.getEnvelope().getRoutingKey();
            System.out.println(" Consumer2接收消息: '" + routingKey + "':'" + message + "'");
        }

    }
}
  • topic(主题式交换器) 所有发送到topic exchange的消息被转发到所有关心routekey中指定topic的queue上
    • exchange将routekey和某个topic进行模糊匹配,此时队列需要绑定一个topic
      注意:可以使用通配符模糊匹配
      一个或多个 * 一个
      在这里插入图片描述

(1)Producer:

public class RabbitMQProducer_Topic {

    private static final String EXCHANGE_NAME = "RABBITMQ_Topic";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        //指定Exchange的Type = "Topic"
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        String routingKey1 = "AAA.orange.BBB";
        String message1 = "Q1 infomations....";
        String routingKey2 = "lazy.orange.fox";
        String message2 = "Q1,Q2 infomations....";
        String routingKey3 = "lazy.brown.fox";
        String message3 = "Q2 infomations....";

        //指定消息的路由参数:routingKey,并发送消息
        channel.basicPublish(EXCHANGE_NAME, routingKey1, null, message1.getBytes());
        channel.basicPublish(EXCHANGE_NAME, routingKey2, null, message2.getBytes());
        channel.basicPublish(EXCHANGE_NAME, routingKey3, null, message3.getBytes());

        //发布消息成功提示信息
        System.out.println("RABBITMQ客户端成功发送信息:" +  message1);
        System.out.println("RABBITMQ客户端成功发送信息:" +  message2);
        System.out.println("RABBITMQ客户端成功发送信息:" +  message3);

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


    }
}

(2)Consumer:
Topic模式下模拟了两个Consumer,一个 bindingKey 是”.orange.“,只能收到routing key 匹配“*.orange.”的消息;另一个是{“…rabbit”, “lazy.#”},可以收到routing key 是匹配{“…rabbit”, “lazy.#”}的消息。

//Consumer1:
public class RabbitMQConsumer_Topic {

    private static final String EXCHANGE_NAME = "RABBITMQ_Topic";

    private static String bindingKey = "*.orange.*";

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        //queueName是随机产生的
        String queueName = channel.queueDeclare().getQueue();
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);

        System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");

        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" Consumer1 Received '" + envelope.getRoutingKey() + "':'" + message + "'");
            }
        };
        channel.basicConsume(queueName, true, consumer);

    }
}
//-------------------------------------------------------------------------------------
//Consumer2:
public class RabbitMQConsumer2_Topic {

    private static final String EXCHANGE_NAME = "RABBITMQ_Topic";

    private static String[] bindingKeys = new String[]{"*.*.rabbit", "lazy.#"};

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        String queueName = channel.queueDeclare().getQueue();
        //打印出来的queueName:>>>>amq.gen-QjLNUuPTzIHuaBq-TBL6fQ,是随机产生的
        System.out.println(">>>>" + queueName);

        //绑定:Exchange与Queue绑定
        for(String bindingKey : bindingKeys){
            channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
        }

        System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" Consumer1 Received '" + envelope.getRoutingKey() + "':'" + message + "'");
            }
        };
        channel.basicConsume(queueName, true, consumer);

    }
}
  • Fanout(广播式交换机) 不处理任何路由键,只需要简单的将队列绑定在交换机上

    • 发送到交互机的消息都会被转发到该交换机绑定的所有队列上
    • fanout交换机转发消息是最快的

    (1)produce

public class RabbitMQProducer_Fanout {

    public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";

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

        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        String message = "This message is from Fanout mode.特点是Consumer均可获取到消息";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());

        System.out.println("---【Producer发送消息】" + message + "---" );

        channel.close();;
        connection.close();
    }
}

(2)Consumer:

//Consumer1
public class RabbitMQConsumer_Fanout {

    public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        //获取Queue随机名
        String queueName = channel.queueDeclare().getQueue();
        //Binding:绑定Queue与Exchange,此处没有binding key。
        channel.queueBind(queueName, EXCHANGE_NAME, "");

        System.out.println("---Consumer1 准备接收消息--");
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" 【Consumer1 接收消息 】'" + message + "'");
            }
        };

        channel.basicConsume(queueName, true, consumer);
    }
}
//--------------------------------------------------------------------------------------
//Consumer2
public class RabbitMQConsumer2_Fanout {

    public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        //获取Queue随机名
        String queueName = channel.queueDeclare().getQueue();
        //Binding:绑定Queue与Exchange,此处没有binding key。
        channel.queueBind(queueName, EXCHANGE_NAME, "");

        System.out.println("---Consumer2 准备接收消息--");
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" 【Consumer2 接收消息 】'" + message + "'");
            }
        };

        channel.basicConsume(queueName, true, consumer);
    }
}
  • Headers:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配

3.Routing Key

路由规则,虚拟机可用它来确定如何路由一个特定消息 Queue:也称为Message Queue,消息队列,保存消息并将它们转发给消费者

4.Binding

Exchange和Queue之间的虚拟连接,binding中可以包含routing key( exchange和exchange、queue 之间的连接关系 binding中可以包含routingKey或者参数)

5.Server

又称Broker, 接受客户端的连接,实现AMQP实体服务

6.Connection

连接,应用程序与Broker的网络连接

7.Channel

网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端可建立多个Channel,每个Channel代表一个会话任务。

8.Message

消息,服务器和应用程序之间传送的数据,由Properties和Body组成
服务器和应用程序之间传送的数据
本质上是一段数据,由properties和 payload(body)组成
常用属性:delivery mode、header(自定义属性)
其他属性:content_typea、content_encoding、priority、correlation_id、reply_to、expiration、message_id…

9.Properties

可以对消息进行修饰, 比如消息的优先级、延迟等高级特性; Body则就是消息体内容。

10.Virtual host

虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个virtual host里面可以有若干个exchange和queue。同一个virtual host里面不能有相同名称的exchange和queue。

三、RabbitMQ高级特性

1.消息如何保障100%投递成功

什么是生产端的可靠性投递?

  • 保障消息的成功发出
  • 保障mq节点成功接收
  • 发送端收到mq节点(broker)确认应答
  • 完善的消息进行补偿机制

1.1 生产端-可靠性投递(一)

解决方案:

  • 消息落库,对消息状态进行打标
    在这里插入图片描述BIZ DB (业务数据库) MSG DB(消息数据库)
  • step1业务入库和消息入库
  • step2:step1成功,生产端的Sender进行消息发送(消息状态初始值为0)
  • step3:Broker(Server)收到消息并发送应答给生产端的Confirm Listener
  • step4:Confirm Listener异步监听Broker的应答消息并进行判断
    假设step2通过,在step3回送响应时,网络突然出现了闪断,导致生产端的Listener收不到这条消息的confirm应答,消息的状态始终为0
  • step5:分布式定时任务抓取状态为0的消息
  • step6:将状态为0的消息重发
  • step7如果尝试了3次(可按实际情况修改)以上则将状态置为2(消息投递失败状态)

1.2 生产端-可靠性投递(二)

在第一种情况下,高并发的情境下,数据库的两次写操作和读取操作会存在数据库IO瓶颈

这种是减少对数据库的操作,提高并发量。但不是关心消息是不是能够100%的投递成功.

解决方案:

  • 消息的延迟投递,做二次确认,回调检查

在这里插入图片描述
Upstream service:上游服务,可能为生产端
Downstream service:下游服务,可能为消费端
MQ Broker:可能为集群
Callback service:回调服务,监听confirm消息,独立的服务

  • step1 业务数据入库,成功后生产端发送消息到Broker
  • step2 消息发送成功之后,生产端发送一条延迟消息(Second Send Delay Check),需要设置延迟时间
  • step3 消息队列进行指定队列的监听,对收到的消息进行处理
  • step4 消费端处理完毕之后,发送Confirm(不是ACK)到Broker
  • step5 Callback service是一个单独的服务,其实它扮演了方案一的存储消息的DB角色,它通过Broker去监听消费端发送的Confirm消息,如果收到消息,那么将消息持久化到DB当中.
  • step6 一定延迟时间之后再次发送消息给Broker,然后还是Callback Service去监听延迟消息所对应的队列.收到之后去检查MSG DB中是否有这条消息,如果存在,通过.不存在或者是消毒费失败了,那么Callback Service就需要主动发起RPC通信给上游服务,告诉它延迟投递的这条消息没有找到,需要重新发送,生产端收到信息后就会重新查询业务消息然后将消息发送出去,循环第一步。

2.幂等性概念

2.1 what幂等性

一个操作多次执行产生的结果与一次执行产生的结果一致,比如 sql无论执行多少次,结果都是相同的(借鉴乐观锁,加版本),利用加版本号Version的方式来保证幂等性。

2.2 消费端-幂等性保障

在海量订单产生的业务高峰期,如何避免消息的重复消费问题?

在高并发的情况下,会有大量的消息到达MQ,消费端需要监听大量的消息。这样的情况下,难免会出现消息的重复投递,网络闪断等等。如果不去做幂等,则会出现消息的重复消费。

解决: 消费端实现幂等性,就是我们的消息永远不会消费多次,即使收到多条一样的消息,也只会执行一次

方法

  • 唯一id+指纹码机制
  • 利用数据库主键去重
  • 或者利用redis原子性去实现
2.2.1 唯一ID+指纹码机制
  • 唯一ID +指纹码机制,利用数据库主键去重
  • select count(1) from t_order where id=唯一ID+指纹码
  • 好处:实现简单
  • 坏处:高并发下有数据库写入的性能瓶颈
  • 解决方案:跟进ID进行分库分表进行路由算法
2.2.2 Redis 原子特性实现

最简单使用Redis的自增。

使用Redis进行幂等,需要考虑的问题。

  • 第一:是否需要数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性?
    加事务不行,Redis和数据库的事务不是同一个,无法保证同时成功同时失败。大家有什么更好的方案呢?
  • 第二:如果不进行落库,那么都存储到缓存中,如何设置定时同步的策略?
    怎么做到缓存数据的稳定性?

3. Confirm 确认消息

理解Confirm 消息确认机制:

  • 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
  • 生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递的核心保障!

在这里插入图片描述
蓝色:producer 生产者 红色:MQ Broker 服务器

生产者把消息发送到Broker端,Broker收到消息之后回送给producer。Confirm Listener 监听应答。

操作是异步操作,当生产者发送完消息之后,就不需要管了。Confirm Listener 监听MQ Broker的应答。

3.1 如何实现Confirm 确认消息

第一步:在channel上开启确认模式:channel.confirmSelect()
第二步;在chanel上 添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!

3.2 实现Confirm 确认消息-代码实例

/**
 * 生产者
**/
package com.gao.rabbitmq.confirm;

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

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

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //1。创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        //2获取connection
        Connection connection = connectionFactory.newConnection();
        //3通过connection 创建channel
        Channel channel = connection.createChannel();
        //4.指定消息投递模式:消息确认模式
        channel.confirmSelect();
        String exchangeName="test_confirm_exchange";
        String rouyingKey="confirm.save";
        //5.发送消息
        String msg="hello";
        channel.basicPublish(exchangeName,rouyingKey,null,msg.getBytes());
        //6。添加确认监听
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long l, boolean b) throws IOException {
                System.out.println("=--------- ack-----");
            }

            @Override
            public void handleNack(long l, boolean b) throws IOException {
                System.out.println("=--------no-ack-----");
            }
        });
     }
}

//------------------------------------------------------------
/**
 * 消费者
**/
package com.gao.rabbitmq.confirm;

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

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

public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //1。创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        //2获取connection
        Connection connection = connectionFactory.newConnection();
        //3通过connection 获取channel
        Channel channel = connection.createChannel();

        String exchangeName="test_confirm_exchange";
        String rouyingKey="confirm.#";
        String queueName="test_confirm_queue";
        //4.声明交换机和队列然后进行绑定,指定路由key
        channel.exchangeDeclare(exchangeName,"topic",true);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,rouyingKey);
        //5创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName,true,queueingConsumer);
        while(true){
               Delivery delivery = queueingConsumer.nextDelivery();
               String msg=new String( delivery.getBody()) ;
               System.out.println("消费端"+msg);

        }

    }
}

先启动消费端->再启动生产端

3.3 查看管控台

1.Queues下有一个name为test_confirm_queue的队列
2.Queues页面下拉有一个Bindings绑定的交换机的名字为test_confirm_exchange
3.Exchange页面的Bindings下有具体信息test_confirm_queue的队列名以及
```confirm.#````的路由key

控制台可以观察到消费端先接收到消息,之后生产端再接收到回调信息。如果出现磁盘已满、RabbitMQ出现异常、queue容量到达上限都可能接收到no ack

如果ack和no ack消息都未接收到,这就是之前所说的。RabbitMQ出现网络闪断,可以采用上面所说的消息补偿。

4. Return消息机制

  • Return Listener用于处理一些不可路由的消息!
  • 消息生产者通过指定一个Exchange和Routingkey,把消息送达到某一个队列中去,然后消费者监听队列,进行消费处理操作!
  • 在某些情况下,如果在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果需要监听这种不可达的消息,就要使用Return Listener!

在基础API中有一个关键的配置项:

  • Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息!

4.1 Return消息机制流程

在这里插入图片描述

  • Producer生产端将消息发送到MQ Broker端,但是出现NotFind Exchange,发送的消息的Exchange,在Broker端未能找到。或者找到了,但是路由key路由不到指定的队列。因此是一个错误的消息。
    -这个时候,生产端应该知道发送的这条消息,并不会被处理。因此MQ Broker提供了这种Return机制,将这些不可达的消息发送给生产端,这时候生产端就需要设置Return Listener接收这些不可达的消息。然后及时记录日志,去处理这些消息。

4.2 Return消息机-代码实例

/**
 *生产者
**/
public class Producer {

	
	public static void main(String[] args) throws Exception {
		
		
		//1 创建ConnectionFactory
		Connection connection = ConnectionUtils.getConnection();
		
		Channel channel = connection.createChannel();
		
		String exchange = "test_return_exchange";
		String routingKey = "return.save";
		String routingKeyError = "abc.save";
		
		String msg = "Hello RabbitMQ Return Message";
		
		
		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));
			}
		});
		
		//第三个参数mandatory=true,意味着路由不到的话mq也不会删除消息,false则会自动删除
		channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
		//修改routingkey,测试是否能够收到消息
		//channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
	}
}
//----------------------------------------------------------------------------
/**
 * 消费者
**/
public class Consumer {

	
	public static void main(String[] args) throws Exception {
		
		
		//1 创建ConnectionFactory
		Connection connection = ConnectionUtils.getConnection();
		
		Channel channel = connection.createChannel();
		
		String exchangeName = "test_return_exchange";
		String routingKey = "return.#";
		String queueName = "test_return_queue";
		
		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
		channel.queueDeclare(queueName, true, false, false, null);
		channel.queueBind(queueName, exchangeName, routingKey);
		
		QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
		
		channel.basicConsume(queueName, true, queueingConsumer);
		
		while(true){
			Delivery delivery = queueingConsumer.nextDelivery();
			String msg = new String(delivery.getBody());
			System.err.println("消费者: " + msg);
		}
	}
}
//--------------------------------------------------------------------------
/**
 * 连接工具类
**/
public class ConnectionUtils {
    public static Connection getConnection() throws IOException, TimeoutException {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);//amqp协议 端口 类似与mysql的3306
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("gn");
        factory.setPassword("123456");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

先启动消费端,在启动生产端

4.3 查看管控台

1.Queues下有一个name为test_return_queue的队列
2.Queues页面下拉有一个Bindings绑定的交换机的名字为test_return_exchange
3.Exchange页面的Bindings下有具体信息test_confirm_queue的队列名以及
confirm.#的路由key

打印结果:
1.正常打印:
放开消费端代码:channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
消费端打印结果:消费者: Hello RabbitMQ Return Message
2.非正常打印:
改代码为:channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
打印结果:生产端收到不可达消息
在这里插入图片描述

5. 自定义消费者

5.1 消费端自定义监听

在代码中编写while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理!这种轮询方式不好,可以使用自定义的Consumer更加方便,解耦性更加强,在实际工作中比较常见。

在这里插入图片描述

5.2 代码实例

/**
 * 生产者
**/
public class Producer {

	public static void main(String[] args) throws Exception {
		
		//1 创建ConnectionFactory
		Connection connection = ConnectionUtils.getConnection();
		Channel channel = connection.createChannel();
		
		String exchange = "test_consumer_exchange";
		String routingKey = "consumer.save";
		
		String msg = "Hello RabbitMQ Consumer Message";
		
		for(int i =0; i<5; i ++){
			channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
		}
		
	}
}
//------------------------------------------------------------------------------------
/**
 *消费者
**/
public class Consumer {

	
	public static void main(String[] args) throws Exception {
		
		
		// 创建ConnectionFactory
		Connection connection = ConnectionUtils.getConnection();
		Channel channel = connection.createChannel();
		
		
		String exchangeName = "test_consumer_exchange";
		String routingKey = "consumer.#";
		String queueName = "test_consumer_queue";
		
		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
		channel.queueDeclare(queueName, true, false, false, null);
		channel.queueBind(queueName, exchangeName, routingKey);
		
		//实现自己的MyConsumer()
		channel.basicConsume(queueName, true, new MyConsumer(channel));
	}
}
//------------------------------------------------------------------------------------
/**
 * 自定义类:MyConsumer
**/
public class MyConsumer extends DefaultConsumer {


	public MyConsumer(Channel channel) {
		super(channel);
	}

	//根据需求,重写自己需要的方法。
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
		System.err.println("-----------consume message----------");
		//消费标签
		System.err.println("consumerTag: " + consumerTag);
		//这个对象包含许多关键信息
		System.err.println("envelope: " + envelope);
		System.err.println("properties: " + properties);
		System.err.println("body: " + new String(body));
	}
}
//------------------------------------------------------------------------------------
**
 * 连接工具类
**/
public class ConnectionUtils {
    public static Connection getConnection() throws IOException, TimeoutException {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);//amqp协议 端口 类似与mysql的3306
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("gn");
        factory.setPassword("123456");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

5.3 控制台打印结果

在这里插入图片描述

6.消费端限流

6.1 what 消费端限流

假设一个场景,首先,Rabbitmq服务器有上万条未处理的消息,随便打开一个消费者客户端,会出现下面情况:
巨量的消息瞬间全部推送过来,但是单个客户端无法同时处理这么多数据!这个时候很容易导致服务器崩溃,出现故障。

为什么不在生产端限流?

因为在高并发的情况下,客户量就是非常大,所以很难在生产端做限制。因此我们可以用MQ在消费端做限流

  • RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前不进行消费新的消息。
    在限流的情况下,千万不要设置自动签收,要设置为手动签收
  • void BasicQos(uint prfetchSize,ushort prefetchCount,bool global);

参数解释:

  • prefetchSize:0
  • prefetchCount:会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息没有ack,则该consumerblock掉直到有消息ack
  • global: true\false 是否将上面设置应用于channel,简单点说,就是上面限制是channel级别还是consumer级别。
  • prefetchSize和global这两项,rabbitmq没有实现,暂且不研究prefetch_count在no_ask = false的情况下生效,即在自动应答的情况下这两个值不生效的。

第一个参数(prefetchSize):消息的限制大小,消息多少兆。一般不做限制,设置为0
第二个参数(prefetchCount):一次最多处理多少条,实际工作中设置为1就好
第三个参数(global):限流策略在什么上应用。在RabbitMQ一般有两个应用级别:1.通道 2.Consumer级别。一般设置为false,true 表示channel级别,false表示在consumer级别

6.2 代码实例

/**
 * 生产者
**/
public class Producer {

	
	public static void main(String[] args) throws Exception {
		
		//1 创建ConnectionFactory
		Connection connection = ConnectionUtils.getConnection();
		Channel channel = connection.createChannel();
		
		String exchange = "test_qos_exchange";
		String routingKey = "qos.save";
		
		String msg = "Hello RabbitMQ QOS Message";
		
		for(int i =0; i<5; i ++){
			channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
		}
		
	}
}


//------------------------------------------------------------------------------------
/**
 * 消费者
**/
public class Consumer {

	
	public static void main(String[] args) throws Exception {
		
		
		//1 创建ConnectionFactory
		Connection connection = ConnectionUtils.getConnection();
		Channel channel = connection.createChannel();
		
		
		String exchangeName = "test_qos_exchange";
		String queueName = "test_qos_queue";
		String routingKey = "qos.#";
		
		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
		channel.queueDeclare(queueName, true, false, false, null);
		channel.queueBind(queueName, exchangeName, routingKey);
		
		//1 限流方式  第一件事就是 autoAck设置为 false
		//设置为1,表示一条一条数据处理
		channel.basicQos(0, 1, false);
		
		channel.basicConsume(queueName, false, new MyConsumer(channel));
		
		
	}
}


//------------------------------------------------------------------------------------
/**
 * 自定义类:MyConsumer
**/
public class MyConsumer extends DefaultConsumer {


	private Channel channel ;
	
	public MyConsumer(Channel channel) {
		super(channel);
		this.channel = channel;
	}

	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
		System.err.println("-----------consume message----------");
		System.err.println("consumerTag: " + consumerTag);
		System.err.println("envelope: " + envelope);
		System.err.println("properties: " + properties);
		System.err.println("body: " + new String(body));
		
		//需要做签收,false表示不支持批量签收
		channel.basicAck(envelope.getDeliveryTag(), false);
		
	}


}

//------------------------------------------------------------------------------------
**
 * 连接工具类
**/
public class ConnectionUtils {
    public static Connection getConnection() throws IOException, TimeoutException {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);//amqp协议 端口 类似与mysql的3306
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("gn");
        factory.setPassword("123456");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

6.3 查看管控台

1.先注释掉:channel.basicAck(envelope.getDeliveryTag(), false);然后启动Consumer
查看Exchange,交换机名称为test_qos_exchange
查看Queues,绑定名字为test_qos_exchange的交换机,Routing key 为qos.#
然后启动Producer,们会发现消费端,只收到了一条消息!
在这里插入图片描述
原因:

  • 在consumer中,channel.basicConsume(queueName, false, new MyConsumer(channel));,参数设置为false为手签收
  • 在qos中设置只接受一条消息。如果这一条消息不给Broker Ack应答的话,那么Broker会认为你并没有消费完这一条消息,那么就不会继续发送消息channel.basicQos(0, 1, false);

管控台,unack=1,Ready=4,total=5.
在这里插入图片描述
放开注释channel.basicAck(envelope.getDeliveryTag(), false); 进行消息签收。重启服务。

6.4 控制台打印结果

正常打印5条消息在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值