RabbitMQ指南(四)交换机

本文深入探讨RabbitMQ中的交换机概念,包括直连、广播、主题和首部类型,以及交换机参数、绑定和AE交换机的使用,通过实例代码演示不同类型交换机的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  交换机用于接收消息,并将消息按照一定路由规则转发给一个或多个队列。交换机的转发规则与交换机的类型有关,交换机共分为4类:直连类型(direct)、广播类型(fanout)、主题类型(topic)和首部类型(headers)。
  服务端安装完毕后,RabbitMQ已经默认建立了各种类型的交换机。用户也可另外建立交换机。

4.1 直连类型

  直连类型交换机与队列通过路由键进行绑定,消息发送时,需指定交换机和路由键,交换机接收到消息后,根据路由键将消息转发至相应队列。

public static void main(String[] args) throws Exception {
	ConnectionFactory factory = new ConnectionFactory();
	// 设置服务端的地址、端口、用户名和密码...
	
	Connection connection = factory.newConnection();
	Channel channel = connection.createChannel();
	
	// 声明直连类型交换机
	channel.exchangeDeclare("direct.exchange", BuiltinExchangeType.DIRECT);
	channel.queueDeclare("direct.queue", true, false, false, null);
	// 将交换机和队列通过路由键“key”绑定
	channel.queueBind("direct.queue", "direct.exchange", "key");
	// 发送消息,指定交换机和路由键
	channel.basicPublish("direct.exchange", "key", null, "Test".getBytes());
	
	channel.close();
	connection.close();
}

  运行代码后,进入Web管理界面,可以看到,一个名为“direct.exchange”的直连类型交换机已被创建,点击交换机名进入交换机管理界面,Bindings下拉框可查看当前交换机的绑定情况。队列“direct.queue”通过路由键“key”与当前交换机绑定,并且从队列“direct.queue”中能找到发送的消息。

  队列与直连交换机绑定,可以有更为复杂的情况:
  (1)一个队列可通过不同路由键与直连交换机进行绑定,指定任意一个路由键发送消息,消息都会被转发到该队列中;

  (2)多个队列可通过相同的路由键与直连交换机进行绑定,指定该路由键发送消息,这些队列都将收到转发的消息,如此也可实现广播的效果。

  队列与直连交换机绑定后,还可通过queueUnbind(String queue, String exchange, String routingKey)方法解绑,注意解绑是针对路由键的,并非将队列与交换机的所有路由关系都解除。

4.2 广播类型

  广播类型交换机与队列绑定时无需指定路由键,交换机收到消息后,会转发给所有与其绑定的队列中。

// 声明广播类型交换机
channel.exchangeDeclare("fanout.exchange", BuiltinExchangeType.FANOUT);
channel.queueDeclare("fanout.queue1", true, false, false, null);
channel.queueDeclare("fanout.queue2", true, false, false, null);
// 将交换机和队列绑定
channel.queueBind("fanout.queue1", "fanout.exchange", "");
channel.queueBind("fanout.queue2", "fanout.exchange", "");

// 发送消息,指定广播交换机
channel.basicPublish("fanout.exchange", "", null, "Test".getBytes());

  运行程序后,进入Web管理界面,可以看到,一个名为“fanout.exchange”的广播类型交换机已被创建,点击交换机名进入交换机管理界面,Bindings下拉框可查看当前交换机的绑定情况。队列“fanout.queue1”“fanout.queue2”与当前交换机绑定,并且从两个队列中都能找到发送的消息。

  在广播交换机的声明和绑定中,应注意:
  (1)路由键不能为null,即使广播交换机与队列的绑定与路由键无关,也需填写路由键入参,一般以空字符串作为路由键入参,发送消息时亦以空字符串作为路由键入参;
  (2)绑定时也可填写路由键入参,在Web管理界面的交换机绑定列表中也会显示绑定的路由键,但这个路由键是无效的,向交换机中发送消息时,即使指定路由键,所有与其绑定的队列也会收到消息。所以在RabbitMQ的使用中,应首先关注交换机的类型;
  (3)若先后以不同的路由键入参将队列多次与广播交换机绑定,绑定列表中也会显示队列与广播交换机绑定使用的不同路由键,但向广播交换机发送消息后,队列也只会收到一次消息。

