八、RabbitMQ之主题模式( Topics)

一、模型

八、RabbitMQ之主题模式( Topics)_第1张图片

  • P(Producer):生产者——发送消息
  • X(Exchange):交换机——一方面接受生产者发送的消息,另一方面是向队列中推送消息
  • Q(Queue): 消息队列(图中红色方形)——存储消息
  • C(consumer): 消费者——接收消息 
  • Binding:绑定——交换机与队列的绑定关系
  • Routing Key:路由键——消息和绑定都有此属性,交换机推送消息时,会在与之绑定的队列中寻找与消息本身路由键一致的队列进行推送

    主题模式(Topics):发送的消息以及与交换机与队列绑定的关系都需设置自属的路由键,并且绑定(Binding)的路由键支持通配符,生产者向交换机发送消息之后,交换机会在与之绑定的队列中寻找路由键与消息的路由键能匹配上的队列推送该消息。若没有相匹配的,则该消息丢失。

通配符说明:

*:匹配一个字符
#:匹配一个或多个字符


注意:
1、若队列与交换机的Binding中Routing Key不包含*和#,则表示相等队列推送,类似于直连交换机(Direct Exchange);
2、若队列与交换机的Binding中Routing Key为#或#.#,则表示全部队列推送,类似于扇形交换机(Fanout Exchange)。

    模型解读

  • 交换机与队列之间的绑定都设定了自属的路由键,并且绑定所带的路由键支持通配符;
  • 消息自己本身也需要设置路由键;
  • 交换机推送消息时需验证消息的路由键与绑定的路由键相匹配才会推送。

    注:路由模式( Routing)需将交换机(Exchange)类型定义为"Topic"。

二、Topic Exchange(主题交换机)

    1、模型

八、RabbitMQ之主题模式( Topics)_第2张图片

    2、含义 

    主题交换机(Topic Exchange)会根据消息自身所携带的路由键(Routing Key)在所有的绑定关系中寻找,与消息的路由键相匹配的队列推送该消息。

   3、与直连交换机(Direct Exchange)的不同

    3.1、直连交换机中,队列与交换机的绑定关系的路由键不支持通配符,但主题交换机中支持;

    3.2、直连交换机中,消息推送至队列中需要消息与绑定的路由键完全一致,但主题交换机是有通配符的情况下相匹配就行。

三、Java编程实现

    1、导入AMQP协议jar包,以及创建RabbitMQ连接工具类,请查看三、RabbitMQ之简单队列(Simple Queue);

    2、创建生产者发送消息;

package com.rabbitMQ.topic;

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

import com.rabbitMQ.util.ConnectionUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * 主题模式-生产者
 * *:代表一个任意字符
 * #:代表一个或多个任意字符
 * @author zhoujin
 * @data 2019-1-23
 */
public class TopicProducer {
	
	private static final String EXCHANGE_NAME = "exchange_topic";
	
	private static final String TOPIC_ERROR = "message.error";
	private static final String ERROR_MESSAGE = "This is topic MQ, routing key is message.error!";
	
	private static final String TOPIC_INFO = "message.info";
	private static final String INFO_MESSAGE = "This is topic MQ, routing key is message.info!";

	public static void main(String[] args) {
		Connection conn = null;
		Channel channel = null;
		try {
			// 1.获取连接
			conn = ConnectionUtils.getConnection();
			// 2.从连接中获取通道
			channel = conn.createChannel();
			// 3.声明交换机
			channel.exchangeDeclare(EXCHANGE_NAME, "topic");
			// 4.发送消息
			String message = ERROR_MESSAGE;
			channel.basicPublish(EXCHANGE_NAME, TOPIC_ERROR, null, message.getBytes());
			System.out.println("======================= Topic MQ send message end! 【Content:" + message + "】 =======================");
			
			message = INFO_MESSAGE;
			channel.basicPublish(EXCHANGE_NAME, TOPIC_INFO, null, message.getBytes());
			System.out.println("======================= Topic MQ send message end! 【Content:" + message + "】 =======================");
			
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			e.printStackTrace();
		} finally {
			// 7.关闭连接
			try {
				ConnectionUtils.closeConnection(channel, conn);
			} catch (IOException e) {
				e.printStackTrace();
			} catch (TimeoutException e) {
				e.printStackTrace();
			}
		}
	}
	
}

   3、创建消费者1接收消息;

package com.rabbitMQ.topic;

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

import com.rabbitMQ.util.ConnectionUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP.BasicProperties;

