RabbitMQ实战运用(四)——RabbitMQ的消息模型

RabbitMQ的消息模型

创建vhost

首先先在rabbitmq提供的管理端界面上创建一个vhost便于测试。

图一

(创建vhost,图一)


RabbitMQ实战运用(四)——RabbitMQ的消息模型_第1张图片

(创建vhost,图二)


RabbitMQ实战运用(四)——RabbitMQ的消息模型_第2张图片

(指定vhost的名字,图三)


图四

(vhost的运行状态表格,图四)


通过以上几个步骤就已经创建了一个名为’rabbitmqstudy’的vhost,但该vhost只允许guest账户访问,我们再通过RabbitMQ提供的管理端界面创建一个用户并为它绑定’rabbitmqstudy’这个vhost


创建用户

RabbitMQ实战运用(四)——RabbitMQ的消息模型_第3张图片

(创建用户,图一)


RabbitMQ实战运用(四)——RabbitMQ的消息模型_第4张图片

(用户名、密码与分组,图二)


RabbitMQ实战运用(四)——RabbitMQ的消息模型_第5张图片

(用户与rabbitmqstudy绑定,图三)


在这里插入图片描述

(再次查看vhost状态,图四)


vhostuser添加完成。


导入依赖

<dependency>
    <groupId>com.rabbitmqgroupId>
    <artifactId>amqp-clientartifactId>
    <version>5.7.2version>
dependency>

holle world模型

概念

img

P代表生产者,C代表消费者,中间的红色区域代表队列,队列只受主机内存和磁盘的限制,它本质上是一个大的消息缓冲区。

直连模型队列只能对应一个消费者,是Rabbitmq中最简单的一种模型。


生产消息

获取RabbitMQ的Connection对象:

@Configuration
public class RabbitMQConfig {
    @Bean
    public ConnectionFactory connectionFactory() {
        ConnectionFactory conn = new ConnectionFactory();
        conn.setHost("192.168.95.130");
        conn.setPort(5672);
        conn.setVirtualHost("/rabbitmqstudy");
        conn.setUsername("twz");
        conn.setPassword("123456");
        return conn;
    }

    @Bean
    public Connection connection(ConnectionFactory connectionFactory) throws IOException, TimeoutException {
        return connectionFactory.newConnection();
    }
}
@Autowired
private Connection connection;
@Test
public void testProducer() throws IOException, TimeoutException {
    Channel channel = connection.createChannel(); // 从连接中获取一个信道channel

    // 在channel中声明一个队列
    // param1: 队列名
    // param2: 【队列】是否需要持久化,队列,队列,队列!
    //			如果是false,则当重启rabbitmq服务时,该队列会丢失
    //			如果是true,则队列不会丢失
    //			至于消息是否持久化是在发布消息的api中通过参数设置,这里只是声明队列的性质
    // param3: 是否是独占队列
    //			独占队列意味着该队列只被该channel绑定,不能再被其余的channel绑定
    //			如果绑定会直接抛出错误,在大多数情况下都是false即非独占
    // param4: 队列中的消息被取完后是否删除队列
    //			只有在消费者与队列断开连接之后,该队列才会删除
    // param5: 参数
    channel.queueDeclare("hello", true, false, false, null);

    // 发布消息,必须要先声明队列才能发布消息
    // param1: 交换器,helloword模型中没有交换器
    // param2: 队列名 或 routingKey,这里指定的是队列名
    // param3: 该消息的属性,比如MessageProperties.PERSISTENT_TEXT_PLAIN就是让该消息持久化,消息持久化的时机是rabbitmq关闭
    // param4: 消息体
    channel.basicPublish("", "hello", null, "这是发布的一个消息".getBytes());

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

RabbitMQ实战运用(四)——RabbitMQ的消息模型_第6张图片

在运行完成后可以看到多出来了一个名字是holle的队列,队列处于idle空闲状态,队列中的消息总数是1。


消费消息

@Test
public void testBasicConsumer() throws IOException {
    Channel channel = connection.createChannel();
    
    // 消费消息,消费消息不需要声明队列
    // param1: 队列名
    // param2: 是否开启消息的自动确认机制
    //				获取消息之后,自动确认该消息已被获取,在后台能看到数据
    // param3: 消息回调接口
    channel.basicConsume("hello", true, new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
        							Envelope envelope, 
        							AMQP.BasicProperties properties, 
        							byte[] body) throws IOException {
            System.out.println(new String(body));
        }
    });
}

RabbitMQ实战运用(四)——RabbitMQ的消息模型_第7张图片

可以到消费之后队列中的消息就被全部消费掉了。


面临的问题

由于队列只能对应一个消费者,如果消息处理过慢,可能会导致消息在队列中堆积。


消息确认机制

消息确认机制的目的是防止消息丢失,它的原理和TCP可靠传输原理完全一样。

以下摘至官方文档:

