1. 写在前面

昨天简单学习了一个消息队列项目——RabbitMQ,今天趁热打铁,将学到的东西记录下来。

学习的资料主要是官网给出的6个基本的消息发送/接收模型,或者称为6种不同的使用场景,本文便是对这6种模型加以叙述。

2. Tutorials

在学习6种模型之前,我们首先需要安装RabbitMQ。RabbitMQ支持多种系统平台,各平台的安装方法可以点此查看。安装好之后,我们使用如下命令启用Web端的管理插件:rabbitmq-plugins enable rabbitmq_management,然后启动RabbitMQ。接着用浏览器访问http://localhost:15672/,若能看到RabbitMQ相关Web页面,说明启动成功。

2.1 Hello World

正所谓万事开头难,我们先从最简单的Hello World开始。首先当然是新建一个项目,导入RabiitMQ相关jar。我采用Maven来构建项目,因此只需要在pom文件中添加如下依赖:

<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5</version>
</dependency>

接下来学习最简单的消息队列模型,如下图:

在图中,P代表producer,它是消息的生产者C代表consumer,它是消息的消费者;而红色的矩形正是我们所谓的消息队列,它位于RabbitMQ中(RabbitMQ中可以有很多这样的队列,并且每个队列都有一个唯一的名字)。生产者(们)可以将消息发送到消息队列中,消费者(们)可以从消息队列中取出消息。

这种模型是不是很简单呢?下面我们使用Java,借助于RabbitMQ来实现这种模型的消息通信。

首先我们介绍如何send消息到消息队列。send之前,当然是和RabbitMQ服务器建立连接:

ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();

接下来我们创建一个channel,大多数API都是通过这个对象来调用的:

Channel channel = connection.createChannel();

之后,我们便可以调用channel的如下方法去声明一个队列:

channel.queueDeclare("hello", false, false, false, null);

该方法的第一个参数是队列的名称,其余的参数先不管,之后会介绍。我们可以尝试着去执行以上的5行代码,然后打开Web端,可以看到新建了一个叫作hello的队列:

有了队列,我们便可以向其中发送消息了,同样还是调用channel对象的API:

channel.basicPublish("", "hello", null, "Hello World".getBytes());

以上代码所做的事情就是发送了一条字符串消息“Hello World”(第4个参数)到消息队列。你可能注意到我们调用了String对象的getBytes方法,没错,我们发送的实际上二进制数据。因此,理论上你能够发送任何数据到消息队列中,而不仅仅是文本信息。

第2个参数叫做路由键(routingKey),在该模型下必须与队列名相同,至于为什么,和其他参数一样,之后会了解到。

我们可以修改发送的文本,再次执行上述代码,然后打开Web端查看,便可以查看到我们发送的消息:

点击上图的name字段下的hello,可以查看hello队列中的具体信息:

接下来,我们去尝试着去获取生产者发送的消息,和send方法一样,我们同样需要连接服务器,创建channel,声明队列:

ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false, false, null);

之后我们可以调用channel的相关方法去监听队列,接收消息:

channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
}
});

以上basicConsume方法中,第一个参数是队列的名字;第二个参数表示是否自动确认消息的接收情况,我们使用true,自动确认;第三个参数需要传入一个实现了Consumer接口的对象,我们简单的new一个默认的Consumer的实现类DefaultConsumer,然后在handleDelivery方法中去处理接收到的消息(handleDelivery方法会在接收到消息时被回调)。

运行以上代码,我们可以打印出之前向队列中send的数据:

Hello World
Hello World2

下面是Hello World的完整代码:

public class App {

    @Test
public void send() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false, false, null); channel.basicPublish("", "hello", null, "Hello World2".getBytes()); channel.close();
connection.close();
} @Test
public void receive() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false, false, null); channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
} });
synchronized (this){
// 因为以上接收消息的方法是异步的(非阻塞),当采用单元测试方式执行该方法时,程序会在打印消息前结束,因此使用wait来防止程序提前终止。若使用main方法执行,则不需要担心该问题。
wait();
}
}
}

