今天我们来学习最后一个交换器类型:topic。direct是放到exchange绑定的一个queue里,fanout是放到exchange绑定的所有queue里。那有没有放到exchange绑定的一部分queue里,或者多个routing key可以路由到一个queue里呢,那就要用到topic类型的exchange。

wKiom1cMrYvCk0vYAAETr3j13dU392.png



我们先来看看多个routing key如何路由到一个queue里。假设我们有三个系统,在出错的时候会写日志,并会把日志发送到RabbitMQ,路由键为:系统名.error。在RabbitMQ里我们想把所有的error信息放到一个queue里面,就可以使用如下的方式:

package com.jaeger.exchange.topic;

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

import org.junit.Test;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class Producer {
	private static final String MY_EXCHANGE_NAME = "MyExchange";
	// 三个不同系统发送日志时使用的路由键
	private static final String SYS1_ERROR_ROUTING_KEY = "sys1.error";
	private static final String SYS2_ERROR_ROUTING_KEY = "sys2.error";
	private static final String SYS3_ERROR_ROUTING_KEY = "sys3.error";
	private static final String MY_QUEUE_NAME = "MyQueue";
	private static final String TOPIC = "topic";
	private static final String HOST = "172.19.64.21";
	private static final String USER = "jaeger";
	private static final String PASSWORD = "root";
	private static final int PORT = 5672;

	@Test
	public void createExchangeAndQueue() throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(HOST);
		connectionFactory.setUsername(USER);
		connectionFactory.setPassword(PASSWORD);
		connectionFactory.setPort(PORT);
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();
		// 创建一个topic类型的exchange
		channel.exchangeDeclare(MY_EXCHANGE_NAME, TOPIC);
		// 创建一个queue
		channel.queueDeclare(MY_QUEUE_NAME, false, false, false, null);
		// 创建一个routing key,把exchange和queue绑定到一起,但这里的routing key并不是一个
		// 具体的名称,而是可以匹配所有以.error结尾的routing key
		channel.queueBind(MY_QUEUE_NAME, MY_EXCHANGE_NAME, "*.error");
		channel.close();
		connection.close();
	}

	@Test
	public void produce() throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(HOST);
		connectionFactory.setUsername(USER);
		connectionFactory.setPassword(PASSWORD);
		connectionFactory.setPort(PORT);
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();
		String message = "Hello 世界!";
		/*
		向RabbitMQ发送消息。我们这里指定了exchange和3个不同的routing key的名称,RabbitMQ会去找有没有叫这个名称的exchange,
		如果找到了又发现这个exchange是topic类型,就会尝试用指定的routing key去匹配exchange绑定的routing key,
		凡是匹配到的routing key的queue都会收到消息。
		*/
		channel.basicPublish(MY_EXCHANGE_NAME, SYS1_ERROR_ROUTING_KEY, null, message.getBytes("utf-8"));
		channel.basicPublish(MY_EXCHANGE_NAME, SYS2_ERROR_ROUTING_KEY, null, message.getBytes("utf-8"));
		channel.basicPublish(MY_EXCHANGE_NAME, SYS3_ERROR_ROUTING_KEY, null, message.getBytes("utf-8"));
		System.out.println("Sent '" + message + "'");
		channel.close();
		connection.close();
	}
	
	@Test
	public void consume() throws IOException, TimeoutException, InterruptedException{
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(HOST);
		connectionFactory.setUsername(USER);
		connectionFactory.setPassword(PASSWORD);
		connectionFactory.setPort(PORT);
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();
		Consumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
					byte[] body) throws IOException {
				String message = new String(body, "UTF-8");
				System.out.println("Received '" + message + "'");
			}
		};
		channel.basicConsume(MY_QUEUE_NAME, true, consumer);
		Thread.sleep(1000);
	}
}

运行createExchangeAndQueue,发现exchange上绑定了一个*.error的路由键:

wKioL1cMsP6zKwIaAAC1Y5JCoE8729.png

wKiom1cMsEnD7U7SAABfgoOsCzQ536.png

wKiom1cMsErhGH85AABdPAU5gJ4835.png

然后运行produce方法,向RabbitMQ发送消息:

wKioL1cMsX_T3jwTAABg2QnyMHI258.png可以看到3条消息进入了同一个queue。最后运行consume来消费消息:

wKiom1cMsQrS_393AAAo5KhmHCM069.png

wKiom1cMsQqytIeEAABfzA1ILfI391.png




