RabbitMQ入门(二)

在上一篇博客,笔者简单的介绍了一些RabbitMQ相关的内容,在这一篇博客会根据RabbitMQ官网的入门介绍,结合笔者自身的理解更深入的在代码方面介绍RabbitMQ的入门使用,。同样,这篇博客主要的目的也是整理记录自己的学习笔记,加深自己对RabbitMQ的使用与理解。

RabbitMQ入门(一)

RabbitMQ入门(二)

RabbitMQ入门(三)


目录

1.入门

创建消息生产者

创建消息消费者

2.工作队列

创建新的消费者

测试

设置AutoAck为false的注意事项

3.发布/订阅模式在RabbitMQ中实现方式

4.部分API使用说明(个人理解,仅供参考)

channel.queueDeclare

channel.queueBind

channel.basicPublish

5.补充说明


1.入门

创建消息生产者

首先我们先来看RabbitMQ官网的一段消息生产者相关代码:

public class Send {
	private final static String QUEUE_NAME = "hello";

	public static void main(String[] args) throws IOException, TimeoutException {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		factory.setVirtualHost("myVirualHost");
		try (Connection conn = factory.newConnection(); Channel channel = conn.createChannel()) {
			channel.exchangeDeclare("helloExchange ", "direct", true);
			channel.queueDeclare(QUEUE_NAME, true, false, false, null);
			channel.queueBind(QUEUE_NAME, " helloExchange ", "bindKey");
			
			String message = ”message from send“;
			channel.basicPublish("helloExchange", "bindKey", null, message.getBytes());
			System.out.println(" [x] Sent '" + message + "'");
		}
	}
}

这里可以看到首先 我们是创建了ConnectionFacotry 并设置了 Host和VirtualHost  因为端口 和用户名 密码我们使用默认的所以并没有设置。然后 fatory.newConnection 创建Connection,之后 Connection.createChannel创建channel,而我们的操作基本都是基于 channel的。这里使用了try-with-resources语句,在try语句中声明channel等资源,这样我们不用手动关闭资源。

channel.exchangeDeclare("helloExchange", "direct", true);

定义一个名为 helloExchange  类型是 direct的exchange. 如果RabbitMQ此VirtualHost中 已经有同名同类型的Exchange 那么可能会报错 这里设置为true就能避免。(但是如果有类型不同的同名Exchange 则还是会报错,那么只能删除此句或者去控制页面删除Exchange)

channel.queueDeclare(QUEUE_NAME, true, false, false, null);

定义名为 QUEUE_NAME的队列 第二个参数是防止有同名Queue。具体函数的参数定义可以查看后面的部分API说明。

channel.queueBind(QUEUE_NAME, " helloExchange ", "bindKey");

将名为QUEUE_NAME的队列与helloExchange绑定在一起,并且Key为bindKey.这样发送到helloExchange的Routing Key为bindKey的消息就会进入此队列。

channel.basicPublish("helloExchange", "bindKey", null, message.getBytes());

发布消息到helloExchange  Routing Key为bingKey。

从上面的代码我们可以看出,就跟上一节我们说明的一样,我们发送消息并没有直接给Queue,而是发送给Exchange 并且设置Routing Key 之后RabbitMQ 内部将根据Exchange 类型和与Exchange绑定的Queue设置的Binding key 将信息放到队列或者舍弃。如果没有设置Exchange 也就是传递空字符串 就会发送到默认的Exchange。

创建消息消费者

相关代码如下:

public class Recv {
	private static final String QUEUE_NAME="hello";
	public static void main(String[] argv) throws Exception {
	    ConnectionFactory factory = new ConnectionFactory();
	    factory.setHost("localhost");
	    factory.setVirtualHost("myVirualHost");
	    Connection connection = factory.newConnection();
	    Channel channel = connection.createChannel();
	    DeliverCallback deliverCallback = (consumerTag,delivery)->{
	    	String message = new String(delivery.getBody(), "UTF-8");
	        System.out.println(" [x] Received '" + message + "'");
	    };
	    channel.basicConsume(QUEUE_NAME, true,deliverCallback,consumerTag->{});
	    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

	  }
}

可以看出来消息消费者  之前操作也类似 就是 创建ConnectionFacotry Connection Channel  然后通过Channel做操作。

DeliverCallback deliverCallback = (consumerTag,delivery)->{

      String message = new String(delivery.getBody(), "UTF-8");

      System.out.println(" [x] Received '" + message + "'");

};

这里是定义收到消息后的回调函数,这里面也就是我们实际处理消息的地方。

channel.basicConsume(QUEUE_NAME, true,deliverCallback,consumerTag->{});

创建 一个消费者,这个消费者从QUEUE_NAME队列中取信息 然后进行消费。这里第二个参数是  消费者是否自动返回ack。这里暂时设置为true(这样设置 会导致 如果处理消息过程中,消费者出现了异常或者关闭了,消息会丢失,下节我们会做一些处理避免因为消费者消费失败 导致消息丢失)。

