这章主要对使用过的RabbitMQ一些概念和功能进行说明。
(1)客户端连接到消息队列服务器,打开一个channel。
(2)客户端声明一个exchange,并设置相关属性。
(3)客户端声明一个queue,并设置相关属性。
(4)客户端使用routing key,在exchange和queue之间建立好绑定关系。
(5)客户端投递消息到exchange。
exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
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密码
Connection:是通过工厂类New出来的rabbitMq连接,我们后续跟mq的交互都是基于这个连接。
Connection connection = factory.newConnection();
Channel 通道:进行消息读写的通道,可以理解为指向队列的路径
交换机Exchange定义了消息路由规则
队列Queue是存储消息的基本单元
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");
发送消息到队列:通过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 + "'");
消费者消费消息:消费者消费消息也需要创建通道,声明指定交换机和队列,然后通过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 + "'");
}
}
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 args = new HashMap();
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);
}