上面我们展示了如何让多个routing key路由到同一个queue。那有没有办法让一个routing key路由到多个queue呢?其实用topic类型的exchange是完全可以做到的。比如,我们的系统出错后会根据不同的错误级别生成error_levelX.log日志,我们在后台首先要把所有的error保存在一个总的queue里,然后再按level分别存放在不同的queue。我们把上面的代码修改下:

package com.jaeger.exchange.topic;

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

import org.junit.Test;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class Producer {
	private static final String MY_EXCHANGE = "MyExchange";
	private static final String SYS_ERROR_ROUTING_KEY = "error.level1.log";
	private static final String SYS_ERROR_QUEUE = "ErrorQueue";
	private static final String SYS_LEVEL1_ERROR_QUEUE = "Level1ErrorQueue";
	private static final String SYS_LEVEL2_ERROR_QUEUE = "Level2ErrorQueue";
	private static final String TOPIC = "topic";
	private static final String HOST = "172.19.64.21";
	private static final String USER = "jaeger";
	private static final String PASSWORD = "root";
	private static final int PORT = 5672;

	@Test
	public void createExchangeAndQueue() throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(HOST);
		connectionFactory.setUsername(USER);
		connectionFactory.setPassword(PASSWORD);
		connectionFactory.setPort(PORT);
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();
		// 创建一个topic类型的exchange
		channel.exchangeDeclare(MY_EXCHANGE, TOPIC);
		// 创建三个存放error的queue
		channel.queueDeclare(SYS_ERROR_QUEUE, false, false, false, null);
		channel.queueDeclare(SYS_LEVEL1_ERROR_QUEUE, false, false, false, null);
		channel.queueDeclare(SYS_LEVEL2_ERROR_QUEUE, false, false, false, null);
		// 创建三个routing key,把exchange和三个queue绑定到一起
		channel.queueBind(SYS_ERROR_QUEUE, MY_EXCHANGE, "error.*.log");
		channel.queueBind(SYS_LEVEL1_ERROR_QUEUE, MY_EXCHANGE, "error.level1.log");
		channel.queueBind(SYS_LEVEL2_ERROR_QUEUE, MY_EXCHANGE, "error.level2.log");
		channel.close();
		connection.close();
	}

	@Test
	public void produce() throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(HOST);
		connectionFactory.setUsername(USER);
		connectionFactory.setPassword(PASSWORD);
		connectionFactory.setPort(PORT);
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();
		String message = "Hello 世界!";
		/*
		向RabbitMQ发送消息。我们这里指定了exchange和一个routing key的名称,RabbitMQ会去找有没有叫这个名称的exchange,
		如果找到了又发现这个exchange是topic类型,就会尝试用指定的routing key去匹配exchange绑定的routing key,
		凡是匹配到的routing key的queue都会收到消息。
		*/
		channel.basicPublish(MY_EXCHANGE, SYS_ERROR_ROUTING_KEY, null, message.getBytes("utf-8"));
		System.out.println("Sent '" + message + "'");
		channel.close();
		connection.close();
	}
	
	@Test
	public void consume() throws IOException, TimeoutException, InterruptedException{
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost(HOST);
		connectionFactory.setUsername(USER);
		connectionFactory.setPassword(PASSWORD);
		connectionFactory.setPort(PORT);
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();
		Consumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
					byte[] body) throws IOException {
				String message = new String(body, "UTF-8");
				System.out.println("Received '" + message + "'");
			}
		};
		channel.basicConsume(SYS_ERROR_QUEUE, true, consumer);
		Thread.sleep(1000);
	}
}

运行createExchangeAndQueue方法,创建3个queue:

wKioL1cMwPSw52DhAACxXZIAGc4442.png

wKioL1cMwPSjrus4AACF_-RpQY4143.png

wKiom1cMwECRYiDdAABgHpGWzTo348.png

然后运行produce方法向RabbitMQ发送消息。可以看到消息只进入了ErrorQueue和Level1ErrorQueue两个队列:

wKioL1cMwWCyxgERAAB9bMWsGxk722.png最后运行consume来消费ErrorQueue队列里的消息:

wKioL1cMwYPw5L3aAAB8XlqXFlc526.png



到此为止,RabbitMQ常用的3类exchange全部介绍完了。对于topic有一点需要注意,就是它的匹配规则。topic的匹配规则是基于标识符的,用.分隔。比如error.level1.log中,error、level1和log都是标识符。

*只能匹配一个标识符,比如error.*.log只能匹配error.level1.log或者error.high.log,而不能匹配error.log或者error.level1.high.log。

#可以匹配0个或多个标识符,如error.#.log就可以匹配error.log或者error.level1.high.log。