2.2 Work queues

接下来我们学习第二种模型——Work Queues。顾名思义,这种模型描述的是一个生产者(Boss)向队列发消息(任务),多个消费者(worker)从队列接受消息(任务),如下图所示:

下面我们用代码去实现。先是生产者send消息到队列,这次我们多发送些数据:

@Test
public void send() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false, false, null); for (int i = 0; i < 9; i++) {
channel.basicPublish("", "hello", null, String.valueOf(i).getBytes());
} channel.close();
connection.close();
}

然后是消费者接收数据:

@Test
public void receive() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false, false, null); channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
try {
// Thread.sleep(1000);
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
synchronized (this) {
wait();
}
}

代码基本上和Hello World的代码一样,只是加上句sleep来模拟消费者(worker)处理消息所花的时间。

我们可以先执行三次receive方法(修改sleep的时间,其中消费者1 sleep 10s,消费者2,3 sleep 1s),让三个消费者(worker)一起等待消息的到来,然后执行send方法发送9条消息,观察三个消费者收到的消息情况。

若不出意外,你会看到如下的打印结果:

// --------消费者1--------
0
// 10s 后
3
// 10s 后
6
// --------消费者2--------
1
// 1s 后
4
// 1s 后
7
// --------消费者3--------
2
// 1s 后
5
// 1s 后
8

通过打印结果,我们可以总结出Work queues的几个特点:

  1. 一条消息只会被一个消费者接收;
  2. 消息是平均分配给消费者的;
  3. 消费者只有在处理完某条消息后,才会收到下一条消息。

事实上,RabbitMQ会循环地(一个接一个地)发送消息给消费者,这种分配消息的方式被称为round-robin(轮询)

2.2.1 消息确认

看到这里,不知你是否会担心:由于worker(消费者)执行任务需要一定的时间(以上用sleep模拟),要是某个worker在运行过程中挂掉,那分配给它的任务岂不是丢失了(永远不可能被执行了)。为解决这个问题,RabbitMQ提供了消息确认机制,即worker需要主动的去确认消息已经接收了,RabbitMQ才认为消息被“真正地接收了”,实现代码如下:

// send的代码不用变,只需改变basicConsume的第二个参数为false,表示不要自动确认
channel.basicConsume("hello", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
try {
// 这里把时间加长了一点便于测试
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 这里手动地确定
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});

下面做测试。首先执行send方法,发送9条消息到队列,查看web端情况如下:

此时队列中有9条未被分发的消息。接着运行改变后的receive方法,然后快速地去Web端查看队列中的消息情况(记得刷新):

发现队列中没有待分发(Ready字段)的消息了,而有9条未被确认(Unacked字段)的消息。但控制台打印出数字6时,关闭程序,再次去web端查看:

此时队列中又有3条待分发的消息了。原因正是由于我们提前终止了receive方法的执行,导致最后3条消息没有被确认而被重新归还到Ready中。

2.2.2 消息持久化

如果你不是一次性跟着本文运行代码到这里,而是第二天接着昨天的工作继续进行,你可能会发现昨天你创建的队列和添加到队列里的消息没有了。很可能的原因就是消息没有持久化,即按照以上代码运行生成的队列和添加到队列中的消息都是储存在内存中的,RabbitMQ一旦关闭它们就没有了。如果你想将下次启动时还能看到关闭前的消息,你应该将其持久化:

// 将第二个参数设为true,表示声明一个需要持久化的队列。
// 需要注意的是,若你已经定义了一个非持久的,同名字的队列,要么将其先删除(不然会报错),要么换一个名字。
channel.queueDeclare("hello", true, false, false, null);
// 修改了第三个参数,这是表明消息需要持久化
channel.basicPublish("", "hello",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());

总的来说,Work queues(Task Queuess)的概念在一些Web场景的应用中是很有用的,比如我们能够用它来构建一个master-slave结构的分布式爬虫系统:系统中有一个master节点和多个slave节点,master节点负责向各个slave节点分配爬取任务。

2.3 Publish/Subscribe

