RabbitMQ详解(一)初识RabbitMQ

什么是MQ:

​ 消息队列(Message Queue,简称MQ),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。
​ 其主要用途:不同进程Process/线程Thread之间通信。

MQ框架非常之多,比较流行的有RabbitMq、kafka、阿里开源的RocketMQ。本文的主角:RabbitMq

特性RabbitMQRocketMQkafka
开发语言erlangjavascala
单机吞吐量万级10万级10万级
时效性us级ms级ms级以内
可用性高(主从架构)非常高(分布式架构)非常高(分布式架构)
功能特性基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富MQ功能比较完备,扩展性佳只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。

RabbitMQ:

后台基本操作:

  1. connection:连接,生产者、消费者、broker之间的物理网络。

  2. channel:消息通道,用于连接生产者和消费者的逻辑结构,一个连接中可以建立多个channel,每个channel代表一个会话任务,通过channel可以隔离同一个连接中的不同交互内容。

  3. exchange:消息交换机,是消息第一个到达的地方,消息通过其指定的路由规则,分发到不同的消息队列(queue)中去。

    exchange的类型:

    1. fanout:广播模式,消息会投递到所有队列。
    2. direct:完全根据key进行投递,如,绑定时设置了routing key为abc,那么客户端发送的消息只有设置了key为abc的才会被投递到队列。
    3. topic:对key进行模式匹配然后再投递,可以使用#匹配一个或多个词,*匹配正好一个词,如,abc.#可以匹配abc.aac.ddd。abc.*只能匹配abc.xxx。
  4. queue:消息队列,消息最终到达的地方,到达queue的消息即进入等待消费状态,一个消息会被发送到一个或多个queue中。

  5. binding:绑定,其作用为把exchange和queue按路由规则绑定起来,也就是exchange和queue之间的虚拟连接。

  6. routing key:路由关键字,exchange根据这个关键字进行消息投递。

  7. virtual host:虚拟主机,一个消息队列服务器的实体中可以存在多个虚拟主机,会存在两个应用程序公用一个服务器实体,为了防止冲突。所以创建两个虚拟主机,每个程序用自己的虚拟主机。

  8. broker:消息队列服务器的实体,它是一个中间件应用负责接收消息,然后把消息发送给消费者或其他的broker。

用户设置:

新增用户:

新增虚拟主机:

将新增用户添加到虚拟主机:

查看结果:

学习5种队列:

P = 生产者

Quere = 队列

X = 交换器

C = 消费者

连接工厂:

public class ConnectionUtil {
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("testhost");
        factory.setUsername("admin");
        factory.setPassword("admin");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}
简单队列:
P
Queue
C
private final static String QUEUE_NAME = "q_test_01";
	// 生产者
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 消息内容
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        //关闭通道和连接
        channel.close();
        connection.close();
    }
----------------------------------------------------------------------------------------------------
    // 消费者
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
        }
    }

我们可以看到这个简单的队列没有交换机,其实系统隐式的绑定了一个默认的交换机.

上图可以看到(Exchange = (AMQP default))。

The default exchange is implicitly bound to every queue, with a routing key equal to the queue name. It is not possible to explicitly bind to, or unbind from the default exchange. It also cannot be deleted.

默认交换机隐式绑定到每个队列,其中路由键等于队列名称。不可能显式绑定到,或从缺省交换中解除绑定。它也不能被删除。

​ 系统会为每个队列都隐式的绑定一个默认的交换机,交换机的名称为“(AMQP default)”,类型为直连接direct,当你手动创建一个队列时,后台会自动将这个队列绑定到一个名称为空的Direct类型交换机上,绑定路由名称与队列名称相同,相当于channel.queueBind(queue:”QUEUE_NAME”, exchange:”(AMQP default)”, routingKey:”QUEUE_NAME”); 所以示例虽然没有显示声明交换机,当路由键和队列名称一样时就将消息发送到这个默认的交换机里。有了这个默认的交换机和绑定,我们就可以像其他轻量级的队列,如Redis那样,直接操作队列来处理消息。不过理论上是可以的,但实际上在RabbitMQ里直接操作是不可取的。消息始终都是先发送到交换机,由交换级经过路由传送给队列,消费者再从队列中获取消息的。不过由于这个默认交换机和路由的关系,使我们只关心队列这一层即可,这个比较适合做一些简单的应用,毕竟没有发挥RabbitMQ的最大功能(RabbitMQ可以重量级消息队列),如果都用这种方式去使用的话就真是杀鸡用宰牛刀了。

work模式:
P
Queue
C1
C2

一个生产者,两个消费者

// 生产者
private final static String QUEUE_NAME = "test_queue_work";
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        for (int i = 0; i < 100; i++) {
            // 消息内容
            String message = "" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep(i * 10);
        }
        channel.close();
        connection.close();
    }
-----------------------------------------------------------------------------------------------------
// 消费者1
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 同一时刻服务器只会发一条消息给消费者
        //channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, true, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [y] Received '" + message + "'");
            //休眠
            Thread.sleep(10);
            // 返回确认状态,注释掉表示使用自动确认模式
            //channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
-----------------------------------------------------------------------------------------------------
// 消费者2
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 同一时刻服务器只会发一条消息给消费者
        //channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, true, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            // 休眠1秒
            Thread.sleep(1000);
            //下面这行注释掉表示使用自动确认模式
            //channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

测试结果:一个消息只能被一个消费者消费,所以每个消费者消费了相同数量的不同的消息。

​ 这样是不合理的,因为消费者1线程停顿的时间短。应该是消费者1要比消费者2获取到的消息多才对。
​ RabbitMQ 默认将消息顺序发送给下一个消费者,这样,每个消费者会得到相同数量的消息。即轮询(round-robin)分发消息。