完成一项任务可能只需要几秒钟。您可能想知道,如果其中一个消费者启动了一个很长的任务,并且只完成了部分任务而死亡(消费者会尽可能多拿取任务再依次去处理),会发生什么情况。在我们目前的代码中,一旦RabbitMQ向消费者发送消息,它就会立即标记该消息为删除。在本例中,如果您杀死一个worker,我们将丢失它正在处理的消息。我们还将丢失所有已发送到这个特定工作器但尚未处理的消息。

但我们不想失去任何任务。如果一个工人死了,我们希望把任务交给另一个工人。

为了确保消息不会丢失,RabbitMQ支持消息确认。消费者发送回确认信息,告知RabbitMQ已经接收并处理了特定的消息,RabbitMQ可以删除该消息。

如果一个消费者在没有发送ack的情况下死亡(通道关闭,连接关闭,或者TCP连接丢失),RabbitMQ会理解消息没有被完全处理,并将其重新排队。如果同时有其他消费者在线,它会很快地将它重新发送给另一个消费者。这样你就可以确保没有信息丢失,即使这些工人偶尔会死亡。

当消费者死亡时,RabbitMQ会重新发送消息。即使处理一条消息要花费非常非常长的时间,这也是可以的。

默认情况下,手动消息确认是打开的。在前面的例子中,我们通过autoAck=true标志显式地关闭了它们。是时候将此标志设置为false并在完成任务后从worker发送正确的确认了。

使用手动确认

在consume那里关闭自动确认机制,然后在处理消息的回调函数那里手动确认消息:

// 手动确认消息,将自动确认设置false
channel.basicConsume("hello", false, new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println(new String(body));
            // 手动确认消息
            // param1: 一个标签
            // param2: 是否确认多条消息
            channel.basicAck(envelope.getDeliveryTag(), false);
        }
});

如果关闭了自动确认机制,同时处理消息后又没有手动确认消息,则消息队列中该消息会保留,因为队列会认为该消息没有被消费者安全处理。

消息确认就是一种保障数据可靠传输的安全机制。


work queue模型

概念

img

work queue模型可以让多个消费者绑定到同一个队列,共同消费队列中的消息。队列中的消息一旦被某个消费者消费就会消失,因此确保任务不会重复执行。

work queue模型可以解决上一种模型中面临的问题。即当任务处理比较耗时时,可能生产消息的速度会远大于处理消息的速度,从而导致消息堆积,无法及时处理。

work queue模型中,消费者消费的消息,是平均分配的。默认情况下,RabbitMQ会将每条消息依次发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环。让三个或更多的工人试试这个方法。


生产消息

// 和上述代码没有任何区别,只是连续发布了多条消息而已

消费消息

// 也没有任何区别,只是消费者的数量有两个,两个消费者一起消费同一个队列中的消息

面临的问题

每个消费者会得到相同数量的消息,但得到不是消费,得到是指消费者将消息从队列中取到了,但是还没有使用它。而有些消费者处理消息的可能很慢,而有些消费者处理的快,但是消息是平均分配的,这样就有消费者的资源被浪费掉了。


实现"能者多劳"

work queue模型消费者拿到的消息,是平均的,比如A、B两个消费者,A处理一个消息要3秒,B处理处理一个消息只要一瞬间。但由于A和B拿的消息是一样多,拿到了再去消费,所以会有浪费资源的情况。

而我们可以设置每个消费者每次只能拿到一条消息,这样就能实现能者多劳。

// 消费者默认会尽可能的将队列中的消息多拿,我们可以设置每次只拿一个消息
channel.basicQos(1); // 通道中一次只接受一条未被破解的消息

fanout模型

回顾

在前几个模型中,我们直接面向队列去发送和接收消息。现在是时候介绍Rabbit中的完整消息传递模型了。

让我们快速浏览一下我们在之前的教程中所涵盖的内容:

  • 生产者是发送消息的用户应用程序。
  • 队列是存储消息的缓冲区。
  • 消费者是接收消息的用户应用程序。

概念

RabbitMQ实战运用(四)——RabbitMQ的消息模型_第8张图片

RabbitMQ消息模型的核心思想是:生产者从不直接向队列发送任何消息

实际上,通常情况下,生产者甚至根本不知道消息是否会被传递到哪个队列。它只能将消息发送到交换器。而交换是一件很简单的事情。一方面它接收来自生产者的消息,另一方面它将消息推送到队列。

交换器必须确切地知道如何处理它接收到的消息。它应该被附加到一个特定的队列吗?它应该被附加到许多队列中吗?这些规则由exchange类型定义。

fanout交换非常简单。正如您可能从名称中猜到的那样,它只是将它接收到的所有消息广播到它知道的所有队列。

# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
# 获取信道
channel = connection.channel()
# 为信道声明交换器,名字是log,策略是fanout
channel.exchange_declare(exchange='logs', exchange_type='fanout')

如您所见,在建立连接之后,我们声明了交换器。这个步骤是必要的,因为发布到一个不存在的交换器是被禁止的。如果没有队列绑定到交换器,消息将丢失,但这对我们来说没有问题;如果还没有消费者在听,我们可以安全地丢弃消息。

