RabbitMQ学习笔记(持续更新ing)

·  阅读 63

快速入门(java)

  1. 首先安装rabbitmq(单机版)

    rabbitmq的安装(官网文档)

    在我自己租的云服务器上,直接用docker进行安装(一行命令搞定)

    docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.9-management
    复制代码

    然后在阿里云的控制台,放开567215672端口

    随后,可以直接登录rabbitmq的管理后台http://127.0.0.1:15672,便能看到rabbitmq的情况

    rabbit会创建一个默认的用户,用户名guest,密码guest

  2. 基于java编写一个简单的生产者和消费者

    rabbitmq的java教程(官网文档)

    创建一个简单的maven项目,引入rabbitmq的java依赖包

    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.9.0</version>
    </dependency>
    复制代码

    把rabbitmq的相关信息放在一个常量类中

    package com.yogurt.demo.rabbit;
    
    /**
     * @Author yogurtzzz
     * @Date 2021/12/14 9:42
     **/
    public class Constants {
    
    	private Constants() { }
    
    	public static final String RABBIT_IP = "127.0.0.1";
    
    	public static final int RABBIT_PORT = 5672;
    
    	public static final String RABBIT_USER = "guest";
    
    	public static final String RABBIT_PASSWORD = "guest";
    
    	public static final String RABBIT_QUEUE_NAME = "hello";
    }
    
    复制代码

    编写一个生产者,负责推送消息到rabbit

    package com.yogurt.demo.rabbit;
    
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.nio.charset.StandardCharsets;
    
    import static com.yogurt.demo.rabbit.Constants.*;
    
    public class Send {
    
    	public static void main(String[] argv) throws Exception {
            // 连接工厂
    		ConnectionFactory factory = new ConnectionFactory();
    		// 设置连接信息, ip, 端口号, 账号, 密码
            factory.setHost(RABBIT_IP);
    		factory.setPort(RABBIT_PORT);
    		factory.setUsername(RABBIT_USER);
    		factory.setPassword(RABBIT_PASSWORD);
    		// 创建连接, 发送消息 (使用try-with-resource)
    		try (Connection connection = factory.newConnection()) {
    				String message = "Hello Rabbit";
    				Channel channel = connection.createChannel();
                    //如果该名称的队列不存在, 则新建一个
    				channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
    				// 向该队列发送一条消息	
                channel.basicPublish("", RABBIT_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
    				System.out.println(" [x] Sent '" + message + "'");
    		}
    	}
    }
    复制代码

    跑起来!

    然后我们登录管理页面看看

    可以看到名为hello的队列中,有1条消息,我们可以点击队列的名称,然后点击Get Messages,获取队列中的消息,可以看到这条消息的内容是Hello Rabbit

    说明消息成功发送到rabbitmq当中了

    随后,我们编写一个消费者

    package com.yogurt.demo.rabbit;
    
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import com.rabbitmq.client.DeliverCallback;
    
    import java.nio.charset.StandardCharsets;
    import static com.yogurt.demo.rabbit.Constants.*;
    
    /**
     * @Author yogurtzzz
     * @Date 2021/12/14 9:42
     **/
    public class Recv {
    
    	public static void main(String[] args) {
    		ConnectionFactory factory = new ConnectionFactory();
    		factory.setHost(RABBIT_IP);
    		factory.setPort(RABBIT_PORT);
    		factory.setUsername(RABBIT_USER);
    		factory.setPassword(RABBIT_PASSWORD);
    
    		// 获取连接
    		Connection connection = null;
    		try {
    			connection = factory.newConnection();
    			Channel channel = connection.createChannel();
    			channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
    			System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
    
    			DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    				String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
    				System.out.println(" [x] Received '" + message + "'");
    			};
    			channel.basicConsume(RABBIT_QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    复制代码

    跑起来!

    消费者成功消费到了

上面的示例就是一个最基本的模型,只有一个生产者,一个队列,一个消费者。

下面演示一个生产者,多个消费者的情况

这是一种竞争消费的模式,在一个队列上,绑定了多个消费者,消费者会争抢着消费消息。

生产者

package com.yogurt.demo.rabbit;

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

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

import static com.yogurt.demo.rabbit.Constants.*;

public class Send {


	public static void main(String[] argv) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(RABBIT_IP);
		factory.setPort(RABBIT_PORT);
		factory.setUsername(RABBIT_USER);
		factory.setPassword(RABBIT_PASSWORD);
		// 获取连接, 发送消息
		try (Connection connection = factory.newConnection()) {
			// 从控制台读入
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
			while (true) {
				String message = reader.readLine();
                // 输入 -1 则表示退出
				if ("-1".equals(message)) return;
				Channel channel = connection.createChannel();
				channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
				channel.basicPublish("", RABBIT_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
				System.out.println(" [x] Sent '" + message + "'");
			}
		}
	}
}
复制代码

消费者

package com.yogurt.demo.rabbit;

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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static com.yogurt.demo.rabbit.Constants.*;

/**
 * @Author yogurtzzz
 * @Date 2021/12/14 9:42
 **/
public class Recv implements Runnable{

	@Override
	public void run() {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(RABBIT_IP);
		factory.setPort(RABBIT_PORT);
		factory.setUsername(RABBIT_USER);
		factory.setPassword(RABBIT_PASSWORD);

		long threadId = Thread.currentThread().getId();
		// 获取连接
		Connection connection = null;
		try {
			connection = factory.newConnection();
			Channel channel = connection.createChannel();
			channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
			System.out.println("Thread " + threadId + " [*] Waiting for messages. To exit press CTRL+C");

			DeliverCallback deliverCallback = (consumerTag, delivery) -> {
				String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
				System.out.println("Thread " + threadId + " [x] Received '" + message + "'");
			};
			channel.basicConsume(RABBIT_QUEUE_NAME, true, deliverCallback, consumerTag -> { });
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws IOException {
		Runnable runnable = new Recv();
		// 启动5个消费者
		for (int i = 0; i < 5; i++) {
			new Thread(runnable).start();
		}
		// stuck here
		System.in.read();
	}
}

复制代码

先启动5个消费者

可以在管理后台看到现在有5个连接

再启动生产者,并在控制台输入一些信息

可以看到发送到rabbitmq的三条消息,成功被消费者消费(5个消费者争抢着消费,一条消息只会被一个消费者消费,此种模式下,rabbitmq会依次将消息推送给消费者,根据下图可以观察到,消费者的启动顺序为15,16,13,14,12。rabbitmq也按照这个顺序(轮询,Round-Robin)依次把消息交给对应的消费者进行消费)

快速入门(springboot)

上面介绍的是基于java的简单教程,但是通常我们开发一个应用,会使用到框架,其中又以springboot为代表。下面介绍rabbitmq整合springboot的基本使用

  1. 创建一个springboot项目

  2. pom.xml中添加如下依赖

    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    复制代码
  3. application.yml中配置rabbitmq的地址等

    spring:
      application:
        name: rabbitmq-demo
      rabbitmq:
        host: 127.0.0.1
        port: 5672
        username: yogurt
        password: yogurt
        virtual-host: /test
    复制代码
  4. 添加配置类,配置队列,consumer工厂,消息转换器等

    package com.demo.rabbitmq.config;
    
    import org.springframework.amqp.core.AcknowledgeMode;
    import org.springframework.amqp.core.Queue;
    import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
    import org.springframework.amqp.rabbit.connection.ConnectionFactory;
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RabbitMqConfig {
    
        /**
        * 注册一个 MessageConverter, 发送消息时可以直接发送一个POJO
        **/
    	@Bean
    	public MessageConverter messageConverter() {
    		return new Jackson2JsonMessageConverter();
    	}
    
    	/**
    	 * 新建一个队列, 队列名为 yogurt
    	 * **/
    	@Bean
    	public Queue yogurt() {
    		return new Queue("yogurt");
    	}
    
        /**
        * 配置consumer工厂
        * **/
    	@Bean
    	public SimpleRabbitListenerContainerFactory consumerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
    	                                                            ConnectionFactory connectionFactory) {
    		SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    		// consumer 的 prefetch 设置
            factory.setPrefetchCount(30);
    		// 并发配置 - 同时开启5个消费者(5个线程)
    		factory.setConcurrentConsumers(5);
            // 最大并发配置 (当消息堆积时, 会新开线程来处理, 最大能到20个)
            // 有点类似jdk的线程池
    		factory.setMaxConcurrentConsumers(20);
            // 消费者开启 手动ack 机制
    		factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
            // 接收消息时, 可以直接将消息反序列化为 POJO
    		factory.setMessageConverter(new Jackson2JsonMessageConverter());
    		configurer.configure(factory, connectionFactory);
    		return factory;
    	}
    }
    复制代码
  5. 定义一个POJO,表示发送到rabbitmq的消息

    public class UserInfo implements Serializable {
    
    	private String name;
    
    	private Integer age;
    
    	private String career;
    
    	private String gender;
    
    	private String hometown;
        
        // 省略了构造函数和 getter/setter
    }
    复制代码
  6. 创建生产者

    package com.demo.rabbitmq.component;
    
    import com.demo.rabbitmq.data.UserInfo;
    import org.springframework.amqp.core.AmqpTemplate;
    import org.springframework.amqp.core.Queue;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Profile;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    /**
     * @Author yogurtzzz
     * @Date 2021/12/15 14:55
     **/
    @Profile("sender")
    @Component
    public class RabbitMqSender {
    
    	private int cnt = 0;
    
        // 由 rabbitmq-starter 自动注册进来的, 其实现目前只有1个  RabbitTemplate 
        // 但为了依赖于接口, 最好用 AmqpTemplate 来接收
    	@Autowired
    	private AmqpTemplate template;
    
        // 这里的 Queue 就是前面配置的名称为 yogurt 的队列
    	@Autowired
    	private Queue queue;
    
    
    	/**
    	 * 每4秒发送一条消息
    	 * */
    	@Scheduled(fixedRate = 5000, initialDelay = 2000)
    	public void send() {
    		cnt++;
    		UserInfo info = new UserInfo("yogurt-" + cnt, 26, "Software Engineer", "Male", "China");
    		// 发送一个 UserInfo 对象到 rabbitmq
            template.convertAndSend(queue.getName(), info);
    		System.out.println("[x] Sent");
    	}
    }
    
    复制代码
  7. 创建消费者

    package com.demo.rabbitmq.component;
    
    import com.demo.rabbitmq.data.UserInfo;
    import com.rabbitmq.client.Channel;
    import org.springframework.amqp.rabbit.annotation.RabbitHandler;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.amqp.support.AmqpHeaders;
    import org.springframework.context.annotation.Profile;
    import org.springframework.messaging.handler.annotation.Headers;
    import org.springframework.messaging.handler.annotation.Payload;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author yogurtzzz
     * @Date 2021/12/15 15:02
     **/
    @Component
    @Profile("receiver")
    public class RabbitMqReceiver {
    
        // 指定要监听的队列名称, 以及消费者的 factory
    	@RabbitListener(queues = "yogurt", containerFactory = "consumerFactory")
    	@RabbitHandler
    	public void receive(@Payload UserInfo info, @Headers Map<String, Object> headers, Channel channel) throws InterruptedException, IOException {
    		long id = Thread.currentThread().getId();
    		System.out.println("Consumer " + id + " has received message : " + info + "");
    		System.out.println("handling...");
    		// 模拟处理... 
            TimeUnit.SECONDS.sleep(5);
            // 获取消息的 deliveryTag
    		Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
            // 手动ack
    		channel.basicAck(deliveryTag, false);
    		System.out.println("Consumer " + id + " finished handle");
    	}
    }
    复制代码
  8. 由于用到了 @Scheduled注解,注意在springboot启动类上加上 @EnableScheduling

进阶

消息确认机制

mq通常被用来进行系统解耦,限流削峰等。若mq中的一条消息,对应了一个耗时任务。那么当一个消费者获取到一条消息后,会执行该耗时任务,如果消费者拿到该任务,但是在执行过程中出错了,或者该消费者宕机了,那该耗时任务实际就没有被执行成功,也就是该消息实际是丢失掉了

为了防止消息丢失,rabbitmq提供了一种消息确认机制,即当rabbbitmq把某条消息推送给某消费者后,还需要该消费者返回一个ack信号给到rabbitmq,随后rabbitmq才会将该消息安全的删除掉。若rabbitmq将某条消息推送给某消费者,该消费者还没有返回ack信号(有一个超时时间的配置,Delivery Acknowledgement Timeout,默认是30分钟),消费者和rabbitmq的连接就断掉了,那么rabbitmq会将该条消息重新推送给另外的消费者进行处理

当消费者发送一个ack信号给rabbitmq,就是告诉rabbitmq,某个特定的消息已经被接收并处理完毕,rabbitmq可以删除该条消息了。

特别注意:某条消息的ack信号,必须由接收该条消息的channel发出。若尝试使用不同的channel发送ack信号, 则会报异常(channel-level protocol exception)。

我们可以在消费者中通过让线程休眠的方式,来模拟耗时任务的处理,修改消费者的代码如下

package com.yogurt.demo.rabbit;

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

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

import static com.yogurt.demo.rabbit.Constants.*;

/**
 * @Author yogurtzzz
 * @Date 2021/12/14 9:42
 **/
public class Recv implements Runnable{

	@Override
	public void run() {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(RABBIT_IP);
		factory.setPort(RABBIT_PORT);
		factory.setUsername(RABBIT_USER);
		factory.setPassword(RABBIT_PASSWORD);

		long threadId = Thread.currentThread().getId();
		// 获取连接
		Connection connection = null;
		try {
			connection = factory.newConnection();
			Channel channel = connection.createChannel();
			channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
			System.out.println("Thread " + threadId + " [*] Waiting for messages. To exit press CTRL+C");

            // 创建一个callback, 处理消息回调
			DeliverCallback deliverCallback = (consumerTag, delivery) -> {
				String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
				System.out.println("Thread " + threadId + " [x] Received '" + message + "'");
				try {
                    // 模拟耗时任务的处理
					TimeUnit.SECONDS.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
                    // 发送ack信号给rabbitmq
                    // 第一个参数可以认为是该消息的唯一id, 用来表示需要确认的是哪条消息
					channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
				}
			};
            
			// 第二个参数是 autoAck, 之前我们设置的是 true , 表示消息被消费后, 立刻返回确认
            // 现在改为 false, 则需要消费者手动发送 ack 信号给 rabbitmq
            // 开始消费
			channel.basicConsume(RABBIT_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws IOException {
		Runnable runnable = new Recv();
		// 启动5个消费者
		for (int i = 0; i < 5; i++) {
			new Thread(runnable).start();
		}
		// stuck here
		System.in.read();
	}
}

复制代码

现在我们启动生产者,随便发送几条消息

进入管理后台,看到有3条信息仍然是unacked状态

等待一会儿后,会发现unacked的消息变为了0

小节:为了防止消息丢失,rabbitmq提供了ack,消息确认机制。注意某条消息的ack信号,必须通过接收该消息的相同channel发送回去。

消息持久化

上面的消息确认机制,保证了在消费者异常(宕机等)时,消息不会丢失;但如果rabbitmq服务自身宕机了呢?

这就涉及到另一个机制,消息持久化(Message Durability)

若rabbitmq退出或者崩溃了,它会丢失所有的队列(queues)和消息(messages),除非我们显式地将队列消息标记为可持久化(durable)。

boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
复制代码

上面这行代码本身没错,但是在我们先前的场景下不会起作用。因为先前我们已经创建了一个名为hello的队列(并且没有声明为durable),rabbitmq不允许对已存在的队列进行重新定义。

所以,我们可以创建一个不同名称的新的队列,并标记为durable

boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
复制代码

至此,我们保证了队列已经是可持久化的了。我们还需保证消息也是可持久化的。

在生产者push一条消息到队列里时,标记该消息为持久化即可,如下

import com.rabbitmq.client.MessageProperties;

// PERSISTENT_TEXT_PLAIN 标记该消息为持久化消息
channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());
复制代码

将某条消息标记为持久化,并不能100%保证该消息就一定不会丢失。

从rabbitmq接收到一条消息,到rabbitmq将该消息持久化,中间仍然有一段很短的时间窗口(若rabbitmq在这个时间窗口内宕机,该消息仍然会丢失)。

rabbitmq并不是对每条消息都会用fsync(2)进行刷盘,消息可能仅仅是存在了cache中,但并没有实际写入disk

如果需要对消息持久化,提供更强的保证,可以使用publisher confirms机制(publisher confirms - 官网文档

消息公平分发

考虑这样一个场景,有2个消费者监听同一个队列。这个队列中,偶数位置的消息非常重量,奇数位置的消息非常轻量

由于rabbitmq默认采用round-robin的轮询策略。则会固定将奇数位置的消息,分发给消费者C1,而将偶数位置的消息,分发给消费者C2

由于偶数位置的消息非常重量,需要较多处理时间,而奇数位置的消息比较轻量,只需要较少处理时间。这就会导致,消费者C2始终处于busy状态(甚至很多消息堆积着等待C2处理),而C1则非常空闲。

这是因为rabbitmq在进行消息分发时,不会检查某个消费者的unacked的消息数量(如果检查这个信息的话,就能得知某个消费者很忙,堆积了很多消息没处理完毕),它仅仅依照round-robin进行消息分发。

为了更加公平地进行消息分发,可以使用basicQos方法,设置prefetchCount=1

int prefetchCount = 1;
channel.basicQos(prefetchCount);
复制代码

这告诉了rabbitmq,每次不要分发超过1条消息给一个consumer。换句话说,当某个consumer正在处理某条消息,并且还没有返回ack时,rabbitmq就不会将消息分发给该consumer,而会将该消息分发给下一个空闲的consumer

上面只是简单的演示代码,为了简化,省略了很多内容,上面的示例代码,不应该用于正式环境。仍然需要了解rabbitmq的其他内容,其中,推荐下面的几个文档:

Publisher Confirms and Consumer Acknowledgements

Production Checklist

Monitoring

消息安全性

Consumer Acknowledgements & Publisher Confirms

上述2者都是为了确保data safety

其中,Publisher Confirms,确保了从publishersrabbitmq节点的消息可靠投递;

Consumer Acknowledgements 确保了从rabbitmq节点到consumers的消息可靠投递。

一次消息的投递(delivery),通过delivery tag进行唯一标识。当rabbitmq把一条消息推送给一个consumer时,会同时带上这条消息的delivery tag。这个delivery tag,在当前这个channel上,是唯一的,且delivery tag是单调递增的,由于其是一个64位的long型整数,所以其最大值为9223372036854775807

故,delivery tag的作用范围是每个channel

由于delivery tag的作用范围是channel,所以,某个deliveryack信号,必须在同一个channel上发送。

Because delivery tags are scoped per channel, deliveries must be acknowledged on the same channel they were received on. Acknowledging on a different channel will result in an "unknown delivery tag" protocol exception and close the channel.

rabbitmq可以在发出一个消息后立刻认为该消息被ack了(自动),也可以通过consumer手动返回ack信号(手动),手动返回的ack信号,可以有如下几种(协议中的方法)

  • basic.ack:告诉rabbitmq这条信息被成功处理了,rabbitmq可以将其丢弃
  • basic.nack
  • basic.reject

自动ack的使用场景

Another thing that's important to consider when using automatic acknowledgement mode is consumer overload. Manual acknowledgement mode is typically used with a bounded channel prefetch which limits the number of outstanding ("in progress") deliveries on a channel. With automatic acknowledgements, however, there is no such limit by definition. Consumers therefore can be overwhelmed by the rate of deliveries, potentially accumulating a backlog in memory and running out of heap or getting their process terminated by the OS

手动ack可以支持批量操作(一次性ack多个deliveries),以减少网络开销。

// 第二个参数 mutile 置为 true, 
channel.basicAck(deliveryTag, true);
复制代码

若在某个channel上,有4个还没有被ackdelivery,他们的delivery tag分别是,5,6,7,8,若8的这个delivery到达,并且准备调用ack,并将multiple置为true,则会一次性ack 8之前的全部delivery,即5,6,7,8都会被ack

若是否定的ack,则rabbitmq可以选择丢弃,也可以选择重新入队,这个行为是通过属性requeue来控制的。

boolean requeue = false;
channel.basicReject(deliveryTag, requeue); // basicReject / basicNack
//否定的ack, 并且该消息不会被requeue
复制代码

Prefetch

Channel Prefetch Settings

Because messages are sent (pushed) to clients asynchronously, there is usually more than one message "in flight" on a channel at any given moment. In addition, manual acknowledgements from clients are also inherently asynchronous in nature. So there's a sliding window of delivery tags that are unacknowledged

The value defines the max number of unacknowledged deliveries that are permitted on a channel

Once the number reaches the configured count, RabbitMQ will stop delivering more messages on the channel unless at least one of the outstanding ones is acknowledged. (A value of 0 is treated as infinite, allowing any number of unacknowledged messages.)

The QoS setting can be configured for a specific channel or a specific consumer. The Consumer Prefetch guide explains the effects of this scoping

使用basic.qos来设置prefetch属性(这个prefetch属性可以针对channel,或者针对consumer来进行设置)

channel.basicQos(10); // Per consumer limit
channel.basicQos(10, false); // Per consumer limit
channel.basicQos(15, true);  // Per channel limit

复制代码

prefetch属性对consumer吞吐量的影响

Acknowledgement mode and QoS prefetch value have significant effect on consumer throughput. In general, increasing prefetch will improve the rate of message delivery to consumers. Automatic acknowledgement mode yields best possible rate of delivery.

However, in both cases the number of delivered but not-yet-processed messages will also increase, thus increasing consumer RAM consumption.

如果增大了prefetch的值,或者开启了自动ack机制(自动ack机制开启后,prefetch没有限制,相当于无限大),如果在consumer端堆积了很多已接收但还未被处理的消息,会很吃consumer的内存。

所以,自动ack机制,或,手动ack+无限制prefetch,者两种模式都应该谨慎采用

Values in the 100 through 300 range usually offer optimal throughput and do not run significant risk of overwhelming consumers

100-300的配置,通常能提供较为优秀的吞吐量表现。

小节:

  • prefetch设置可以是per consumer,也可以是per channel
  • prefetch设为0表示无限制
  • 无限制的prefetch可能会导致consumer端消息堆积,而非常消耗consumer的内存
  • prefetch通常设为100到300的区间,能够使得consumer取得较好的吞吐量表现
  • prefetch设为1是最保守的,会显著的降低吞吐量

当设置了手动ack,并且某个consumer宕机或者连接丢失了,会自动将该消息重新入队,requeue。该消息随后会被重新投递(redelivery),并且在头信息中,会有一个布尔属性redelivery=true

注意,考虑这样一个场景,若某个consumer消费并成功处理完一条消息,在返回ack信号时,由于网络抖动,rabbitmq没有收到这个ack,则rabbitmq进行requeue,这条消息随后可能会被另外的consumer重新消费到。此时会导致消息重复消费。需要注意在consumer端通过一定的策略来保证消息消费的幂等性。

其他消息模式

发布订阅模型

在上面的介绍中,我们都是先创建一个queue,然后每条消息都会被投递给某一个consumer

现在,我们介绍发布-订阅模式pub/sub,或publish/subscribe)。在这种模式下,一条消息可以被投递给多个consumer

设想这样一个场景,我们准备构建一个日志系统。其中包含两类程序,第一类为生产者,它会发送日志信息到rabbitmq,第二类为消费者,它会接收日志信息。

在日志系统中,消费者程序有2个实例,其一会直接将日志持久化到磁盘,其二会将日志直接输出到屏幕,方便观察。也就是说,同一条消息,会被2个消费者重复消费。

本质上来讲,由生产者推送到mq的日志信息,会被广播全部的消费者。这就是发布-订阅模型

在前面,我们只介绍了如何向一个队列发送消息,或从一个队列接收消息。现在,我们将介绍rabbitmq中完整的消息模型

我们先回顾一下先前的模型中,所包含的组件

  • 生产者producer):一个用户程序,负责发送消息

  • 队列queue):一个缓冲区(buffer),负责存储消息

    关于queue的官方文档

  • 消费者consumer):一个用户程序,负责接收消息

Rabbitmq中一个核心的概念是:producer发送消息时,其实不是直接发送给某个queue的。

实际上,producer对任何queue都一无所知。producer只能把消息发送给一个交换机exchange

exchange是个很简单的东西,一方面,它从producer那里接收消息;另一方面,它将消息推送到queues

exchangeproducer那里接收到一条消息后,它需要决定怎么做,比如

  • 将这条消息追加到某个特定的queue
  • 将这条消息追加到多个queue
  • 将这条消息丢弃
  • ....

根据上述的针对消息的不同处理方式,将exchange分为了不同的类型,这种类型称为exchange type

有如下几种的exchange type

  • direct
  • topic
  • headers
  • fanout

下面我们将特别关注最后一个exchange type,即fanout

这个fanout也非常的简单,从其名称就能看出来,它做的仅仅是,将它从producer那里收到的消息,广播给所有它知道的全部queue。而这种类型的exchange,恰好是我们前面讨论的发布-订阅模型所需要的。

在先前的教程中,我们对exchange都没有感知,但仍然能够向某个特定的queue发送消息,这是因为我们拥有一个默认的exchange,这个exchange用空字符串""来标识,回想我们前面快速入门中的生产者的代码

// 第一个参数就是exchange的名称, 前面的教程中我们直接将其设为了空字符串, 表示使用默认exchange
channel.basicPublish("", RABBIT_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
复制代码

这个默认exchange,会直接将消息路由给指定名称的队列(由routeKey标识)

当我们创建了几个queue,以及一个fanout类型的exchange后,我们需要告诉exchange,将消息发送到哪些queue。此时我们需要把queueexchange进行绑定binding)。

channel.queueBind("queue_name", "exchange_name", "routing_key");
复制代码

如果某个exchange没有绑定任何的queue,则发送到该exchange的消息,将会丢失。

下面的示例代码,演示了exchange的使用

rabbitmq配置信息:

package com.demo.rabbitmq.simple;

/**
 * @Author yogurtzzz
 * @Date 2021/12/14 9:42
 **/
public class Constants {

	private Constants() { }

	public static final String RABBIT_IP = "127.0.0.1";

	public static final int RABBIT_PORT = 5672;

	public static final String RABBIT_USER = "yogurt";

	public static final String RABBIT_PASSWORD = "yogurt";

	public static final String RABBIT_VIRTUAL_HOST = "/test";

	public static final String RABBIT_QUEUE_NAME = "hello";

	public static final String EXCHANGE_NAME = "logs";
}

复制代码

生产者:

package com.demo.rabbitmq.fanout;

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

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

import static com.demo.rabbitmq.simple.Constants.*;

/**
 * @Author yogurtzzz
 * @Date 2021/12/16 10:36
 **/
public class LogEmitter {

	public static void main(String[] args) throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(RABBIT_IP);
		connectionFactory.setPort(RABBIT_PORT);
		connectionFactory.setUsername(RABBIT_USER);
		connectionFactory.setPassword(RABBIT_PASSWORD);
		connectionFactory.setVirtualHost(RABBIT_VIRTUAL_HOST);

		try(Connection connection = connectionFactory.newConnection()) {
			Channel channel = connection.createChannel();
			// 新建一个 exchange
			channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            // 从控制台读取输入
			BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
			String message;
			while(!"-1".equals(message = reader.readLine())) {
				channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
			}
		}
	}
}
复制代码

消费者:

package com.demo.rabbitmq.fanout;

import com.rabbitmq.client.*;
import com.rabbitmq.client.impl.ChannelN;

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

import static com.demo.rabbitmq.simple.Constants.*;
import static com.demo.rabbitmq.simple.Constants.RABBIT_VIRTUAL_HOST;

/**
 * @Author yogurtzzz
 * @Date 2021/12/16 10:54
 **/
public class LogReceiver {

	public static void main(String[] args) throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(RABBIT_IP);
		connectionFactory.setPort(RABBIT_PORT);
		connectionFactory.setUsername(RABBIT_USER);
		connectionFactory.setPassword(RABBIT_PASSWORD);
		connectionFactory.setVirtualHost(RABBIT_VIRTUAL_HOST);

		Connection connection = connectionFactory.newConnection();

		Channel channel = connection.createChannel();

		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

		// 创建一个 临时queue (默认是 autoDelete, exclusive 的)
		// 即, 这个临时queue, 默认是被当前 connection 独占, 且当前 connection 断开后会自动删除
		String queueName = channel.queueDeclare().getQueue();

		// 将该 queue 绑定到 exchange
		channel.queueBind(queueName, EXCHANGE_NAME, "");

		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

		// 创建回调函数
		DeliverCallback callback = (consumerTag, delivery) -> {
			String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
			System.out.println(" [x] Received '" + message + "'");
		};

		// 开始监听
		channel.basicConsume(queueName, true, callback, consumerTag -> {});
	}
}
复制代码

跑起来!先运行2个消费者实例

进入管理界面,能够看到已经创建了2个临时queue

并且能看到有一个名为logsexchange

并且这个exchange是绑定到了2个临时queue

随后运行生产者,并在控制台输入一些信息,便能在2个消费者看到接收到了消息。

于是,借助fanout类型的exchange,我们实现了前面所说的发布-订阅模型

上面的示例是基于java的实现,而基于springboot实现的发布-订阅模型,参考官方文档

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改