4.3 主题类型

  主题类型交换机与队列绑定时也需指定路由键。与直连类型交换机不同的是,直连交换机在消息指定的路由键与队列绑定使用的路由键完全相同时,才将消息转发到该队列中;主题类型交换机可在路由键匹配过程中使用星号(*)、井号(#)进行模糊匹配,此时,路由键应为由英文句点(.)隔开的一个字符串,“*”匹配一个单词,“#”可匹配零至多个任意数量的单词。
  由句点隔开的各单词通常表示消息的各类属性值,称为消息的主题,消息的消费者往往仅关系某几个维度的特征,通过模糊匹配,消息转发到处理各类主题的队列中,交由处理相应主题的消费者进行处理。主题类型交换机想必由此得名。
  想象如下场景,一个证券行情发布系统正在向客户端发布各类证券的实时行情,这些证券行情消息可以由三个维度分类:(1)该证券所属的市场,包括上海(sh)和深圳(sz);(2)该证券的类别,包括股票(stock)、债券(bond)、基金(fund);(3)该行情的类型,包括最新成交价(deal)和买卖两方五档价格(buy1 - buy5、sell1 - sell5)。
  有3个用户出于不同的需求,需要不同的行情信息:甲需要上海市场所有证券的全部行情信息,乙需要深圳市场所有证券的最新价,丙需要全市场股票的最新价。则3个用户的消息队列与主题交换机绑定的路由键为——
  甲(队列topic.queue.a):sh.#
  乙(队列topic.queue.b):sz.*.deal
  丙(队列topic.queue.c):*.stock.deal
  当行情发布系统发布不同的行情消息,3个用户收到行情消息也不同。举例如下,×表示不收到该消息,√表示会收到该消息:

甲(sh.#)乙(sz.*.deal)丙(*.stock.deal)
上海某债券买一价
sh.bond.buy1
××
上海某股票最新价
sh.stock.deal
×
深圳某基金最新价
sz.fond.deal
××
深圳某股票最新价
sz.stock.deal
×

  可通过如下程序进行验证。

public static void main(String[] args) throws Exception {
	ConnectionFactory factory = new ConnectionFactory();
	// 设置服务端的地址、端口、用户名和密码...
	
	Connection connection = factory.newConnection();
	Channel channel = connection.createChannel();
	
	// 声明主题类型交换机
	channel.exchangeDeclare("topic.exchange", BuiltinExchangeType.TOPIC);
	channel.queueDeclare("topic.queue.a", true, false, false, null);
	channel.queueDeclare("topic.queue.b", true, false, false, null);
	channel.queueDeclare("topic.queue.c", true, false, false, null);
	// 将交换机和队列绑定
	channel.queueBind("topic.queue.a", "topic.exchange", "sh.#");
	channel.queueBind("topic.queue.b", "topic.exchange", "sz.*.deal");
	channel.queueBind("topic.queue.c", "topic.exchange", "*.stock.deal");
	
	// 发送消息
	channel.basicPublish("topic.exchange", "sh.bond.buy1", null, "sh.bond.buy1 message".getBytes());
	channel.basicPublish("topic.exchange", "sh.stock.deal", null, "sh.stock.deal message".getBytes());
	channel.basicPublish("topic.exchange", "sz.fond.deal", null, "sz.fond.deal message".getBytes());
	channel.basicPublish("topic.exchange", "sz.stock.deal", null, "sz.stock.deal message".getBytes());

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

  运行程序后,可以看到,交换机列表中出现了创建的主题类型交换机“topic.exchange”,其与队列的绑定关系与设置相同。进入队列,查看队列中的消息,与表格所列是一致的。

  如此,通过主题交换机,三个队列都只收到自己需要的消息。特殊地,路由键为“#”的队列会收到所有消息。

4.4 首部类型

  首部类型交换机定义路由规则时,并非使用一个字符串型的路由键,而是使用一组键值对作为消息首部,首部中包含一个特殊的键“x-match”,该键的值有两个选项“all”和“any”,由此定义首部类型交换机的两种路由规则:(1)全匹配(all),消息首部需包含队列绑定定义的首部的所有键,并且对应的值都相同,才匹配;(2)任意一组匹配(any),消息首部和队列绑定定义的首部中,只要有一组键和值相同,就进行匹配。
  仍以证券行情发布系统为例,证券的行情信息由两个维度分类:(1)市场(market),包括上海(sh)和深圳(sz);(2)证券类别(type),股票(stock)、债券(bond)、基金(fund)。
  甲只需要上海市场股票的行情信息,而乙需要沪深两市的股票以及上海其他证券的所有行情信息,则两个消费者队列绑定时定义的首部如下——
  甲:{“x-match”:”all”, “market”:”sh”, “type”:”stock”}
  (即market=sh、type=stock的消息才接收)
  乙:{“x-match”:”any”, “market”:”sh”, “type”:”stock”}
  (即market=sh或type=stock的消息全部接收)
  当行情发布系统发布不同的行情消息,2个用户收到行情消息也不同。举例如下,×表示不收到该消息,√表示会收到该消息:


x-match:all
market:sh
type:stock

x-match:any
market:sh
type:stock
上海某股票的行情消息
market:sh
type:stock
上海某债券的行情消息
market:sh
type:bond
×
深圳某股票的行情消息
market:sz
type:stock
×

  可通过如下程序进行验证。

public static void main(String[] args) throws Exception {
	ConnectionFactory factory = new ConnectionFactory();
	// 设置服务端的地址、端口、用户名和密码...
	
	Connection connection = factory.newConnection();
	Channel channel = connection.createChannel();
	
	// 声明首部类型交换机
	channel.exchangeDeclare("headers.exchange", BuiltinExchangeType.HEADERS);
	channel.queueDeclare("headers.queue.a", true, false, false, null);
	channel.queueDeclare("headers.queue.b", true, false, false, null);
	// 将交换机和队列绑定
	Map<String, Object> heardersMapA = new HashMap<String, Object>();
	heardersMapA.put("x-match", "all");
	heardersMapA.put("market", "sh");
	heardersMapA.put("type", "stock");
	channel.queueBind("headers.queue.a", "headers.exchange", "", heardersMapA);
	Map<String, Object> heardersMapB = new HashMap<String, Object>();
	heardersMapB.put("x-match", "any");
	heardersMapB.put("market", "sh");
	heardersMapB.put("type", "stock");
	channel.queueBind("headers.queue.b", "headers.exchange", "", heardersMapB);
	
	// 发送消息
	// 消息1
	Map<String, Object> heardersMessage1 = new HashMap<String, Object>();
	heardersMessage1.put("market", "sh");
	heardersMessage1.put("type", "stock");
	AMQP.BasicProperties.Builder properties1 = new AMQP.BasicProperties().builder().headers(heardersMessage1);
	channel.basicPublish("headers.exchange", "", properties1.build(), "sh stock".getBytes());
	// 消息2
	Map<String, Object> heardersMessage2 = new HashMap<String, Object>();
	heardersMessage2.put("market", "sh");
	heardersMessage2.put("type", "bond");
	AMQP.BasicProperties.Builder properties2 = new AMQP.BasicProperties().builder().headers(heardersMessage2);
	channel.basicPublish("headers.exchange", "", properties2.build(), "sh bond".getBytes());
	// 消息3
	Map<String, Object> heardersMessage3 = new HashMap<String, Object>();
	heardersMessage3.put("market", "sz");
	heardersMessage3.put("type", "stock");
	AMQP.BasicProperties.Builder properties3 = new AMQP.BasicProperties().builder().headers(heardersMessage3);
	channel.basicPublish("headers.exchange", "", properties3.build(), "sz stock".getBytes());

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

  运行程序后,可以看到,交换机列表中出现了创建的首部类型交换机“headers.exchange”,其与队列的绑定关系与设置相同。进入队列,查看队列中的消息,与表格所列是一致的。

  声明与绑定首部交换机时需注意:
  (1)队列绑定首部交换机时无需路由键,但路由键入参不能为null,一般填空字符串;
  (2)队列绑定首部交换机时,若首部不包含“x-match”键,则其默认为“all”类型的绑定。

4.5 交换机参数

  声明交换机的方法exchangeDeclare()含有一系列重载方法,可通过这些方法设置交换机的各种属性,包括——
  durable:持久化,交换机以及其与队列的绑定关系默认保存在内存中,RabbitMQ服务端重启后将丢失,若需将其保存在磁盘上,需开启持久化(一般实际使用中都是开启的);该持久化是针对交换机和队列绑定关系的,若队列持久化而交换机未持久化,RabbitMQ重启后只会丢失交换机而不会丢失队列;
  autoDelete:自动删除,当与该交换机绑定的队列全部解绑或删除后,该交换机自动删除;
  internal:内部,启用该参数后,RabbitMQ客户端无法直接向该交换机发布消息,只能绑定到另外的交换机使用。

4.6 交换机之间的绑定

  不仅只有队列可以与交换机绑定,交换机之间也可以进行绑定,绑定的交换机之间类型也可不同。利用交换机之间的绑定,可以实现较复杂的转发规则,形成一个网状的路由结构。一般只有一个对外开放的交换机,消息只能发往这个入口交换机,其余交换机则声明为内部(internal)的。
  利用之前证券行情发布的例子,定义如下转发结构。证券所属市场放在消息的首部,而证券的类型以及行情的类型作为消息的主题。发送如图所示的消息时,根据路由规则,消息将由交换机“headers.exchange”转发至“topic.exchange.sh”,在根据主题转发至队列“queue.sh.b”。

  可由以下程序验证。

public static void main(String[] args) throws Exception {
	ConnectionFactory factory = new ConnectionFactory();
	// 设置服务端的地址、端口、用户名和密码...
	
	Connection connection = factory.newConnection();
	Channel channel = connection.createChannel();
	
	// 声明交换机和队列
	channel.exchangeDeclare("headers.exchange", BuiltinExchangeType.HEADERS, true, false, false, null);
	channel.exchangeDeclare("topic.exchange.sh", BuiltinExchangeType.TOPIC, true, false, true, null);
	channel.exchangeDeclare("topic.exchange.sz", BuiltinExchangeType.TOPIC, true, false, true, null);
	channel.queueDeclare("queue.sh.a", true, false, false, null);
	channel.queueDeclare("queue.sh.b", true, false, false, null);
	channel.queueDeclare("queue.sz.a", true, false, false, null);
	
	// 绑定
	Map<String, Object> headersMapSH = new HashMap<String, Object>();
	headersMapSH.put("market", "sh");
	channel.exchangeBind("topic.exchange.sh", "headers.exchange", "", headersMapSH);
	Map<String, Object> headersMapSZ = new HashMap<String, Object>();
	headersMapSZ.put("market", "sz");
	channel.exchangeBind("topic.exchange.sz", "headers.exchange", "", headersMapSZ);
	channel.queueBind("queue.sh.a", "topic.exchange.sh", "stock.*");
	channel.queueBind("queue.sh.b", "topic.exchange.sh", "bond.*");
	channel.queueBind("queue.sz.a", "topic.exchange.sz", "#");
	
	// 发送消息
	Map<String, Object> headersMessage = new HashMap<String, Object>();
	headersMessage.put("market", "sh");
	AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder().headers(headersMessage);
	channel.basicPublish("headers.exchange", "bond.deal", properties.build(), "sh.bond.deal message".getBytes());

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

  运行程序后,查看Web管理界面,与设想的一致,消息出现在队列“queue.sh.b”中。

4.7 AE交换机

  一般情况下若消息的路由键与所有绑定队列都不匹配时,该消息将丢弃。RabbitMQ为交换机提供“AE交换机(Alternate Exchange)”属性,当发送到此交换机的消息没有转发到队列(即没有任意一个绑定队列的路由规则与消息匹配)时,该消息就被转发到“AE交换机”属性指定的交换机里。
  声明AE交换机,可使用rabbitmqctl工具或客户端API,其中官方网站推荐使用rabbitmqctl进行声明。
  API调用示例——

public static void main(String[] args) throws Exception {
	
	ConnectionFactory factory = new ConnectionFactory();
	// 设置服务端的地址、端口、用户名和密码...
	
	Connection connection = factory.newConnection();
	Channel channel = connection.createChannel();
	
	// 创建广播交换机ae.exchange和队列ae.queue并绑定
	channel.exchangeDeclare("ae.exchange", BuiltinExchangeType.FANOUT, true, false, null);
	channel.queueDeclare("ae.queue", true, false, false, null);
	channel.queueBind("ae.queue", "ae.exchange", "");
	
	// 创建直连交换机direct.exchange,其AE交换机为ae.exchange
	Map<String, Object> arguments = new HashMap<String, Object>();
	arguments.put("alternate-exchange", "ae.exchange");
	channel.exchangeDeclare("direct.exchange", BuiltinExchangeType.DIRECT, true, false, arguments);
	
	// 发送消息到direct.exchange,路由键为不存在的队列名
	channel.basicPublish("direct.exchange", "no_queue", null, "test".getBytes());
	
	channel.close();
	connection.close();
}

  运行程序后可以看到,交换机“direct.exchange”的特性一列中出现了“AE”,即表示该交换机拥有AE交换机。消息出现在了队列“ae.queue”中。这是由于没有任何队列与交换机“direct.exchange”绑定,消息转发到了它的AE交易机“ae.exchange”中,“ae.exchange”将其广播到了“ae.queue”。

  官方网站更推荐使用rabbitmqctl进行AE交换机的设置(可能由于这样更容易通过脚本实施),上文的AE交换机的声明可用以下命令代替:

rabbitmqctl set_policy AE "^direct.exchange$" '{"alternate-exchange":"ae.exchange"}'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值