但有些时候,我们可能希望一条消息能够被多个消费者接受到,比如一些公告信息等,这时候用Work Queue模型显然不合适,而Publish/Subscribe模型正是对应这种使用场景的。

在介绍Publish/Subscribe之前,我们快速回顾之前的两个模型,它们好像都是生产者将消息直接发送到消息队列,但其实不是这样的,甚至有可能生产者根本就不知道消息发送到了哪一个消息队列。

先别着急,下面我们完整地介绍RabbitMQ消息发送/接受的方式。

事实上,生产者是把消息发送到了交换机(exchange)中,然后交换机负责(决定)将消息发送到(哪一个)消息队列中。其模型如下图:

这时候你可能会疑惑:既然消息是被发送到了交换机中,那我们之前发送的消息是被发送到了哪一个交换机中了?它有没有机制能够让特定的消息发送到指定的队列?

先回答第一个问题。还记得我们在Hello World中写的发送消息的代码吗?

channel.basicPublish("", "hello", null, message.getBytes());

事实上第一个参数便是指定交换机的名字,即指定消息被发送到哪一个交换机。空字符串表示默认交换机(Default Exchange),即我们之前发送的消息都是先发送到默认交换机,然后它再路由到相应的队列中。其实我们可以通过Web页面去查看所有存在的交换机:

接着回答第二个问题。路由的依据便是通过第二个参数——路由键(routing key)指定的,之前已经提到过。在之前代码中,我们指定第二个参数为"hello",便是指定消息应该被交换机路由到路由键为hello的队列中。而默认交换机(Default Exchange)有一个非常有用的性质:

每一个被创建的队列都会被自动的绑定到默认交换机上,并且路由键就是队列的名字。

交换机还有4种不同的类型,分别是directfanouttopicheaders,每种类型路由的策略不同。

direct类型的交换机要求和它绑定的队列带有一个路由键K,若有一个带有路由键R的消息到达了交换机,交换机会将此消息路由到路由键K = R的队列。默认交换机便是该类型。因此,在下图中,消息会沿着绿色箭头路由:

fanout类型的交换机会路由每一条消息到所有和它绑定的队列,忽略路由键。

剩下的两种类型之后再做介绍。

在以上概念基础上,我们来看第3种消息模型:Publish/Subscribe。如下图:

该模型是要让所有的消费者都能够接收到每一条消息。显然,fanout类型的交换机更符合我们当前的需求。为此,先创建一个fanout类型的交换机。

channel.exchangeDeclare("notice", "fanout");

其中,第一个参数是交换机的名称;第二个参数是交换机的类型。

然后我们可以send消息了:

channel.basicPublish( "notice", "", null, message.getBytes());

对于消费者,我们需要为每一个消费者创建一个独立的队列,然后将队列绑定到刚才指定的交换机上即可:

// 该方法会创建一个名称随机的临时队列
String queueName = channel.queueDeclare().getQueue();
// 将队列绑定到指定的交换机("notice")上
channel.queueBind(queueName, "notice", "");

以下完整的代码:

@Test
public void send() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("notice", "fanout");
channel.basicPublish( "notice", "", null, "Hello China".getBytes());
channel.close();
connection.close();
} @Test
public void receive() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("notice", "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, "notice", ""); channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
} });
synchronized (this) {
wait();
}
}

首先运行两次receive方法,让两个消费者等待接收消息,然后可以在Web端查看此时的队列情况,如下图所示:

可以看到图中有两个名称随机的队列。接着运行send方法发送一条消息,最终我们会看到两个消费者都打印出了Hello China。然后停止虚拟机让消费者断开连接,再次在Web端查看队列情况,会发现刚才的两个队列被自动删除了。

2.4 Routing

以上是Publish/Subscribe模式,它已经能让我们的通知(notice)系统正常运转了。现在再考虑这样一个新需求:对于一些机密通知,我们只想让部分人看到。这就要求交换机对绑定在其上的队列进行筛选,于是引出了又一个新的模型:Routing

之前我们说过,对于direct类型的交换机,它会根据routing key进行路由,因此我们可以借助它来实现我们的需求,模型结构如下图:

