RabbitMQ系列(三)RabbitMQ交换器Exchange介绍与实践

RabbitMQ交换器Exchange介绍与实践

导读

有了Rabbit的基础知识之后(基础知识详见:深入解读RabbitMQ工作原理及简单使用),本章我们重点学习一下Rabbit里面的exchange(交换器)的知识。

交换器分类

RabbitMQ的Exchange(交换器)分为四类:

  • direct(默认)
  • headers
  • fanout
  • topic

其中headers交换器允许你匹配AMQP消息的header而非路由键,除此之外headers交换器和direct交换器完全一致,但性能却很差,几乎用不到,所以我们本文也不做讲解。

注意:fanout、topic交换器是没有历史数据的,也就是说对于中途创建的队列,获取不到之前的消息。

1、direct交换器

direct为默认的交换器类型,也非常的简单,如果路由键匹配的话,消息就投递到相应的队列,如图:

image

使用代码:channel.basicPublish("", QueueName, null, message)推送direct交换器消息到对于的队列,空字符为默认的direct交换器,用队列名称当做路由键。

direct交换器代码示例

发送端:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
// 声明队列【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(config.QueueName, false, false, false, null);
String message = String.format("当前时间:%s", new Date().getTime());
// 推送内容【参数说明:参数一:交换机名称;参数二:队列名称,参数三:消息的其他属性-路由的headers信息;参数四:消息主体】
channel.basicPublish("", config.QueueName, null, message.getBytes("UTF-8"));

接收端,持续接收消息:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
// 声明队列【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(config.QueueName, false, false, false, null);
Consumer defaultConsumer = 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(workName + "收到消息 => " + message);
        channel.basicAck(envelope.getDeliveryTag(), false); // 手动确认消息【参数说明:参数一:该消息的index;参数二:是否批量应答,true批量确认小于当前id的消息】
    }
};
channel.basicConsume(config.QueueName, false, "", defaultConsumer);

接收端,获取单条消息

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(config.QueueName, false, false, false, null);
GetResponse resp = channel.basicGet(config.QueueName, false);
String message = new String(resp.getBody(), "UTF-8");
channel.basicAck(resp.getEnvelope().getDeliveryTag(), false); // 消息确认

持续消息获取使用:basic.consume;单个消息获取使用:basic.get。

注意:不能使用for循环单个消息消费来替代持续消息消费,因为这样性能很低;

消息的发后既忘特性

发后既往只的是接受者不知道消息的来源是谁发送的,如果想要指定消息的发送者,需要包含在发送内容里面,这点就像我们在信件里面注明自己的姓名一样,只有这样才能知道发送者是谁。

消息确认

看了上面的代码我们可以知道,消息接收到之后必须使用channel.basicAck()方法手动确认(非自动确认删除模式下),那么问题来了。

消息收到未确认会怎么样?

如果应用程序接收了消息,因为bug忘记确认接收的话,消息在队列的状态会从“Ready”变为“Unacked”,如图:

image

如果消息收到却未确认,Rabbit将不会再给这个应用程序发送更多的消息了,这是因为Rabbit认为你没有准备好接收下一条消息。

此条消息会一直保持Unacked的状态,直到你确认了消息,或者断开与Rabbit的连接,Rabbit会自动把消息改完Ready状态,分发给其他订阅者。

当然你可以利用这一点,让你的程序延迟确认该消息,直到你的程序处理完相应的业务逻辑,这样可以有效的防治Rabbit给你过多的消息,导致程序崩溃。

消息确认Demo:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(config.QueueName, false, false, false, null);
GetResponse resp = channel.basicGet(config.QueueName, false);
String message = new String(resp.getBody(), "UTF-8");
channel.basicAck(resp.getEnvelope().getDeliveryTag(), false);

channel.basicAck(long deliveryTag, boolean multiple)为消息确认,参数1:消息的id;参数2:是否批量应答,true批量确认小于次id的消息。

总结:消费者消费的每条消息都必须确认。

消息拒绝

消息在确认之前,可以有两个选择:

选择1:断开与Rabbit的连接,这样Rabbit会重新把消息分派给另一个消费者;

选择2:拒绝Rabbit发送的消息使用channel.basicReject(long deliveryTag, boolean requeue),参数1:消息的id;参数2:处理消息的方式,如果是true,Rabbib会重新分配这个消息给其他订阅者,如果设置成false的话,Rabbit会把消息发送到一个特殊的“死信”队列,用来存放被拒绝而不重新放入队列的消息。

消息拒绝Demo:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(config.QueueName, false, false, false, null);
GetResponse resp = channel.basicGet(config.QueueName, false);
String message = new String(resp.getBody(), "UTF-8");
channel.basicReject(resp.getEnvelope().getDeliveryTag(), true); //消息拒绝

2、fanout交换器——发布/订阅模式

fanout有别于direct交换器,fanout是一种发布/订阅模式的交换器,当你发送一条消息的时候,交换器会把消息广播到所有附加到这个交换器的队列上。

比如用户上传了自己的头像,这个时候图片需要清除缓存,同时用户应该得到积分奖励,你可以把这两个队列绑定到图片上传的交换器上,这样当有第三个、第四个上传完图片需要处理的需求的时候,原来的代码可以不变,只需要添加一个订阅消息即可,这样发送方和消费者的代码完全解耦,并可以轻而易举的添加新功能了。