# 交换器与队列绑定
channel.queue_bind(exchange='logs', queue=queue_name)

交换器与队列的绑定在consumer完成。


生产消息

public void producer() throws Exception {
    Channel channel = connection.createChannel();
    /*
     * 声明交换器
     * param1: 交换器名
     * param2: 交换器策略  fanout 广播
     */
    channel.exchangeDeclare("registry", "fanout");

    /*
     * 有交换器之后,发送消息就会给交换器了
     * 生产方与队列就没有关系了
     * 队列与交换器的绑定将在消费方完成
     */
    channel.basicPublish("registry", "", null, "this is message".getBytes());

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

消费消息

public void consumer() throws Exception {
    Channel channel = connection.createChannel();

	// 声明交换器
    channel.exchangeDeclare("registry", "fanout");

    /*
        由于一个交换器可能对应多个队列,因此直接用API生成一个临时队列
        临时队列在消息消费完后就会自动删除掉

        创建一个临时队列,返回临时队列的标识
    */
    String queue = channel.queueDeclare().getQueue();

    channel.queueBind(queue, "registry", "");

    channel.basicConsume(queue, true, new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println(new String(body));
        }
    });

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

小结

  • 可以有多个消费者。
  • 每个消费者都有自己的queue。
  • 每个队列都要绑定到exchange。
  • 生产者只能将消息发给交换器,交换器自己来决定要发给哪个队列,生产者无法决定。
  • 对于发布订阅模式而言,交换器会广播给所有队列。
  • 队列的所有消费者都能拿到消息,这样能实现一条消息被多个消费者消费

面临的问题

所有与交换器绑定的队列都会收到消息,无法区别对待。


direct模型

回顾

在fanout模型中,一条消息会被所有订阅的队列消费。但是在某些场景下,我们希望不同的消息被不同的队列消费(通过routingkey),这时就要用到direct类型的交换器。

概念

RabbitMQ实战运用(四)——RabbitMQ的消息模型_第9张图片

图解
  • P:生产者,想交换器发送消息时,会指定消息的routingkey。

  • X:交换器,接收了生产者的消息后会将消息递交给与routingkey完全匹配的队列。

  • C1:消费者,其所在队列指定了需要routingkey为error的消息。

  • C2:消费者,其所在队列指定了需要routingkey为info、error、warning的消息。

因此在direct模型下

  • 队列与交换器的绑定,需要为队列指定一个routingkey
  • 消息发送时,需要为消息指定一个routingkey
  • 交换器不在将消息发给每一个与交换器绑定的队列,而是根据routingkey进行判断,将消息发送给routingkey相同的队列。

生产消息

public void producer() throws Exception {
    Channel channel = connection.createChannel();
    // 声明交换器,策略是direct
    channel.exchangeDeclare("routing_exc", "direct");
	// 发布消息,并制定routingkey为info
    channel.basicPublish("routing_exc", "info", null, "this is info msg".getBytes());
    channel.close();
    connection.close();
}

消费消息

public void consumer() throws Exception {
    Channel channel = connection.createChannel();
    // 声明交换器
    channel.exchangeDeclare("routing_exc", "direct");
	// 临时队列
    String queue = channel.queueDeclare().getQueue();
	/*
    	队列与交换器绑定,一个队列可以绑定多个routingkey
     */
    channel.queueBind(queue, "routing_exc", "info");
    channel.queueBind(queue, "routing_exc", "warning");
    channel.queueBind(queue, "routing_exc", "error");
	// 消费消息
    channel.basicConsume(queue, true, new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println(new String(body));
        }
    });
    channel.close();
    connection.close();
}

面临的问题

只能对routingkey进行完全匹配,无法使用通配符,因此direct也称为静态路由。


topic模型

概念

img

topic模型与direct模型类似,它们都可以根据routingkey将消息路由到不同的队列。不同的是topic模型的交换器可以使用通配符。

topic模型的routingkey一般是由一个或多个单词组成,多个单词之间以.分隔,例如:item.insert


rabbitmq的通配符

  1. *: 匹配任意一个单词
  2. #: 匹配任意一个或多个单词

生产消息

public void producer() throws Exception{
    Channel channel = connection.createChannel();
    // 声明交换器,策略是topic
    channel.exchangeDeclare("topic_exc" ,"topic");
	// 发送消息,routingkey是user.save
    channel.basicPublish("topic_exc", "user.save", null, "this is topic msg and routing key is user.save".getBytes());

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

消费消息

public void consumer() throws Exception{
    Channel channel = connection.createChannel();
	// 声明交换器
    channel.exchangeDeclare("topic_exc", "topic");
 	// 临时队列
    String queue = channel.queueDeclare().getQueue();
	// 交换器与队列绑定,routingkey是user.*
    channel.queueBind(queue, "topic_exc", "user.*");
    channel.basicConsume(queue, true, new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println(new String(body));
        }
    });
    channel.close();
    connection.close();
}

面临的问题

完美。

你可能感兴趣的:(MQ,rabbitmq,java,队列)