转载请注明出处

0.目录

RabbitMQ-从基础到实战(1)— Hello RabbitMQ

RabbitMQ-从基础到实战(2)— 防止消息丢失

RabbitMQ-从基础到实战(4)— 消息的交换(中)

RabbitMQ-从基础到实战(5)— 消息的交换(下)

RabbitMQ-从基础到实战(6)— 与Spring集成

1.简介

在前面的例子中,每个消息都只对应一个消费者,即使有多个消费者在线,也只会有一个消费者接收并处理一条消息,这是消息中间件的一种常用方式。

另外一种方式,生产者生产一条消息,广播给一个或多个队列,所有订阅了这个队列的消费者,都可以消费这条消息,这就是消息订阅。

官方教程列举了这样一个场景,生产者发出一条记录日志的消息,消费者1接收到后写日志到硬盘,消费者2接收到后打印日志到屏幕。工作中还有很多这样的场景有待发掘,适当的使用消息订阅后可以成倍的增加效率。

2.RabbitMQ的交换中心(Exchange)

在前两章的例子中,我们涉及到了三个概念

  1. 生产者
  2. 队列
  3. 消费者

这不禁让我们以为,生产者生产消息后直接到发送到队列,消费者从队列中获取消息,再消费掉。

其实这是错误的,在RabbitMQ中,生产者不会直接把消息发送给队列,实际上,生产者甚至不知道一条消息会不会被发送到队列上。

正确的概念是,生产者会把消息发送给RabbitMQ的交换中心(Exchange),Exchange的一侧是生产者,另一侧则是一个或多个队列,由Exchange决定一条消息的生命周期--发送给某些队列,或者直接丢弃掉。

这个概念在官方文档中被称作RabbitMQ消息模型的核心思想(core idea)

如下图,其中X代表的是Exchange。

RabbitMQ中,有4种类型的Exchange

  • direct    通过消息的routing key比较queue的key,相等则发给该queue,常用于相同应用多实例之间的任务分发

    • 默认类型   本身是一个direct类型的exchange,routing key自动设置为queue name。注意,direct不等于默认类型,默认类型是在queue没有指定exchange时的默认处理方式,发消息时,exchange字段也要相应的填成空字符串“”
  • topic    话题,通过可配置的规则分发给绑定在该exchange上的队列,通过地理位置推送等场景适用
  • headers    当分发规则很复杂,用routing key不好表达时适用,忽略routing key,用header取代之,header可以为非字符串,例如Integer或者String
  • fanout    分发给所有绑定到该exchange上的队列,忽略routing key,适用于MMO游戏、广播、群聊等场景

更详细的介绍,请看官方文档

3.临时队列

可以对一个队列命名是十分重要的,在消费者消费消息时,要指明消费哪个队列的消息(下面的queue),这样就可以让多个消费者同时分享一个队列

String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;

上述记录日志的场景中,有以下几个特点

  • 所有消费者都需要监听所有的日志消息,因此每个消费者都需要一个单独的队列,不需要和别人分享
  • 消费者只关心最新的消息,连接到RabbitMQ之前的消息不需要关心,因此,每次连接时需要创建一个队列,绑定到相应的exchange上,连接断开后,删除该队列

自己声明队列是比较麻烦的,因此,RabbitMQ提供了简便的获取临时队列的方法,该队列会在连接断开后销毁

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

这行代码会获取一个名字类似于“amq.gen-JzTY20BRgKO-HjmUJj0wLg”的临时队列

4.绑定

再次注意,在RabbitMQ中,消息是发送到Exchange的,不是直接发送的Queue。因此,需要把Queue和Exchange进行绑定,告诉RabbitMQ把指定的Exchange上的消息发送的这个队列上来

绑定队列使用此方法

Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;

其中,queue是队列名,exchange是要绑定的交换中心,routingKey就是这个queue的routingKey

5.实践

下面来实现上述场景,生产者发送日志消息,消费者1记录日志,消费者2打印日志

下面的代码中,把连接工厂等方法放到了构造函数中,也就是说,每new一个对象,都会创建一个连接,在生产环境这样做是很浪费性能的,每次创建一个connection都会建立一次TCP连接,生产环境应使用连接池。而Channel又不一样,多个Channel是共用一个TCP连接的,因此可以放心的获取Channel(本结论出自官方文档对Channel的定义)

AMQP 0-9-1 connections are multiplexed with channels that can be thought of as "lightweight connections that share a single TCP connection".

For applications that use multiple threads/processes for processing, it is very common to open a new channel per thread/process and not share channels between them.