下面用代码来实现。先看生产者。

首先要声明一个direct类型的交换机:

// 这里名称改为notice2
channel.exchangeDeclare("notice2", "direct");

需要注意的是,因为我们之前声明了一个fanout类型的名叫notice的交换机,因此不能再声明一个同名的类型却不一样的交换机。

然后可以发送消息了,我们发送10条消息,其中偶数条消息是秘密消息,只能被routing key 为s的队列接受,其余的消息所有的队列都能接受。

for (int i = 0; i < 10; i++) {
String routingKey = "n"; // normal
if (i % 2 == 0) {
routingKey = "s"; // secret
}
channel.basicPublish("notice2", routingKey, null, String.valueOf(i).getBytes());
}

接下来是消费者:

// 声明一个名称随机的临时的队列
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机,同时带上routing key
channel.queueBind(queueName, "notice2", "n");
// 消费者2号运行时,打开以下注释
// channel.queueBind(queueName, "notice2", "s");

注意,我们可以多次调用队列绑定方法,调用时,队列名和交换机名都相同,而routing key不同,这样可以使一个队列带有多个routing key

以下是完整代码:

@Test
public void send() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel(); channel.exchangeDeclare("notice2", "direct");
for (int i = 0; i < 10; i++) {
String routingKey = "n"; // normal
if (i % 2 == 0) {
routingKey = "s"; // secret
}
channel.basicPublish("notice2", routingKey, null, String.valueOf(i).getBytes());
}
channel.close();
connection.close();
} @Test
public void receive() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("notice2", "direct"); String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, "notice2", "n");
// channel.queueBind(queueName, "notice2", "s"); channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
}
});
synchronized (this) {
wait();
}
}

测试时,我们可以先运行一个receive,然后打开channel.queueBind(queueName, "notice2", "s")注释,再运行一次receive,这样就有两个消费者绑定到notice2交换机上,其中消费者1只能收到normal类型的消息,而消费者2既能收到normal类型的消息,又能收到secret类型的消息。接着可以运行send方法。如不出意外,可以看到如下打印结果:

// 消费者1
1
3
5
7
9
// 消费者2
0
1
2
3
4
5
6
7
8
9

2.5 Topic

有了以上的改进,我们的notice系统基本ok了。但有些时候,我们还需要更加灵活的消息刷选方式。比如我们对于电影信息,我们可能需要对它的地区,类型,限制级进行筛选。这时候就要借助Topics模型了。

Topics模型中,我们“升级”了routing key,它可以由多个关键词组成,词与词之间由点号(.)隔开。特别地,规定*表示任意的一个词;#号表示任意的0个或多个词

假设我们现在需要接收电影信息,每条电影消息附带的routingKey有地区、类型、限制级3个关键字,即:district.type.age。现在想实现的功能如下图:

如上图所示,队列Q1只关心美国适合13岁以上的电影信息,队列Q2对动作片感兴趣,而队列Q3喜欢中国电影。

下面用Java代码去实现上述功能,相较于之前基本上没有什么改变,下面直接给出代码:

@Test
public void send() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel(); channel.exchangeDeclare("movie", "topic"); channel.basicPublish("movie", "American.action.13", null, "The Bourne Ultimatum".getBytes());
channel.basicPublish("movie", "American.comedy.R", null, "The Big Lebowski".getBytes());
channel.basicPublish("movie", "American.romantic.13", null, "Titanic".getBytes()); channel.basicPublish("movie", "Chinese.action.13", null, "卧虎藏龙".getBytes());
channel.basicPublish("movie", "Chinese.comedy.13", null, "大话西游".getBytes());
channel.basicPublish("movie", "Chinese.romantic.13", null, "梁山伯与祝英台".getBytes()); channel.close();
connection.close();
} @Test
public void receive() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("movie", "topic");
// 队列1
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, "movie", "American.*.13");
// 队列2
// String queueName = channel.queueDeclare().getQueue();
// channel.queueBind(queueName, "movie", "*.action.*");
// 队列3
// String queueName = channel.queueDeclare().getQueue();
// channel.queueBind(queueName, "movie", "Chinese.#"); channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, "UTF-8"));
} });
synchronized (this) {
wait();
}
}

