RabbitMQ:从零开始
一、介绍
RabbitMQ是基于AMQP协议的消息中间件,服务器端用Erlang语言编写,支持多种客户端,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
二、安装
去官网下载对应的版本,在安装rabbitmq之前需要安装对应Erlang环境,安装完成后通过命令/sbin/service rabbitmq-server start启动。
http://www.rabbitmq.com/download.html
三、基本配置
- 用户权限配置:rabbitmq默认的guest/guest用户只能在本机访问,如果需要外网用户访问,需要单独创建用户,通过命令创建用户及赋予权限 ,通过命令:./rabbitmqctl add_user test test ,./rabbitmqctl set_user_tags test administrator添加test用户和赋予admin权限。注:命令行创建的用户需要在接下来的web监控页面用户管理里设定权限(下图的Set permisson),否则无法连接成功。
- WEB监控配置:通过./rabbitmq-plugins enable rabbitmq_management命令开启web监控页面显示,默认端口15672,可通过localhost:15672查看web监控页面,方便后期管理和查看队列消息。
四、Java Demo
消息提供者Provider:
package org.rabbitmq.RabbitMq;
import java.io.IOException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* java访问mq基础demo --provider
* @author Mr.tanzc
*
*/
public class MySpreadSend {
//发送消息
public static void main(String[] args) throws IOException {
/*使用工厂类建立Connection和Channel,并且设置参数*/
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");//MQ的IP
factory.setPort(5672);//MQ端口
factory.setUsername("test");//MQ用户名
factory.setPassword("test");//MQ密码
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/*定义交换机*/
channel.exchangeDeclare("COLOR_EXCHANGE", "direct");
/*创建多个消息队列*/
channel.queueDeclare("BLACK_QUEUE", false, false, false, null);
channel.queueDeclare("RED_QUEUE", false, false, false, null);
/*绑定交换机和队列*/
channel.queueBind("BLACK_QUEUE", "COLOR_EXCHANGE", "black");
channel.queueBind("RED_QUEUE", "COLOR_EXCHANGE", "red");
/*通过交换机发送不同类别的消息到不同的队列中,注意,消息是由一个交换机来根据标志发往不同的队列中去*/
String message = "black";
channel.basicPublish("COLOR_EXCHANGE", "black", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
message="red";
channel.basicPublish("COLOR_EXCHANGE", "red", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
/*关闭连接*/
channel.close();
connection.close();
}
}
定义两个消费者Comsumer:
第一个消费者MySpreadRecvRed
package org.rabbitmq.RabbitMq;
import java.io.IOException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ShutdownSignalException;
public class MySpreadRecvRed {
public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException {
/*建立连接*/
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");//MQ的IP
factory.setPort(5672);//MQ端口
factory.setUsername("test");//MQ用户名
factory.setPassword("test");//MQ密码
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/*声明要连接的队列*/
/*定义交换机*/
channel.exchangeDeclare("COLOR_EXCHANGE", "direct");
/*绑定交换机和队列*/
channel.queueBind("RED_QUEUE", "COLOR_EXCHANGE", "red");
/*创建消费者对象,用于读取消息*/
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume("RED_QUEUE", true, consumer);
/* 读取队列,并且阻塞,即在读到消息之前在这里阻塞,直到等到消息,完成消息的阅读后,继续阻塞循环*/
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
}
第二个消费者MySpreadRecvBlack:
package org.rabbitmq.RabbitMq;
import java.io.IOException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ShutdownSignalException;
/**
* 获取指定队列的消息
* @author Mr.tanzc
*
*/
public class MySpreadRecvBlack {
public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException {
/*建立连接*/
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.1.25");//MQ的IP
factory.setPort(5672);//MQ端口
factory.setUsername("twk");//MQ用户名
factory.setPassword("twk");//MQ密码
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/*声明要连接的队列*/
/*定义交换机*/
channel.exchangeDeclare("COLOR_EXCHANGE", "direct");
/*绑定交换机和队列*/
channel.queueBind("BLACK_QUEUE", "COLOR_EXCHANGE", "black");
/*创建消费者对象,用于读取消息*/
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume("BLACK_QUEUE", true, consumer);
/* 读取队列,并且阻塞,即在读到消息之前在这里阻塞,直到等到消息,完成消息的阅读后,继续阻塞循环*/
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
}
先运行Provider,查看到mq监控台队列里面有消息,再运行Comsumer,可以看到打印出来的消费信息,队列里面对应数据清空。
五、基础API使用
主要对使用过的RabbitMQ一些概念和功能进行说明。
1.MQ使用流程
- 客户端连接到消息队列服务器,打开一个channel。
- 客户端声明一个exchange,并设置相关属性。
- 客户端声明一个queue,并设置相关属性。
- 客户端使用routing key,在exchange和queue之间建立好绑定关系。
- 客户端投递消息到exchange。
- exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
2.ConnectionFactory
连接工厂类,我们通过这个类可以设定一些连接参数
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");//MQ的IP
factory.setPort(5672);//MQ端口
factory.setUsername("test");//MQ用户名
factory.setPassword("test");//MQ密码
3.Connection
是通过工厂类New出来的rabbitMq连接,我们后续跟mq的交互都是基于这个连接。
Connection connection = factory.newConnection();
4.Channel 通道
进行消息读写的通道,可以理解为指向队列的路径
5.交换机Exchange
定义了消息路由规则
6.队列Queue
是存储消息的基本单元
7.Bind
绑定了Queue和Exchange,意即为符合什么样路由规则的消息,将会放置入哪一个消息队列
使用队列之前都需要用channel声明队列,channel.queueDeclare方法会在队列不存在的时候创建队列,如果队列存在,则不创建。
Channel channel = connection.createChannel();
/*定义交换机*/
channel.exchangeDeclare("COLOR_EXCHANGE", "direct");
/*创建多个消息队列*/
channel.queueDeclare("BLACK_QUEUE", false, false, false, null);
channel.queueDeclare("RED_QUEUE", false, false, false, null);
/*绑定交换机和队列*/
channel.queueBind("BLACK_QUEUE", "COLOR_EXCHANGE", "black");
channel.queueBind("RED_QUEUE", "COLOR_EXCHANGE", "red");
8.publish
发送消息到队列:通过channel.basicPublish方法来发送消息到指定队列,第一个参数是交换机名称,第二个参数是队列名称,第三个参数是用于优先级队列(后面再谈,若不适用为null),最后一个参数为消息内容的字节
/*通过交换机发送不同类别的消息到不同的队列中,注意,消息是由一个交换机来根据标志发往不同的队列中去*/
String message = "black";
channel.basicPublish("COLOR_EXCHANGE", "black", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
message="red";
channel.basicPublish("COLOR_EXCHANGE", "red", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
9.consume
消费者消费消息也需要创建通道,声明指定交换机和队列,然后通过consumer对象的basicConsume方法来绑定队列和消费者,第一个参数是队列名称,第二个参数为是否自动ack
nextDelivery(long timeout)方法可以获取消息,参数为最多等待时间,如果在这个时间内获取到消息,则返回消息,若无,将会最多等待这个时间,仍没有消息数据就会返回null。
再通过consumer.nextDelivery().getBody()方法获取消息内容
/*创建消费者对象,用于读取消息*/
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume("RED_QUEUE", false, consumer);
/* 读取队列,并且阻塞,即在读到消息之前在这里阻塞,直到等到消息,完成消息的阅读后,继续阻塞循环*/
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
六、ACK机制
QUEUE里面的消息存在一个ACK机制,即消息的确认机制。
channel.basicConsume(queueName, false, consumer)
第二个参数就是是否自动ack。
在实际项目应用中,消费者拿到消息进行处理需要一段时间,中间因为用户中止操作或者因为网络问题宕机时,如果设置为自动ack,这个消费就会丢失。为防止这种情况的出现,一般设置为取消auto ack,需要消费者发送确认回执后才从队列中删除,确保消息能够被正确消费。
消费者处理完消息后可通过
channel.basicAck(delivery .getEnvelope().getDeliveryTag(), false);
来确认ack消息,这个方法的第一个参数是消息的标识tag,第二个为是否重新放入队列,设置为false消息会从队列里面删除,true的话会重新放入队列,等待再次消费。
七、消息的持久化
MQ的消息是支持持久化操作的,会将数据保存到硬盘里面,防止因为服务器宕机或者RabbitMQ重启后的数据丢失。我们要分别设置队列的持久化(重启后队列名称恢复)和消息的持久化(重启后数据保留)。
队列的持久化:
channel.queueDeclare(queueName, true, false, false, null);
第一个参数为队列名称,第二个参数即是否持久化操作,设置为true即实现队列信息的持久化
消息的持久化:
channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
其中第三个参数MessageProperties.PERSISTENT_TEXT_PLAIN即设置消息的持久化
八、消息的公平分发
如果有多个消费者监听同一队列,MQ默认会把消息平摊给多个消费者,在不同消费者处理时间不同的情况下,就有可能造成某个消费者堆积了很多消息未处理而另外一个消费者无消息可处理的情况,为避免这种情况,我们可以通过
channel.basicQos(1,true);
方法设置每个消费者的分发数量,1即代表这个消费者每次最多处理一个消息,如果要拿下一个消息,必须把未ack的消息ack掉以后才能拿到下一个消息,这样就实现了消息的公平分发机制。
九、消息的优先级
队列的消息默认是先进先出的,这也是RabbitMQ默认支持的队列。
3.5.0版本之前的MQ默认是不支持优先级队列的,只能通过插件安装的方式来实现。
3.5.0版本之后的MQ已经集成了这一功能,可以直接使用。
首先,我们需要声明优先级队列:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-max-priority", 100);
channel.queueDeclare(queueName, true, false, false, args);
queueDeclare方法的最后一个参数之前设置为null,即默认的非优先级队列,这么我们传递一个包含key为x-max-priority的map作为参数,就可以创建一个优先级的队列,100的数值设定队列最大支持优先级的数字。
注:这里需要注意的是,在第一次声明队列并创建队列后,后续使用这个队列时的声明如果属性不一致的话,会报错,必须声明相同属性的相同队列。
接下来,我们放入消息时传递消息的优先级:
BasicProperties props = MessageProperties.PERSISTENT_BASIC.builder().priority(1).build();
channel.basicPublish("", queueCheckName, props, message.getBytes());
首先定义一个BasicProperties的变量,传递一个优先级为1的变量,再将这个参数传递到
basicPublish方法的第三个参数(即之前持久化的参数,MessageProperties.PERSISTENT_BASIC.builder()方法也是持久化的)
十、消息的路由分发
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种(AMQP规范里还提到两种Exchange Type,分别为system与自定义,这里不予以描述)。
默认的是direct直接发送,如上文中queueDeclare方法中不指定交换机,而直接指定队列名称,就是默认的交换机,将消息放到对应名称的队列中。这种形式在发送和消费消息的时候都需要指定对应队列名称。
fanout即广播形式,通过交换机将消息发到绑定的所有队列上面。
// 声明一个名称为"exchange_fanout"的exchange
channel.exchangeDeclare("exchange_fanout", "fanout");
// 将消息发送给exchange
channel.basicPublish("exchange_fanout", "", null, msg.getBytes());
topic即匹配模式,我们可以为每一个消息类型设定一个主题topic,放入队列的时候带上topic,消费者获取消息的时候可以通过指定topic来获取消息。
消息提供者:
//指定topic类型的交换机
String exchange = "exchange03";
String msgType= "type1";
channel.exchangeDeclare(exchange , "topic") ;
String msg = "Hello World!";
//发送指定主题的消息
channel.basicPublish( exchange , msgType, null , msg.getBytes());
消息消费者:
String exchange = "exchange03";
channel.exchangeDeclare(exchange , "topic") ;
String queueName = channel.queueDeclare().getQueue() ;
//第三个参数就是type
channel.queueBind(queueName, exchangeName, "type1") ;
QueueingConsumer consumer = new QueueingConsumer(channel) ;
channel.basicConsume(queueName, true, consumer) ;
//循环获取消息
while(true){
//获取消息,如果没有消息,这一步将会一直阻塞,可以设定超时时间
Delivery delivery = consumer.nextDelivery() ;
String msg = new String(delivery.getBody()) ;
System.out.println("received message[" + msg + "] from " + exchangeName);
}
十一、Spring集成
RabbitMQ可以和Spring无缝集成,就无需跟client打交道,使用更为方便。
spring-rabbit.xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit-1.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<description>rabbitmq 连接服务配置</description>
<context:component-scan base-package="com.mop.self.mq"/>
<context:property-placeholder location="classpath:conf/rabbitmq.properties"/>
<!-- 连接配置 -->
<rabbit:connection-factory id="connectionFactory" host="${mq.host}" username="${mq.username}" password="${mq.password}" port="${mq.port}"/>
<rabbit:admin connection-factory="connectionFactory"/>
<!-- spring template声明-->
<rabbit:template exchange="test-mq-exchange" id="amqpTemplate" connection-factory="connectionFactory" message-converter="jsonMessageConverter" />
<!-- 队列说明 -->
<rabbit:queue id="test_queue" name="test_queue" durable="true" auto-delete="false" exclusive="false" />
<!-- direct 交换器 -->
<rabbit:direct-exchange name="test-mq-exchange" durable="false" auto-delete="false" id="test-mq-exchange">
<rabbit:bindings>
<rabbit:binding queue="test_queue" key="test_queue_key"/>
</rabbit:bindings>
</rabbit:direct-exchange>
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="auto" message-converter="jsonMessageConverter">
<rabbit:listener ref="mqListener" queues="test_queue" />
</rabbit:listener-container>
<!-- 消息对象json转换类 -->
<bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter" />
</beans>
rabbitmq.propreties:
mq.host=127.0.0.1
mq.username=tzc
mq.password=tzc
mq.port=5672
定义MQ消息传输类和生产消费者:
MqMessage:
package com.mop.self.mq;
/**
* Author: Mr.tan
* Date: 2017/08/04
* Description:MQ消息封装
*/
public class MqMessage {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "MqMessage{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
MqProducer:
package com.mop.self.mq;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Author: Mr.tan
* Date: 2017/08/04
* Description: MQ生产者
*/
@Component
public class MqProducer {
private static final Logger logger = LoggerFactory.getLogger(MqProducer.class);
@Autowired
private AmqpTemplate amqpTemplate;
public void sendMessage(Object message){
logger.debug("produce message:"+message);
amqpTemplate.convertAndSend("test_queue_key",message);
}
}
MqListener:
package com.mop.self.mq;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;
/**
* Author: Mr.tan
* Date: 2017/08/04
* Description: mq监听
*/
@Component
public class MqListener implements MessageListener {
private static final Logger logger = LoggerFactory.getLogger(MqListener.class);
public void onMessage(Message message) {
MqMessage mqMessage = JSON.parseObject(new String(message.getBody()),MqMessage.class);
logger.debug("get message:"+mqMessage);
}
}
测试代码:
package com.mop.self.mq;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Author: Mr.tan
* Date: 2017/08/04
* Description:
*/
public class MqTest {
public static void main(String[]args){
ApplicationContext applicationContext=new ClassPathXmlApplicationContext("classpath:conf/spring-core.xml");
MqProducer mqProducer = (MqProducer) applicationContext.getBean("mqProducer");
MqMessage mqMessage = new MqMessage();
mqMessage.setId("34");
mqMessage.setName("测试");
mqProducer.sendMessage(mqMessage);
}
}
执行方法,生产者发送消息,监听获取到消息并打印