和direct交换器不同,我们在发送消息的时候新增channel.exchangeDeclare(ExchangeName, "fanout"),这行代码声明fanout交换器。

发送端:

final String ExchangeName = "fanoutec"; // 交换器名称
Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "fanout"); // 声明fanout交换器
String message = "时间:" + new Date().getTime();
channel.basicPublish(ExchangeName, "", null, message.getBytes("UTF-8"));

接受消息不同于direct,我们需要声明fanout路由器,并使用默认的队列绑定到fanout交换器上。

接收端:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "fanout"); // 声明fanout交换器
String queueName = channel.queueDeclare().getQueue(); // 声明队列
channel.queueBind(queueName, ExchangeName, "");
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");
    }
};
channel.basicConsume(queueName, true, consumer);

fanout和direct的区别最多的在接收端,fanout需要绑定队列到对应的交换器用于订阅消息。

其中channel.queueDeclare().getQueue()为随机队列,Rabbit会随机生成队列名称,一旦消费者断开连接,该队列会自动删除。

注意:对于fanout交换器来说routingKey(路由键)是无效的,这个参数是被忽略的。

3、topic交换器——匹配订阅模式

最后介绍的是topic交换器,topic交换器运行和fanout类似,但是可以更灵活的匹配自己想要订阅的信息,这个时候routingKey路由键就排上用场了,使用路由键进行消息(规则)匹配。

假设我们现在有一个日志系统,会把所有日志级别的日志发送到交换器,warning、log、error、fatal,但我们只想处理error以上的日志,要怎么处理?这就需要使用topic路由器了。

topic路由器的关键在于定义路由键,定义routingKey名称不能超过255字节,使用“.”作为分隔符,例如:com.mq.rabbit.error。

消费消息的时候routingKey可以使用下面字符匹配消息:

  • "*"可以匹配所有内容;
  • "#"匹配0和多个字符;

例如发布了一个“com.mq.rabbit.error”的消息:

能匹配上的路由键:

  • cn.mq.rabbit.*

  • cn.mq.rabbit.#

  • #.error

  • cn.mq.#

  • #

不能匹配上的路由键:

  • cn.mq.*
  • *.error
  • *

所以如果想要订阅所有消息,可以使用“#”匹配。

注意:fanout、topic交换器是没有历史数据的,也就是说对于中途创建的队列,获取不到之前的消息。

发布端:

String routingKey = "com.mq.rabbit.error";
Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "topic"); // 声明topic交换器
String message = "时间:" + new Date().getTime();
channel.basicPublish(ExchangeName, routingKey, null, message.getBytes("UTF-8"));

接收端:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "topic"); // 声明topic交换器
String queueName = channel.queueDeclare().getQueue(); // 声明队列
String routingKey = "#.error";
channel.queueBind(queueName, ExchangeName, routingKey);
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(routingKey + "|接收消息 => " + message);
    }
};
channel.basicConsume(queueName, true, consumer);

扩展部分—自定义线程池

如果需要更大的控制连接,用户可自己设置线程池,代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService es = Executors.newFixedThreadPool(20);
Connection conn = factory.newConnection(es);

其实看过源码的同学可能知道,factory.newConnection本身默认也有线程池的机制,ConnectionFactory.class部分源码如下:

private ExecutorService sharedExecutor;
public Connection newConnection() throws IOException, TimeoutException {
        return newConnection(this.sharedExecutor, Collections.singletonList(new Address(getHost(), getPort())));
}
public void setSharedExecutor(ExecutorService executor) {
        this.sharedExecutor = executor;
}

其中this.sharedExecutor就是默认的线程池,可以通过setSharedExecutor()方法设置ConnectionFactory的线程池,如果不设置则为null。

用户如果自己设置了线程池,像本小节第一段代码写的那样,那么当连接关闭的时候,不会自动关闭用户自定义的线程池,所以用户必须自己手动关闭,通过调用shutdown()方法,否则可能会阻止JVM的终止。

官方的建议是只有在程序出现严重性能瓶颈的时候,才应该考虑使用此功能。

项目地址

GitHub:https://github.com/vipstone/rabbitmq-java.git

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 本章我们重点学习一下Rabbit里面的exchange(交换器)的知识。 交换器分类 RabbitMQ的Excha...
    Java大生阅读 347评论 0 1
  • 一、前言 RabbitMQ是一个开源的消息代理软件(面向消息的中间件),它的核心作用就是创建消息队列,异步接收和发...
    Java中文社群_老王阅读 930评论 0 50
  • 什么叫消息队列? 消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复...
    Agile_dev阅读 2,350评论 0 24
  • 今晚没有作业,都是回家复习,明天全部考试,家长们的心比孩子都激动,今晚接回来和儿子复习了一下,和他说了一下考试注意...
    2018级高佳浩妈妈阅读 209评论 0 1
  • 2019年12月3日,2:30报告会开始,2:00我就到达了会场,期待听到专家的报告,让自己有所改变,听毕...
    031a025ff3e6阅读 297评论 0 1