运行3次receive方法,注意打开或关闭相应的注释;再运行send方法,可以看到控制台输出如下内容:

// 消费者1
The Bourne Ultimatum
Titanic
// 消费者2
The Bourne Ultimatum
卧虎藏龙
// 消费者3
卧虎藏龙
大话西游
梁山伯与祝英台

2.6 RPC

第6种模型是用来做RPC(Remote procedure call, 远程程序调用)的。这里直接贴上代码,就不做解释了,想要了解更多细节,可参考这里。代码演示的是,客户端调用服务端的fib方法,得到返回结果。

RPCServer.java

import com.rabbitmq.client.*;
import com.rabbitmq.client.AMQP.BasicProperties; /**
* Description:
*
* @author derker
* @Time 2016-10-26 18:24
*/
public class RPCServer { private static final String RPC_QUEUE_NAME = "rpc_queue"; private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
} public static void main(String[] argv) {
Connection connection = null;
Channel channel = null;
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
System.out.println(" [x] Awaiting RPC requests");
while (true) {
String response = null;
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
AMQP.BasicProperties props = delivery.getProperties();
BasicProperties replyProps = new BasicProperties
.Builder()
.correlationId(props.getCorrelationId())
.build();
try {
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
response = "" + fib(n);
} catch (Exception e) {
System.out.println(" [.] " + e.toString());
response = "";
} finally {
channel.basicPublish("", props.getReplyTo(), replyProps, response.getBytes("UTF-8"));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (Exception ignore) {
}
}
}
}
}

RPCClient.java


import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties;
import java.util.UUID; /**
* Description:
*
* @author derker
* @Time 2016-10-26 18:36
*/
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
private QueueingConsumer consumer; public RPCClient() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel(); replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
} public String call(String message) throws Exception {
String response = null;
String corrId = UUID.randomUUID().toString(); BasicProperties props = new BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build(); channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8")); while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response = new String(delivery.getBody(), "UTF-8");
break;
}
} return response;
} public void close() throws Exception {
connection.close();
} public static void main(String[] argv) {
RPCClient fibonacciRpc = null;
String response = null;
try {
fibonacciRpc = new RPCClient();
System.out.println(" [x] Requesting fib(10)");
response = fibonacciRpc.call("10");
System.out.println(" [.] Got '" + response + "'");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fibonacciRpc != null) {
try {
fibonacciRpc.close();
} catch (Exception ignore) {
}
}
}
}
}