2.工作队列

上节我们说到如果消费者已经取得消息 但是处理过程中发生了异常,会导致消息丢失的问题,这节我们就处理一下。由于生产者没有太大变化,我们这里只是将消息改为了从控制台获得。即

String message = String.join(“ ”,args);

创建新的消费者

public class Worker {
	private static final String QUEUE_NAME = "hello";

	public static void main(String[] args) throws IOException, TimeoutException {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		factory.setVirtualHost("myVirualHost");
		Connection conn = factory.newConnection();
		Channel channel = conn.createChannel();
//		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		DeliverCallback deliverCallback = (consumerTag, delivery) -> {
			String message = new String(delivery.getBody(), "UTF-8");
			System.out.println(" [x] Received '" + message + "'");
			try {
				doWork(message);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				System.out.println(" [x] Done");
				channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
			}
		};
		boolean autoAck = false; // acknowledgment is covered below
		channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
		});
	}
	private static void doWork(String task) throws InterruptedException {
		for (char ch : task.toCharArray()) {
			if (ch == '.')
				Thread.sleep(5000);
		}
	}
}

可以从上面代码看出来,我们的主要区别之处主要在于 回调函数中 我们增加了模拟处理消息的方法,并且在处理完成之后 调用

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

这代表着我们告诉RabbitMQ,此条信息已经处理完成。

并且我们在声明消费者时候 使用如下代码:

boolean autoAck = false;

channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {});

设置了自动返回ack为false。

这样我们就可以避免 消费者取到信息 处理信息过程中出现异常 导致信息丢失了。

测试

执行消息生产者 并分别传入 参数1.1.1.1 Message 和 2.2.2.2 Message

启动消费者  并在显示

而未显示[x] Done之前  关闭此消费者。

启动第二个消费者,发现 这个消费者最后打印

说明 我们的消息 1.1.1.1 Message并没有因为第一个消费者消费失败 而丢失。这是因为如果消费者没有将成功处理消息的标识返回给RabbitMQ ,那么RabbitMQ会在此消费者 关闭之后将消息重新入栈。

我们将

channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {});

改为上一节的

channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});

之后重复操作 会发现信息发生了丢失。从而说明如果设置默认返回Ack的话,会有消息处理出现异常从而导致消息丢失的风险。

设置AutoAck为false的注意事项

之前我们已经设置了AutoAck为false,这样的话 我们要注意一定要在处理完消息之后使用发送过来这条消息的channel(使用其他channel会报错!)调用channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); 的代码 将消息被消费的信息 告诉RabbitMQ 否则的话就会出现 消息被消费了 但是还是在队列中,也就一直在内存中保存此消息,更严重的是当这个消费此消息的消费者关闭之后,这些消息 由于是unacked状态的,会再次被其他消费者消费。

3.发布/订阅模式在RabbitMQ中实现方式

在RabbitMQ中 如果我们想实现 类似之前ActiveMQ 那种 topic 模式的消息处理,也就是一个发布者发布消息 能让多个订阅者收到,并且 也是订阅者只收到自己订阅后的消息,那么我们可以 创建一个exchange 其类型是 fanout,也就是不管routingkey 此exchange收到的消息 将发送到所有它绑定的队列中,那么我们订阅者发送消息给fanout的exchange时候不用指定routingkey(指定也没作用)channel.basicPublish("fanoutExchange", "",null, msgByte);

而消费者端 会定义一个临时队列,建立的临时队列

String queueName = channel.queueDeclare().getName();

将此临时队列 与 fanout类型的EXCHANGE绑定

channel.queueBind(queueName, EXCHANGE_NAME, "");

然后channel.basicConsume(queueName, true, deliverCallback, consumerTak->{});

创建消费者,这样每次启动消费者 都会建立一个临时队列 并且与之前的fanoutExchange相绑定,从而接收到发送到fanoutExchange的消息,并且关闭消费者 就会自动销毁此临时队列。

4.部分API使用说明(个人理解,仅供参考)

channel.queueDeclare

channel.queueDeclare(queue, durable, exclusive, autoDelete, arguments)

queue 队列名称(queue如果设置为空字符串,那么会自动生成队列名称,使用queueDeclare().getName()就能在创建队列之后 获取自动生成的队列名称)
durable  是否持久化
exclusive 是否只有一个channel和这个queue连接,如果是,那么当这个channel断开删除该队列
autoDelete 是否自动删除
arguments 是其他参数 可以设置队列长度限制 最大优先级数等等。

下面表格是对于arguments的说明。

x-max-length 限定队列消息的最大值长度,超过指定长度会把最早的几条删除
x-max-length-bytes 限定队列的最大占用空间大小
x-dead-letter-exchange 将因为长度不够或者过期的消息 从队列删除 并推送到指定交换机中而不是丢弃
x-dead-letter-routing-key