日志消息发送类 LogSender

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory; public class LogSender { private Logger logger = LoggerFactory.getLogger(LogSender.class);
private ConnectionFactory factory;
private Connection connection;
private Channel channel; /**
* 在构造函数中获取连接
*/
public LogSender(){
super();
try {
factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
connection = factory.newConnection();
channel = connection.createChannel();
} catch (Exception e) {
logger.error(" [X] INIT ERROR!",e);
}
}
/**
* 提供个关闭方法,现在并没有什么卵用
* @return
*/
public boolean closeAll(){
try {
this.channel.close();
this.connection.close();
} catch (IOException | TimeoutException e) {
logger.error(" [X] CLOSE ERROR!",e);
return false;
}
return true;
} /**
* 我们更加关心的业务方法
* @param message
*/
public void sendMessage(String message) {
try {
//声明一个exchange,命名为logs,类型为fanout
channel.exchangeDeclare("logs", "fanout");
//exchange是logs,表示发送到此Exchange上
//fanout类型的exchange,忽略routingKey,所以第二个参数为空
channel.basicPublish("logs", "", null, message.getBytes());
logger.debug(" [D] message sent:"+message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

在LogSender中,和之前的例子不一样的地方是,我们没有直接声明一个Queue,取而代之的是声明了一个exchange

发布消息时,第一个参数填了我们声明的exchange名字,routingKey留空,因为fanout类型忽略它。

在前面的例子中,我们routingKey填的是队列名,因为默认的exchange(exchange位空字符串时使用默认交换中心)会把队列的routingKey设置为queueName(声明队列的时候设置的,不是发送消息的时候),又是direct类型,所以可以通过queueName当做routingKey找到队列。

消费类 LogConsumer

 package com.liyang.ticktock.rabbitmq;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 LogConsumer { private Logger logger = LoggerFactory.getLogger(LogConsumer.class);
private ConnectionFactory factory;
private Connection connection;
private Channel channel; /**
* 在构造函数中获取连接
*/
public LogConsumer() {
super();
try {
factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
connection = factory.newConnection();
channel = connection.createChannel();
// 声明exchange,防止生产者没启动,exchange不存在
channel.exchangeDeclare("logs","fanout");
} catch (Exception e) {
logger.error(" [X] INIT ERROR!", e);
}
} /**
* 提供个关闭方法,现在并没有什么卵用
*
* @return
*/
public boolean closeAll() {
try {
this.channel.close();
this.connection.close();
} catch (IOException | TimeoutException e) {
logger.error(" [X] CLOSE ERROR!", e);
return false;
}
return true;
} /**
* 我们更加关心的业务方法
*/
public void consume() {
try {
// 获取一个临时队列
String queueName = channel.queueDeclare().getQueue();
// 把刚刚获取的队列绑定到logs这个交换中心上,fanout类型忽略routingKey,所以第三个参数为空
channel.queueBind(queueName, "logs", "");
//定义一个Consumer,消费Log消息
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");
logger.debug(" [D] 我是来打印日志的:"+message);
}
};
//这里自动确认为true,接收到消息后该消息就销毁了
channel.basicConsume(queueName, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}

复制一个项目,把72行改为如下代码,代表两个做不同工作的消费者

 logger.debug(" [D] 我已经把消息写到硬盘了:"+message);

消费者App

 public class App
{
public static void main( String[] args )
{
LogConsumer consumer = new LogConsumer();
consumer.consume();
}
}

生产者App

 public class App {
public static void main( String[] args ) throws InterruptedException{
LogSender sender = new LogSender();
while(true){
sender.sendMessage(System.nanoTime()+"");
Thread.sleep(1000);
}
}
}

把消费者打包成两个可执行的jar包,方便观察控制台

用java -jar 命令执行,结果如下

6.结束语

本章介绍了RabbitMQ中消息的交换,再次强调,RabbitMQ中,消息是通过交换中心转发到队列的,不要被默认的exchange混淆,默认的exchange会自动把queue的名字设置为它的routingKey,所以消息发布时,才能通过queueName找到该队列,其实此时queueName扮演的角色就是routingKey。

本教程是参考官方文档写出来的,后续章节会介绍更多RabbitMQ的相关知识以及项目中的实战技巧

RabbitMQ-从基础到实战(3)— 消息的交换(上)的更多相关文章

  1. RabbitMQ-从基础到实战(3)— 消息的交换

    1.简介 在前面的例子中,每个消息都只对应一个消费者,即使有多个消费者在线,也只会有一个消费者接收并处理一条消息,这是消息中间件的一种常用方式.还有另外一种方式,生产者生产一条消息,广播给所有的消费者 ...

  2. RabbitMQ-从基础到实战(2)— 防止消息丢失

    转载请注明出处 1.简介 RabbitMQ中,消息丢失可以简单的分为两种:客户端丢失和服务端丢失.针对这两种消息丢失,RabbitMQ都给出了相应的解决方案. 2.防止客户端丢失消息 如图,生产者P向 ...

  3. RabbitMQ-从基础到实战(4)— 消息的交换(下)

    0.目录 RabbitMQ-从基础到实战(1)- Hello RabbitMQ RabbitMQ-从基础到实战(2)- 防止消息丢失 RabbitMQ-从基础到实战(3)- 消息的交换(上) 1.简介 ...

  4. RabbitMQ-从基础到实战(5)— 消息的交换(下)

    转载请注明出处 0.目录 RabbitMQ-从基础到实战(1)- Hello RabbitMQ RabbitMQ-从基础到实战(2)- 防止消息丢失 RabbitMQ-从基础到实战(3)- 消息的交换 ...

  5. RabbitMQ-从基础到实战(6)— 与Spring集成

    0.目录 RabbitMQ-从基础到实战(1)- Hello RabbitMQ RabbitMQ-从基础到实战(2)- 防止消息丢失 RabbitMQ-从基础到实战(3)- 消息的交换(上) Rabb ...

  6. RabbitMQ消息的交换

    消息的交换 目录 RabbitMQ-从基础到实战(1)— Hello RabbitMQ RabbitMQ-从基础到实战(2)— 防止消息丢失 1.简介 在前面的例子中,每个消息都只对应一个消费者,即使 ...

  7. RabbitMQ-从基础到实战(1)— Hello RabbitMQ

    转载请注明出处 1.简介 本篇博文介绍了在windows平台下安装RabbitMQ Server端,并用JAVA代码实现收发消息 2.安装RabbitMQ RabbitMQ是用Erlang开发的,所以 ...

  8. RabbitMQ-从基础到实战(4)— 消息的交换(中)

    转自:https://www.cnblogs.com/4----/p/6590459.html 1.简介 本章节和官方教程相似度较高,英文好的可以移步官方教程 在上一章的例子中,我们创建了一个消费者, ...

  9. C# RabbitMQ延迟队列功能实战项目演练

    一.需求背景 当用户在商城上进行下单支付,我们假设如果8小时没有进行支付,那么就后台自动对该笔交易的状态修改为订单关闭取消,同时给用户发送一份邮件提醒.那么我们应用程序如何实现这样的需求场景呢?在之前 ...

随机推荐

  1. zabbix_windowsagent_cpu

    zabbix的WEB端--配置-模板--Template OS Windows--项目--创建项目 名称:UserPerfCountercpu 键值:UserPerfCountercpu 数据类型:数 ...

  2. Netty入门(八)构建Netty HTTP/HTTPS应用

    HTTP/HTTPS 是最常见的一种协议,这节主要是看一下 Netty 提供的 ChannelHaandler. 一.HTTP Decoder,Encoder 和 Codec HTTP 是请求-响应模 ...

  3. BZOJ3155:Preprefix sum(线段树)

    Description Input 第一行给出两个整数N,M.分别表示序列长度和操作个数 接下来一行有N个数,即给定的序列a1,a2,....an 接下来M行,每行对应一个操作,格式见题目描述 Out ...

  4. ingress 代理方式

    一: 单域名 + 多工程 apiVersion: extensions/v1beta1 kind: Ingress metadata: name: test annotations: nginx.in ...

  5. DJI Mobile SDK 新教程

    DJI Mobile SDK 新教程发布! http://bbs.dji.com/thread-20282-1-1.html Android 如何创建一个航拍相机App: 你将学到如何配置DJI Mo ...

  6. mybatis的一对一,一对多查询,延迟加载,缓存介绍

    一对一查询 需求 查询订单信息关联查询用户信息 sql语句 /*通过orders关联查询用户使用user_id一个外键,只能关联查询出一条用户记录就可以使用内连接*/ SELECT orders.*, ...

  7. Storm 运行例子

    1.建立Java工程 使用idea,添加lib库,拷贝storm中lib到工程中 2.拷贝wordcount代码 下载src包,解压找到 apache-storm-0.9.4-src\apache-s ...

  8. QQ 的一些URI 协议命令

    //System.Diagnostics.Process.Start(@"C:\Program Files\Tencent\TIM\Bin\Timwp.exe", "te ...

  9. 20155313 杨瀚 《网络对抗技术》实验九 Web安全基础

    20155313 杨瀚 <网络对抗技术>实验九 Web安全基础 一.实验目的 本实践的目标理解常用网络攻击技术的基本原理.Webgoat实践下相关实验. 二.基础问题回答 1.SQL注入攻 ...

  10. 一、InnoDB引擎

    一.InnoDB的历史 MYSQL的5.1版本的时候还是使用旧的innoDB,当时orale公司推出的新的innoDB引擎, 但是需要以插件的形式编译,叫innoDB plugin : 知道MYSQL ...