消息队列——RabbitMQ学习笔记的更多相关文章

  1. RabbitMQ学习笔记五:RabbitMQ之优先级消息队列

    RabbitMQ优先级队列注意点: 1.只有当消费者不足,不能及时进行消费的情况下,优先级队列才会生效 2.RabbitMQ3.5以后才支持优先级队列 代码在博客:RabbitMQ学习笔记三:Java ...

  2. RabbitMQ学习笔记1-hello world

    安装过程略过,一搜一大把. rabbitmq管理控制台:http://localhost:15672/   默认账户:guest/guest RabbitMQ默认监听端口:5672 JAVA API地 ...

  3. 官网英文版学习——RabbitMQ学习笔记(十)RabbitMQ集群

    在第二节我们进行了RabbitMQ的安装,现在我们就RabbitMQ进行集群的搭建进行学习,参考官网地址是:http://www.rabbitmq.com/clustering.html 首先我们来看 ...

  4. 官网英文版学习——RabbitMQ学习笔记(一)认识RabbitMQ

    鉴于目前中文的RabbitMQ教程很缺,本博主虽然买了一本rabbitMQ的书,遗憾的是该书的代码用的不是java语言,看起来也有些不爽,且网友们不同人学习所写不同,本博主看的有些地方不太理想,为此本 ...

  5. RabbitMQ学习笔记(五) Topic

    更多的问题 Direct Exchange帮助我们解决了分类发布与订阅消息的问题,但是Direct Exchange的问题是,它所使用的routingKey是一个简单字符串,这决定了它只能按照一个条件 ...

  6. (一)RabbitMQ消息队列-RabbitMQ的优劣势及产生背景

    原文:(一)RabbitMQ消息队列-RabbitMQ的优劣势及产生背景 本篇并没有直接讲到技术,例如没有先写个Helloword.我想在选择了解或者学习一门技术之前先要明白为什么要现在这个技术而不是 ...

  7. ASP.NET Core消息队列RabbitMQ基础入门实战演练

    一.课程介绍 人生苦短,我用.NET Core!消息队列RabbitMQ大家相比都不陌生,本次分享课程阿笨将给大家分享一下在一般项目中99%都会用到的消息队列MQ的一个实战业务运用场景.本次分享课程不 ...

  8. .NET 开源工作流: Slickflow流程引擎高级开发(七)--消息队列(RabbitMQ)的集成使用

    前言:工作流流程过程中,除了正常的人工审批类型的节点外,事件类型的节点处理也尤为重要.比如比较常见的事件类型的节点有:Timer/Message/Signal等.本文重点阐述消息类型的节点处理,以及实 ...

  9. C#中使用消息队列RabbitMQ

    在C#中使用消息队列RabbitMQ 2014-10-27 14:41 by qy1141, 745 阅读, 2 评论, 收藏, 编辑 1.什么是RabbitMQ.详见 http://www.rabb ...

随机推荐

  1. Git Bash的一些命令和配置

    查看git版本号: git --version 如果是第一次使用Git,你需要设置署名和邮箱: $ git config --global user.name "用户名" $ gi ...

  2. 结巴分词3--基于汉字成词能力的HMM模型识别未登录词

    作者:zhbzz2007 出处:http://www.cnblogs.com/zhbzz2007 欢迎转载,也请保留这段声明.谢谢! 1 算法简介 在 结巴分词2--基于前缀词典及动态规划实现分词 博 ...

  3. ASP.NET MVC5+EF6+EasyUI 后台管理系统(65)-MVC WebApi 用户验证 (1)

    系列目录 前言: WebAPI主要开放数据给手机APP,其他需要得知数据的系统,或者软件应用,所以移动端与系统的数据源往往是相通的. Web 用户的身份验证,及页面操作权限验证是B/S系统的基础功能, ...

  4. 简记某WebGIS项目的优化之路

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1. 背景 该项目为研究生时的老师牵头,个人已毕业数年,应老师要求协助其 ...

  5. 【夯实PHP基础】nginx php-fpm 输出php错误日志

    本文地址 原文地址 分享提纲: 1.概述 2.解决办法(解决nginx下php-fpm不记录php错误日志) 1. 概述 nginx是一个web服务器,因此nginx的access日志只有对访问页面的 ...

  6. AFNetworking报错"_UTTypeCopyPreferredTagWithClass", referenced from: _AFContentTypeForPathExtens

    问题: 在和Unity交互的过程中,从Unity开发工具打包出来的项目文件,在添加AFNetworking库,运行时报出以下错误: Undefined symbols for architecture ...

  7. 手机游戏渠道SDK接入工具项目分享(二)万事开头难

    一般接到任务后程序员们通常都开始着手进行技术调研了,但我这活是项目负责人.还有一大堆事情要先期准备,没人能帮忙. 一.人力配置 考虑的之前已经有一波人搞了大半年,但没有起色,先期也没有太大人力需求,所 ...

  8. How to accept Track changes in Microsoft Word 2010?

    "Track changes" is wonderful and remarkable tool of Microsoft Word 2010. The feature allow ...

  9. spring boot1

    spring boot 玩转spring boot--快速开始   开发环境: IED环境:Eclipse JDK版本:1.8 maven版本:3.3.9 一.创建一个spring boot的mcv ...

  10. java根据html生成摘要

    转自:http://java.freesion.com/article/48772295755/ 开发一个系统,需要用到这个,根据html生成你指定多少位的摘要 package com.chendao ...