/**
 * 主题模式-第一个消费者
 * *:代表一个任意字符
 * #:代表一个或多个任意字符
 * @author zhoujin
 * @data 2019-1-23
 */
public class TopicFirstConsumer {
	
	private static final String QUEUE_NAME = "queue_topic_first";
	private static final String EXCHANGE_NAME = "exchange_topic";
	
	public static void main(String[] args) {
		try {
			// 1.获取连接
			Connection conn = ConnectionUtils.getConnection();
			// 2.从连接中获取通道
			final Channel channel = conn.createChannel();
			// 3.声明队列
			channel.queueDeclare(QUEUE_NAME, false, false, false, null);
			// 4.将队列绑定到交换机上
			channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "#.info");
			// 5.保证一次只接收一条消息
			channel.basicQos(1);
			// 6.创建消费者
			DefaultConsumer consumer = new DefaultConsumer(channel){
				
				@Override
				public void handleDelivery(String consumerTag,
						Envelope envelope, BasicProperties properties,
						byte[] body) throws IOException {

					String message = new String(body, "UTF-8");
					System.out.println("======================= First topic(#.info) consumer received a message! 【Content:" + message + "】 =======================");
					
					// 休眠,模拟业务处理
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						// 7.手动发送反馈回执
						channel.basicAck(envelope.getDeliveryTag(), false);
					}
					
				}
				
			};
			// 8.监听队列(关闭自动反馈,即第二个参数为false)
			channel.basicConsume(QUEUE_NAME, false, consumer);
			
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			e.printStackTrace();
		}
	}

}

    4、创建消费者2接收消息。

package com.rabbitMQ.topic;

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

import com.rabbitMQ.util.ConnectionUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP.BasicProperties;

/**
 * 主题模式-第二个消费者
 * *:代表一个任意字符
 * #:代表一个或多个任意字符
 * @author zhoujin
 * @data 2019-1-23
 */
public class TopicSecondConsumer {
	
	private static final String QUEUE_NAME = "queue_topic_second";
	private static final String EXCHANGE_NAME = "exchange_topic";
	
	public static void main(String[] args) {
		try {
			// 1.获取连接
			Connection conn = ConnectionUtils.getConnection();
			// 2.从连接中获取通道
			final Channel channel = conn.createChannel();
			// 3.声明队列
			channel.queueDeclare(QUEUE_NAME, false, false, false, null);
			// 4.将队列绑定到交换机上
			channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "message.#");
			// 5.保证一次只接收一条消息
			channel.basicQos(1);
			// 6.创建消费者
			DefaultConsumer consumer = new DefaultConsumer(channel){
				
				@Override
				public void handleDelivery(String consumerTag,
						Envelope envelope, BasicProperties properties,
						byte[] body) throws IOException {
					
					String message = new String(body, "UTF-8");
					System.out.println("======================= Second topic(message.#) consumer received a message! 【Content:" + message + "】 =======================");
					
					// 休眠,模拟业务处理
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						// 7.手动发送反馈回执
						channel.basicAck(envelope.getDeliveryTag(), false);
					}
					
				}
				
			};
			// 8.监听队列(关闭自动反馈,即第二个参数为)
			channel.basicConsume(QUEUE_NAME, false, consumer);
			
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			e.printStackTrace();
		}
	}

}

    注: 若只在生产者代码中定义了交换机,而未在消费者中定义队列或定义了队列但未与交换机绑定,程序编译和运行都不会报错,但是消息会被丢失。因为交换机没有存储消息的能力,RabbitMQ中只有队列有存储消息的能力。

四、运行代码以及控制台输出

    1、 运行生产者代码——定义交换机;

    ① 为何这里需要先运行生产者的代码?

        因为需要先在生产者中定义交换机。若先启动消费者,则会报错提示"no exchange"错误,原因是未定义交换机!

    ② 为何此次生产者发送的消息丢失了?

        因为此时还没有队列与之绑定,而交换机本身是不具备存储消息的功能,因此会导致消息丢失。

    2、运行两个消费者的代码;

    2.1、在Web管理页面查看队列绑定情况;

八、RabbitMQ之主题模式( Topics)_第3张图片

    3、再次运行生产者代码——发送消息。

    3.1、生产者控制台输出;

生产者控制台输出

    3.2、第一个消费者(#.info)控制台输出;

第一个消费者(#.info)控制台输出

    3.3、第二个消费者(message.#)控制台输出

八、RabbitMQ之主题模式( Topics)_第4张图片

 


你可能感兴趣的:(RabbitMQ学习笔记)