Hello World一对一的简单模式。生产者直接发送消息给RabbitMQ,另一端消费。未定义和指定Exchange的情况下,使用的是AMQP default这个内置的Exchange。
HelloWorldSender.java
/**
* Rabbitmq是一个消息broker:接收消息,传递给下游应用
* *
术语:
* Producing就是指发送消息,发送消息的程序是Producer
* Queue指的是RabbitMQ内部的一个组件,消息存储于queue中。queue使用主机的内存和磁盘存
储,收到内存和磁盘空间的限制
* 可以想象为一个大的消息缓冲。很多Producer可以向同一个queue发送消息,很多消费者
可以从同一个queue消费消息。
* Consuming就是接收消息。一个等待消费消息的应用程序称为Consumer
* *
生产者、消费者、队列不必在同一台主机,一般都是在不同的主机上的应用。一个应用可以同时是
生产者和消费者。
* */
public class HelloWorldSender {
private static String QUEUE_NAME = "hello";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("node1");
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("123456");
factory.setPort(5672);
try (Connection conn = factory.newConnection();
Channel channel = conn.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, true, null);
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
} catch (Exception e) {
}
}
}
HelloWorldReceiver.java
public class HelloWorldReceiver {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
// 连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置服务器主机名或IP地址
factory.setHost("node1");
// 设置Erlang的虚拟主机名称
factory.setVirtualHost("/");
// 设置用户名
factory.setUsername("root");
// 设置密码
factory.setPassword("123456");
// 设置客户端与服务器的通信端口,默认值为5672
factory.setPort(5672);
// 获取连接
Connection connection = factory.newConnection();
// 从连接获取通道
Channel channel = connection.createChannel();
// 声明一个队列
// 第一个参数是队列名称,第二个参数false表示在rabbitmq-server重启后就没有了
// 第三个参数表示该队列不是一个排外队列,否则一旦客户端断开,队列就删除了
// 第四个参数表示该队列是否自动删除,true表示一旦不使用了,系统删除该队列
// 第五个参数表示该队列的参数,该参数是Map集合,用于指定队列的属性
// channel.queueDeclare(QUEUE_NAME, false, false, true, null);
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit pressCTRL+C");
// 消息的推送回调函数
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
/*
使用服务器生成的consumerTag启动本地,非排他的使用者。
启动一个
仅提供了basic.deliver和basic.cancel AMQP方法(对大多数情形够用了)
第一个参数:队列名称
autoAck – true 只要服务器发送了消息就表示消息已经被消费者确认; false服务
端等待客户端显式地发送确认消息
deliverCallback – 服务端推送过来的消息回调函数
cancelCallback – 客户端忽略该消息的回调方法
Returns:
服务端生成的consumerTag
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag-> { });
}
}
生产者和消费者,需要与RabbitMQ Broker 建立TCP连接,也就是Connection 。一旦TCP 连接建立起来,客户端紧接着创建一个AMQP 信道(Channel),每个信道都会被指派一个唯一的ID。信道是建立在Connection 之上的虚拟连接, RabbitMQ 处理的每条AMQP 指令都是通过信道完成的。
为什么不直接使用TCP连接,而是使用信道?
RabbitMQ 采用类似NIO的做法,复用TCP 连接,减少性能开销,便于管理。
当每个信道的流量不是很大时,复用单一的Connection 可以在产生性能瓶颈的情况下有效地节省TCP 连接资源。
当信道本身的流量很大时,一个Connection 就会产生性能瓶颈,流量被限制。需要建立多个Connection ,分摊信道。具体的调优看业务需要。
信道在AMQP 中是一个很重要的概念,大多数操作都是在信道这个层面进行的
channel.exchangeDeclare
channel.queueDeclare
channel.basicPublish
channel.basicConsume
// ...
RabbitMQ 相关的API与AMQP紧密相连,比如channel.basicPublish 对应AMQP 的Basic.Publish命令。
官网地址:https://www.rabbitmq.com/getstarted.htm
生产者发消息,启动多个消费者实例来消费消息,每个消费者仅消费部分信息,可达到负载均衡的效果。
Consumer:
public class Consumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 保险起见,先声明一下,如果RabbitMQ的虚拟主机中有该队列,当然好,如果没有,则创建
// 此处的队列应该和生产者声明的队列属性等一致
channel.queueDeclare("queue.wq", true, false, false, null);
channel.basicConsume("queue.wq", new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(new String(message.getBody(), "utf-8"));
}
}, new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("consumerTag : " + consumerTag);
}
});
// channel.close();
// connection.close();
}
}
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 声明消息队列
channel.queueDeclare("queue.wq", true, false, false, null);
//声明一个交换器
channel.exchangeDeclare("ex.wq", BuiltinExchangeType.DIRECT, true, false, null);
// 将交换器绑定到消息队列,同时指定绑定键(binding-key)
// channel.exchangeBind("queue.wq", "ex.wq", "key.wq");
channel.exchangeBind("ex.wq", "queue.wq", "key.wq");
for (int i = 0; i < 15; i++) {
channel.basicPublish("ex.wq", "key.wq", null, ("工作队列:" + i).getBytes("utf-8"));
}
// 关闭通道
channel.close();
// 关闭连接
connection.close();
}
}
使用fanout类型交换器,routingKey忽略。每个消费者定义生成一个队列并绑定到同一个Exchange,每个消费者都可以消费到完整的消息。
消息广播给所有订阅该消息的消费者。
在RabbitMQ中,生产者不是将消息直接发送给消息队列,实际上生产者根本不知道一个消息被发送到哪个队列。
生产者将消息发送给交换器。交换器非常简单,从生产者接收消息,将消息推送给消息队列。交换器必须清楚地知道要怎么处理接收到的消息。应该是追加到一个指定的队列,还是追加到多个队列,还是丢弃。规则就是交换器类型。
交换器的类型前面已经介绍过了: direct 、 topic 、 headers 和 fanout 四种类型。发布订阅使用fanout。创建交换器,名字叫 logs :
fanout 交换器很简单,从名字就可以看出来(用风扇吹出去),将所有收到的消息发送给它知道的所有的队列。
rabbitmqctl list_exchanges
列出RabbitMQ的交换器,包括了 amq.* 的和默认的(未命名)的交换器。
未命名交换器
在前面的那里中我们没有指定交换器,但是依然可以向队列发送消息。这是因为我们使用了默认的交换器。
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数就是交换器名称,为空字符串。直接使用routingKey向队列发送消息,如果该routingKey指定的队列存在的话。
现在,向指定的交换器发布消息
channel.basicPublish("logs", "", null, message.getBytes());
临时队列
前面我们使用队列的名称,生产者和消费者都是用该名称来发送和接收该队列中的消息
首先,我们无论何时连接RabbitMQ的时候,都需要一个新的,空的队列。我们可以使用随机的名字创建队列,也可以让服务器帮我们生成随机的消息队列名字。
其次,一旦我们断开到消费者的连接,该队列应该自动删除。
String queueName = channel.queueDeclare().getQueue();
上述代码我们声明了一个非持久化的、排他的、自动删除的队列,并且名字是服务器随机生成的。
queueName一般的格式类似: amq.gen-JzTY20BRgKO-HjmUJj0wLg 。
在创建了消息队列和 fanout 类型的交换器之后,我们需要将两者进行绑定,让交换器将消息发送给该队列。
channel.queueBind(queueName, "logs", "");
此时, logs 交换器会将接收到的消息追加到我们的队列中。
可以使用下述命令列出RabbitMQ中交换器的绑定关系:
rabbitmqctl list_bindings
Producer:
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 声明fanout类型的交换器
channel.exchangeDeclare("ex.myfan", "fanout", true, false, null);
for (int i = 0; i < 20; i++) {
channel.basicPublish("ex.myfan",
"", // fanout类型的交换器不需要指定路由键
null,
("hello world fan:" + i).getBytes("utf-8"));
}
channel.close();
connection.close();
}
}
OneConsumer:
public class OneConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 声明临时队列,队列的名字由RabbitMQ自动生成
final String queueName = channel.queueDeclare().getQueue();
System.out.println("生成的临时队列的名字为:" + queueName);
channel.exchangeDeclare("ex.myfan",
BuiltinExchangeType.FANOUT,
true,
false,
null);
// fanout类型的交换器绑定不需要routingkey
channel.queueBind(queueName, "ex.myfan", "");
channel.basicConsume(queueName, (consumerTag, message) -> {
System.out.println("One " + new String(message.getBody(), "utf-8"));
}, consumerTag -> {});
}
}
当消费者启动起来之后,命令 rabbitmqctl list_bindings 列出绑定关系:
消息的推拉:
实现RabbitMQ的消费者有两种模式,推模式(Push)和拉模式(Pull)。 实现推模式推荐的方式是继承 DefaultConsumer 基类,也可以使用Spring AMQP的 SimpleMessageListenerContainer 。 推模式是最常用的,但是有些情况下推模式并不适用的,比如说: 由于某些限制,消费者在某个条件成立时才能消费消息 需要批量拉取消息进行处理 实现拉模式 RabbitMQ的Channel提供了 basicGet 方法用于拉取消息。
使用 direct 类型的Exchange,发N条消费并使用不同的 routingKey ,消费者定义队列并将队列、 routingKey 、Exchange绑定。此时使用 direct 模式Exchagne必须要 routingKey 完全匹配的情况下消息才会转发到对应的队列中被消费。
上一个模式中,可以将消息广播到很多接收者。
现在我们想让接收者只接收部分消息,如,我们通过直接模式的交换器将关键的错误信息记录到log文件,同时在控制台正常打印所有的日志信息。
绑定
上一模式中,交换器的使用方式:
channel.queueBind(queueName, EXCHANGE_NAME, "");
绑定语句中还有第三个参数: routingKey :
channel.queueBind(queueName, EXCHANGE_NAME, "black");
bindingKey 的作用与具体使用的交换器类型有关。对于 fanout 类型的交换器,此参数设置无效,系统直接忽略
分布式系统中有很多应用,这些应用需要运维平台的监控,其中一个重要的信息就是服务器的日志记录
我们需要将不同日志级别的日志记录交给不同的应用处理。
如何解决?
使用direct交换器如果要对不同的消息做不同的处理,此时不能使用 fanout 类型的交换器,因为它只会盲目的广播消息。
我们需要使用 direct 类型的交换器。 direct 交换器的路由算法很简单:只要消息的routingKey 和队列的 bindingKey 对应,消息就可以推送给该队
列。
上图中的交换器 X 是 direct 类型的交换器,绑定的两个队列中,一个队列的 bindingKey 是orange ,另一个队列的 bindingKey 是 black 和 green 。
如此,则 routingKey 是 orange 的消息发送给队列Q1, routingKey 是 black 和 green 的消息发送给Q2队列,其他消息丢弃。
上图中,我们使用 direct 类型的交换器 X ,建立了两个绑定:队列Q1根据 bindingKey 的值black 绑定到交换器 X ,队列Q2根据 bindingKey 的值 black 绑定到交换器 X ;交换器 X 会将消息发送给队列Q1和队列Q2。交换器的行为跟 fanout 的行为类似,也是广播。
在案例中,我们将日志级别作为 routingKey
Producer:
public class Producer {
private final static String[] LOG_LEVEL = {
"ERROR",
"FATAL",
"WARN"
};
private static Random random = new Random();
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 声明direct类型的交换器,交换器和消息队列的绑定不需要在这里处理
channel.exchangeDeclare("ex.routing", "direct", false, false, null);
for (int i = 0; i < 100; i++) {
String level = LOG_LEVEL[random.nextInt(100) % LOG_LEVEL.length];
channel.basicPublish("ex.routing", level, null, ("这是【" + level + "】的消息").getBytes());
}
}
}
WarnConsumer:
public class WarnConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("ex.routing", "direct", false, false, null);
// 此处也可以声明为临时消息队列
channel.queueDeclare("queue.warn", false, false, false, null);
channel.queueBind("queue.warn", "ex.routing", "WARN");
channel.basicConsume("queue.warn", ((consumerTag, message) -> {
System.out.println("WarnConsumer收到的消息:" + new String(message.getBody(), "utf-8"));
}), consumerTag -> { });
}
}
ErrorConsumer:
public class ErrorConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("ex.routing", "direct", false, false, null);
// 此处也可以声明为临时消息队列
channel.queueDeclare("queue.error", false, false, false, null);
channel.queueBind("queue.error", "ex.routing", "ERROR");
channel.basicConsume("queue.error", ((consumerTag, message) -> {
System.out.println("ErrorConsumer收到的消息:" + new String(message.getBody(), "utf-8"));
}), consumerTag -> { });
}
}
FatalConsumer:
public class FatalConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("ex.routing", "direct", false, false, null);
// 此处也可以声明为临时消息队列
channel.queueDeclare("queue.fatal", false, false, false, null);
channel.queueBind("queue.fatal", "ex.routing", "FATAL");
channel.basicConsume("queue.fatal", ((consumerTag, message) -> {
System.out.println("FatalConsumer收到的消息:" + new String(message.getBody(), "utf-8"));
}), consumerTag -> { });
}
}
使用 topic 类型的交换器,队列绑定到交换器、 bindingKey 时使用通配符,交换器将消息路由转发到具体队列时会根据消息 routingKey 模糊匹配,比较灵活。
上个模式中,我们通过 direct 类型的交换器做到了根据日志级别的不同,将消息发送给了不同队列的。
这里有一个限制,加入现在我不仅想根据日志级别划分日志消息,还想根据日志来源划分日志,怎么做?
比如,我想监听cron服务发送的 error 消息,又想监听从kern服务发送的所有消息。
此时可以使用RabbitMQ的主题模式( Topic )
要想 topic 类型的交换器, routingKey 就不能随便写了,它必须得是点分单词。单词可以随便写,生产中一般使用消息的特征。如:“stock.usd.nyse”,“nyse.vmw”,“quick.orange.rabbit”等。该点分单词字符串最长255字节。
bindingKey 也必须是这种形式。 topic 类型的交换器背后原理跟 direct 类型的类似:只要队列的 bindingKey 的值与消息的 routingKey 匹配,队列就可以收到该消息。有两个不同:
上图中,我们发送描述动物的消息。消息发送的时候指定的 routingKey 包含了三个词,两个点。
第一个单词表示动物的速度,第二个是颜色,第三个是物种:..。
创建三个绑定:Q1绑定到" .orange. “Q2绑定到” ..rabbit “和” lazy.# "。
如果不能匹配,就丢弃消息。
如果发送的消息 routingKey 是" lazy.orange.male.rabbit ",则会匹配最后一个绑定
如果在 topic 类型的交换器中 bindingKey 使用 # ,则就是 fanout 类型交换器的行为。
如果在 topic 类型的交换器中 bindingKey 中不使用 * 和 # ,则就是 direct 类型交换器的行为。
Producer:
public class Producer {
private static final String[] LOG_LEVEL = {"info", "error", "warn"};
private static final String[] LOG_AREA = {"beijing", "shanghai", "shenzhen"};
private static final String[] LOG_BIZ = {"edu-online", "biz-online", "emp-online"};
private static final Random RANDOM = new Random();
public static void main(String[] args) throws Exception {
final ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("ex.topic", "topic", true, false, null);
String area, level, biz;
String routingKey, message;
for (int i = 0; i < 100; i++) {
area = LOG_AREA[RANDOM.nextInt(LOG_AREA.length)];
level = LOG_LEVEL[RANDOM.nextInt(LOG_LEVEL.length)];
biz = LOG_BIZ[RANDOM.nextInt(LOG_BIZ.length)];
// routingKey中包含了三个维度
routingKey = area + "." + biz + "." + level;
message = "LOG: [" + level + "] :这是 [" + area + "] 地区 [" + biz + "] 服务器发来的消息,MSG_SEQ = " + i;
channel.basicPublish("ex.topic", routingKey, null, message.getBytes("utf-8"));
}
channel.close();
connection.close();
}
}
BeijingConsumer:
public class BeijingConsumer {
public static void main(String[] args) throws Exception {
final ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 临时队列,返回值是服务器为该队列生成的名称
final String queue = channel.queueDeclare().getQueue();
channel.exchangeDeclare("ex.topic", "topic", true, false, null);
// beijing.biz-online.error
// 只要routingKey是以beijing开头的,后面不管几个点分单词,都可以接收
channel.queueBind(queue, "ex.topic", "beijing.#");
channel.basicConsume(queue, (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "utf-8"));
}, consumerTag -> {});
}
}
ShanghaiErrorConsumer:
public class ShanghaiErrorConsumer {
public static void main(String[] args) throws Exception {
final ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 临时队列,返回值是服务器为该队列生成的名称
final String queue = channel.queueDeclare().getQueue();
channel.exchangeDeclare("ex.topic", "topic", true, false, null);
// beijing.biz-online.error
channel.queueBind(queue, "ex.topic", "shanghai.*.error");
channel.basicConsume(queue, (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "utf-8"));
}, consumerTag -> {});
}
}
ShenZhenEmponlineConsumer:
public class ShenZhenEmponlineConsumer {
public static void main(String[] args) throws Exception {
final ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 临时队列,返回值是服务器为该队列生成的名称
final String queue = channel.queueDeclare().getQueue();
channel.exchangeDeclare("ex.topic", "topic", true, false, null);
// beijing.biz-online.error
channel.queueBind(queue, "ex.topic", "shenzhen.emp-online.*");
channel.basicConsume(queue, (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "utf-8"));
}, consumerTag -> {});
}
}