最近在看消息队列的书籍,把一些收获总结一下。
首先说说什么是消息队列。这里就不说那种教科书的定义了,以我的理解,消息队列就是通过接收和发送消息,使不同的应用系统连接起来。实现了业务系统的解耦,也跨越了系统编写语言的限制。总结来说,消息队列在当下分布式系统中的应用场景可归纳如下:
1.异步RPC;
2.增强性能拓展性,并行处理不同业务;
3.构建日志告警系统,针对不同日志级别发送不同告警;
目前比较流行的消息队列框架有ActiveMQ、RabbitMQ、RocketMQ和Kafka等等。本篇先写写RabbitMQ,按照惯例还是推荐下面这本《RabbitMQ实战》,本文大部分内容总结于此(但此书是基于python语言实现的,特此说明):
RabbitMQ的介绍
RabbitMQ的设计之初就是为了解决金融业系统的分发消息问题的,这就奠定了它安全稳定的基础。而且在当下流行的开源消息队列解决方案中,它也可能是唯一一个遵循AMQP规范实现的(AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制)。
先说一下RabbitMQ的大体模型吧。RabbitMQ主要由生产者、消费者、消息、信道(channel)、交换器(exchange)和队列(queue)组成。
生产者先通过tcp连接到RabbitMQ服务器,然后在tcp连接中创建一条AMQP信道,信道中可以声明交换器、队列及它们之间的绑定关系(bind)。当信道的各种信息设置好后,就可以生产消息并发布到信道中了。
消费者也是同理,先连接到RabbitMQ服务器,建立一条tcp连接。当tcp连接打开后,消费者就可以创建一条AMQP信道(channel)。设置好信道上的各种信息后就可以开始接收消息了。
AMQP消息包含两部分,有效载荷(payload)和标签(label)。有效载荷就是想传输的数据,标签是用来决定谁将获得消息。标签通常包括一个交换器的名称和可选的主体标记,rabbit会根据标签把消息发送给接收方,不过在路由的过程中则不会带上标签。
信道是建立在真实的tcp连接内的虚拟连接,AMQP命令都是通过信道发送出去的。每条信道都会被指派一个唯一id(AMQP会记住)。因为创建和销毁tcp连接开销很大,所以有信道的存在。不同线程可以使用不同的信道,而且互不影响。
AMQP消息路由有三个部分:交换器(exchange)、队列(queue)和绑定(bind)。生产者把消息发布到交换器上,消息最终到达队列并被消费者接收;绑定决定了消息如何从路由器路由到特定的队列。
具体来说,绑定(Bind)是交换器(Exchange)和队列(Queue)绑定的规则描述,可解析当交换器接收到的消息中路由键(RoutingKey)这个字段,根据这个路由键和当前交换器所有的绑定做匹配,如果满足则向所绑定的队列(Queue)发送消息。这样我们就解决了我们向RabbitMQ发送一次消息,可以分发到不同的Queue的过程。
三种消息路由模式
RabbitMQ共支持4种消息路由模式,分别为direct、fanout、topic和head。其中head模式是基于消息的head信息进行投递的,应用场景并不多,这里着重介绍前面三种模式。
direct:交换器和队列是一对一的关系,消费者在接收消息的时候交换器和队列的名称必须和生产者定义的完全匹配。
参考代码示例
生产者:
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
com.rabbitmq.client.Connection connection = cf.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("direct_queue", true, false, false, null);
for (int i = 0; i < 1000; i++) {
channel.basicPublish("", "direct_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, ("test"+i).getBytes());
System.out.println("消息"+i+"已发送");
}
channel.close();
connection.close();
消费者:
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
final Connection connection = cf.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare("xinzun_queue", true, false, false, null);
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, com.rabbitmq.client.Envelope envelope, com.rabbitmq.client.AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("收到消息"+ message);
try {
Thread.sleep(1000);
} catch (Exception e) {
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("xinzun_queue", false, consumer);
上面代码我们使用了一个名字为空字符串的交换器。这是因为RabbitMQ要求服务器必须实现direct类型交换器,包含一个空白字符串名称的默认交换器。当声明一个队列时会自动绑定到默认交换器,并以队列名称作为路由键。
fanout:把消息投递给所有绑定在此交换器上的队列。
使用此模式时,生产者可以在发布消息的时候,只在信道上声明fanout类型的交换器而不绑定队列给交换器,绑定的操作留给消费者去完成。
代码示例
生产者:
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
com.rabbitmq.client.Connection connection = cf.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("fanout_exchange", "fanout",false,false,null);
for (int i = 0; i < 1000; i++) {
channel.basicPublish("fanout_exchange", "", MessageProperties.PERSISTENT_TEXT_PLAIN, ("test"+i).getBytes());
System.out.println("消息"+i+"已发送");
}
channel.close();
connection.close();
消费者:
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
final Connection connection = cf.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, "fanout_exchange", "");
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, com.rabbitmq.client.Envelope envelope, com.rabbitmq.client.AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("收到消息"+ message);
try {
Thread.sleep(1000);
} catch (Exception e) {
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("fanout_exchange", false, consumer);
topic:交换器和队列可以通过模糊绑定,“#”表示0个或若干个关键字,“*”表示一个关键字。关键字之间通过“.”分隔。
示例代码
生产者:
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
com.rabbitmq.client.Connection connection = cf.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topic_exchange", "topic",false,false,null);
for (int i = 0; i < 1000; i++) {
channel.basicPublish("topic_exchange", "topic.info.msg", MessageProperties.PERSISTENT_TEXT_PLAIN, ("test"+i).getBytes());
System.out.println("消息"+i+"已发送");
}
channel.close();
connection.close();
消费者:
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
final Connection connection = cf.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("topic_exchange", "topic",false,false,null);
channel.queueDeclare("info_queue2", true, false, false, null);
channel.queueBind("info_queue2", "topic_exchange", "#.info.#");
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, com.rabbitmq.client.Envelope envelope, com.rabbitmq.client.AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println(consumerTag+"收到消息"+ message);
try {
Thread.sleep(1000);
} catch (Exception e) {
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("info_queue2", false, consumer);
下一篇文章: RabbitMQ知识盘点【贰】_实现原理及RabbitMQ集群