联合使用 Qos 和 Acknowledge 就可以做到能者多劳,basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。

​ 我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送,还有一点需要注意,**使用公平分发,必须关闭自动应答,改为手动应答。 **

上边代码开启注释的两行代码:

// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
//开启这行 表示使用手动确认模式
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

测试结果:消费者1消费的比消费者2要多。

订阅模式(Fanout):
P
X
Queue1
Queue2
C1
C2

一个生产者,多个消费者。

每个队列都需要绑定(Binding)到交换机上。

每个消费者都有自己的一个队列。

生产者生产消息经过交换机到达队列,可以被多个消费者消费。

	// 生产者
	private final static String EXCHANGE_NAME = "test_exchange_fanout";
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        channel.close();
        connection.close();
    }
---------------------------------------------------------------------------------------------------
	// 消费者1
    private final static String QUEUE_NAME = "test_queue_work1";
    private final static String EXCHANGE_NAME = "test_exchange_fanout";
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
-----------------------------------------------------------------------------------------------------
    // 消费者2
    private final static String QUEUE_NAME = "test_queue_work2";
    private final static String EXCHANGE_NAME = "test_exchange_fanout";
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

测试结果: 同一个消息被多个消费者获取 。 **一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。 **

可以看到test_exchange_fanout交换器关联了test_queue_work1,test_queue_work2两个队列。

路由模式(Direct):
a
a
b
P
X:Type=direct
Queue1
Queue2
C1
C2
// 生产者	
private final static String EXCHANGE_NAME = "direct_exchange";
    public static void main(String[] args) throws Exception {
        //1、获取连接
        Connection connection = getConnection();
        //2、声明信道
        Channel channel = connection.createChannel();
        //3、声明交换器
        // channel.exchangeDeclare(EXCHANGE_NAME, "fanout");//发布订阅模式
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        //4、创建消息
        String message = "hello rabbitmq";
        //5、发布消息
        channel.basicPublish(EXCHANGE_NAME, "add", null, message.getBytes());
        System.out.println("[x] Sent'" + message + "'");
        //6、关闭通道
        channel.close();
        //7、关闭连接
        connection.close();
     }
-----------------------------------------------------------------------------------------------------
    // 消费者1
    private final static String QUEUE_NAME = "q_test_01";
    private final static String EXCHANGE_NAME = "direct_exchange";
    public static void main(String[] args) throws Exception {
        Connection connection = getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定queue 和 exchange 设置routingkey
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"add");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"select");
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
-----------------------------------------------------------------------------------------------------
    // 消费者2
    private final static String QUEUE_NAME = "q_test_02";
    private final static String EXCHANGE_NAME = "direct_exchange";
    public static void main(String[] args) throws Exception {
        Connection connection = getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定queue 和 exchange 设置routingkey
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(1000);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

测试结果:只有消费者1会消费。

​ 因为在队列会通过一个Routing Key和交换机绑定,生产者生产的消息头里会含有Routing Key,交换机会根据消息头的Key匹配队列进行投递

主题模式(Topic):
a.#
#.a.#
*.*.rabbit
P
X:Type=topic
Queue1
Queue2
C1
C2

​ 主题模式也叫通配符模式,和Direct模式类似。他的Routing Key是可以模糊匹配的,#匹配一个或者多个字符,*匹配一个字符。

同一个消息被多个消费者获取。**一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。 **
// 生产者	
private final static String EXCHANGE_NAME = "direct_exchange";
    public static void main(String[] args) throws Exception {
        //1、获取连接
        Connection connection = getConnection();
        //2、声明信道
        Channel channel = connection.createChannel();
        //3、声明交换器
        // channel.exchangeDeclare(EXCHANGE_NAME, "fanout");//发布订阅模式
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        //4、创建消息
        String message = "hello rabbitmq";
        //5、发布消息
        channel.basicPublish(EXCHANGE_NAME, "delete.a.b", null, message.getBytes());
        System.out.println("[x] Sent'" + message + "'");
        //6、关闭通道
        channel.close();
        //7、关闭连接
        connection.close();
     }
-----------------------------------------------------------------------------------------------------
    // 消费者1
    private final static String QUEUE_NAME = "q_test_01";
    private final static String EXCHANGE_NAME = "direct_exchange";
    public static void main(String[] args) throws Exception {
        Connection connection = getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定queue 和 exchange 设置routingkey
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update.*");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"add.*");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete.*");
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
-----------------------------------------------------------------------------------------------------
    // 消费者2
    private final static String QUEUE_NAME = "q_test_02";
    private final static String EXCHANGE_NAME = "direct_exchange";
    public static void main(String[] args) throws Exception {
        Connection connection = getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定queue 和 exchange 设置routingkey
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update.*");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete.#");
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(1000);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

测试结果:只有消费者2消费。因为消息头的Key = delete.a.b,消费者1Key = delete.*,消费者2Key = delete.#。消费者2的可以匹配delete后多个字符,消费者1只能匹配一个字符。

​ queueDeclare的参数:第一个参数表示队列名称、第二个参数为是否持久化(true表示是,队列将在服务器重启时生存)、第三个参数为是否是独占队列(创建者可以使用的私有队列,断开后自动删除)、第四个参数为当所有消费者客户端连接断开时是否自动删除队列、第五个参数为队列的其他参数

​ basicConsume的第二个参数autoAck: 应答模式,true:自动应答,即消费者获取到消息,该消息就会从队列中删除掉,false:手动应答,当从队列中取出消息后,需要程序员手动调用方法应答,如果没有应答,该消息还会再放进队列中,就会出现该消息一直没有被消费掉的现象

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值