上面删除的消息 推送到指定交换机的指定路由键的队列中

x-max-priority 优先级队列,声明队列时先定义最大优先级值(定义最大值一般不要太大),在发布消息的时候指定该消息的优先级, 优先级更高(数值更大的)的消息先被消费
x-queue-mode 为lazy 则先将消息保存到磁盘上,不放在内存中,当消费者开始消费的时候才加载到内存中
x-expires 当队列在指定的时间没有被访问(consume, basicGet, queueDeclare…)就会被删除
x-message-ttl 设置队列中的所有消息的生存周期(统一为整个队列的所有消息设置生命周期), 也可以在发布消息的时候单独为某个消息指定剩余生存时间,单位毫秒, 类似于redis中的ttl,生存时间到了,消息会被从队里中删除,注意是消息被删除,而不是队列被删除

当设置exclusive 以及autoDelete 都为true,也就是这个queue只能被一个channel使用 而且channel断开后这个queue直接被删除(queue如果设置为空字符串 那么就会直接删除)

 在RabbitMQ中 使用channel.queueDeclare  第二个参数是 是否持久化 。如果发布者和消费者都调用了queueDeclare (并且设置的QueueName是一样的) 那么这个参数要一致  否则会报错。ExchangeDeclare同样可以设置是否持久化。如果想RabbitMQ被异常关闭之后 消息也不会丢失,那么我们可以将queue设置为持久化,然后在发送消息的时候 将第三个参数 也就是设置消息是持久化的文本形式!!

channel.basicPublish("leiTopicExchange", "leiTopicMsg",MessageProperties.PERSISTENT_TEXT_PLAIN, msgByte);

这样 RabbitMQ关闭之后 重启 原来在消息队列的消息也不会丢失。

channel.queueBind

channel.queueBind(queue, exchange, routingKey, arguments)

主要用来绑定队列和exchange

queue 要绑定的队列名
exchange 绑定的exchange名
routingKey 绑定exchange和队列的Bindkey (如果是fanout或者headers其实无所谓 可以传递空字符串)
arguments 绑定传递的额外参数,如exchange是headers时候 添加的键值对,从而让消息根据发送消息设置的headers与这里的headers对比,符合条件才发送到这个queue

channel.basicPublish

channel.basicPublish(exchange, routingKey, props, body)

exchange 是发送消息到哪个exchange
routingKey exchange为topic或者direct时候  决定routingKey和绑定在此exchange的queue的bindingKey 是否匹配的值
props 消息的其他信息 具体可以查看BasicProperties 这个类,里面可以设置信息过期等
body 就是传递的消息体

其中的props我们可以从BasicProperties这个类中得到一些信息:

public BasicProperties(
            String contentType,
            String contentEncoding,
            Map headers,
            Integer deliveryMode,
            Integer priority,
            String correlationId,
            String replyTo,
            String expiration,
            String messageId,
            Date timestamp,
            String type,
            String userId,
            String appId,
            String clusterId)
        {
            this.contentType = contentType;
            this.contentEncoding = contentEncoding;
            this.headers = headers==null ? null : Collections.unmodifiableMap(new HashMap(headers));
            this.deliveryMode = deliveryMode;
            this.priority = priority;
            this.correlationId = correlationId;
            this.replyTo = replyTo;
            this.expiration = expiration;
            this.messageId = messageId;
            this.timestamp = timestamp;
            this.type = type;
            this.userId = userId;
            this.appId = appId;
            this.clusterId = clusterId;
        }

如contentType就是消息的类型如text/plain就代表是普通文本,contentEncoding就是消息的编码,而headers就决定在消息进入Type是headers的Exchange的时候,消息该发往哪个队列,priority代表消息的优先级,后续的一些在之后用到的时候会再进行讲解。

5.补充说明

之前我们设置中 会发现 两个Worker,有时候会出现一个Worker一直执行,而另外一个Worker好像几乎不工作 这是因为RabbitMQ只是在消息进入队列的时候发送消息,而不考虑消费者未确认的数量,为了避免这种情况,我们可以在消费者使用channel.basicQos(prefetchcount)  prefetchcount是整数,也就是预存计数,这就告诉rabbitMQ  一次最多向消费者发送prefetchcount条信息。(也可以认为是这个消费者最多能持有prefetchcount条信息,比如设置为3 ,如果消息够多 那么每次处理并发送ack给rabbitMQ,rabbitMQ会再给一条信息,也就是保持消费者有3条信息)如果设置为1,那么就是在worker处理并确认一条消息之前,不向这个worker发送新消息 相反他会发送给下一个不忙的工人。

RabbitMQ中消息的状态,1.准备交付(未给消费者),2.已交付但是消费者尚未确认(没发送ack)。

你可能感兴趣的:(RabbitMQ,Java)