RabbitMQ的学习
MQ的概念
概念
Mq(message queue) 本质上是一个消息队列,遵循先入先出的原则,只不过队列中存放的是message,是一种跨进程之间的通信机制,用于上下游的通信,使用了MQ之后,消息发送上游只需要依赖MQ,不需要依赖其他的服务
用途
1.流量削峰
不直接访问订单系统,如果一下子来很多的请求,就可能会造成系统崩溃,所以加入MQ,,实现等待,进行削峰,不会宕机。
2.应用解耦
3.异步处理
有一些服务是异步的,如果A调用B的,B却需要花费很长时间执行,但是A需要知道B执行结束的时间,可以使用队列来完成,当A调用后,只需要监听B处理完成的消息,当B处理完之后,发送消息给MQ,MQ就会把结束的消息发送会给A,就可以及时的收到异步处理成功的消息。
MQ分类
1 ActiveMQ,单机的吞吐量万级,时效性ms级,可用性高,可靠性高
但是缺点是官方社区维护少,对于高吞吐量场景使用较少
2 Kafka
缺点:当队列多时,发送消息的相应时间就变长,消息失败之后不支持重试,支持消息顺序,有一台代理宕机之后,就会产生消息乱序
3 RocketMQ,出自阿里巴巴,用java语言实现优点是单继吞吐量十万级,可用性高,分布时架构可以做到0数据丢失,功能较为完善。源码是java写的
缺点是支持客户端语言不多
4 RabbitMQ
缺点是要钱,学习成本高
具体的选择
根据不同MQ的特点,可以在不同业务场景下使用不同的队列。
1kafka特点是高吞吐量,一开始的目的是用于日志的传输和收集,适合产生大量的数据的互联网服务的数据收集业务,一般是大型公司使用,如果有日志采集功能首选kafka
2RocketMQ天生为金融互联网服务,对于可靠性高的场景,尤其是电商里面的订单扣款以及业务削峰,稳定性高
3RabbitMQ,结合erlang语言本身的并发优势,性能好,社区活跃度高,如果数据没有这么大,中小型公司优先选择RabbitMQ
RabbitMQ
工作原理
四大核心概念
六大核心部分
启动和关闭rabbitmq服务
启动
[root@root sbin]# cd /opt/rabbitmq_server-3.7.16/sbin/
[root@root sbin]# ls
cuttlefish rabbitmqctl rabbitmq-defaults rabbitmq-diagnostics rabbitmq-env rabbitmq-plugins rabbitmq-server
[root@root sbin]# ./rabbitmq-server -detached
关闭
[root@root sbin]# ./rabbitmqctl stop
在浏览器上访问rabbitmq界面
http://192.168.154.128:15672
第一个程序
根据流程写代码
消费者
流程是按照原理图的流程进行生产消息放到队列中:
/**
* Creat with IntelliJ IDEA
*
* @Auther:倔强的加瓦
* @Date:2021/11/28/17:42
* @Description:
*/
public class Producer {
//所要连接的队列的名字
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//要先创建RabbitMq工厂对象
ConnectionFactory factory = new ConnectionFactory();
//指定工厂ip,连接存储数据的队列
factory.setHost("192.168.154.128");
//设置队列的密码和用户名
factory.setUsername("guest");
factory.setPassword("guest");
//创建连接
Connection connection = factory.newConnection();
//从原理图可以看出,真正传输消息的是信道,而不是连接,所以要获取连接里面的channel
Channel channel = connection.createChannel();
/**
* 给信道连接一个队列来存储消息。
* 第一个参数是连接队列的名称
* 第二个参数是队列里的消息是否持久化,默认情况下存储在磁盘上,
* 第三个是该队列是否只供一个消费者进行消费,其他消费者是否可以共享
* 第四个是是当最后一个消费者断开连接之后,队列是否自动删除
* 第五个是其他参数
*
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//你的消息
String message="Hello,RabbitMQ!你把我害的好惨啊";
/**
* 第一个参数表示交换机的名字(本次没指定)
* 第二个是目的队列名称
* 第三个是其他参数信息
* 第四个是消息的内容,但是要调用二进制
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息传送完毕");
}
}
RabbitMQ图示
可以看出,队列中有一个信息已经被生产了
消费者
/**
* Creat with IntelliJ IDEA
*
* @Auther:倔强的加瓦
* @Date:2021/11/28/17:57
* @Description:
*/
public class Consumer {
//声明目标队列的名字
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//获取rabbitMQ工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.154.128");
factory.setUsername("guest");
factory.setPassword("guest");
//创建连接,有链接创建信道
Connection connection = factory.newConnection();
//创建channel,目的是用来消息的消费
Channel channel = connection.createChannel();
//两个回调函数,来处理接收信息的不同情况
//声明接收消息
DeliverCallback deliverCallback=(consumerTag,message)->{
//只打印消息体
System.out.println(new String(message.getBody()));
};
CancelCallback cancelCallback=a->{
System.out.println("消息被中断时会被执行");
};
/**
* 方法具体的含义:
* 第一个:消费哪一个队列,要指明要消费队列的姓名
* 第二个:消费成功之后是否要自动应答,true代表自动
* 第三个:消费者没有成功消费的回调函数
* 第四个:消费者取消消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
RabbitMQ结果
WorkQueues工作队列
工作队列,又称为任务队列,主要思想是避免立即执行资源密集型任务而不得不等待他的完成,设多个线程来同时处理这些队列中的大量任务
消息应答机制:
消费者完成任务需要一定的时间,为了确保消息在发送过程中发送的数据不丢失,rabbitmq引入了消息应答机制,消息应答就是消费者在收到消息之后,并且处理完消息之后,就会告诉rabbitMq他已经处理完毕了,队列可以把消息删除了
分为自动应答和手动应答
自动应答:消息发送后,立即被认为已经传送成功了。适用于在消费者可以高效并且以某种速率能够处理这些消息的情况下使用。
手动应答:三种方法
少了一个批量应答的参数,为false时关闭批量应答,只应答当前的消息
当参数为true时,开启批量应答,此时在信道中5678的都确认接收应答。
消息自动重新入队
package com.njupt.reQueue;
import com.njupt.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* Creat with IntelliJ IDEA
*
* @Auther:倔强的加瓦
* @Date:2021/11/28/21:07
* @Description:
*/
public class ConsumerSlow {
public static final String NAME="queue1";
public static void main(String[] args) throws Exception {
//抽出来的方法,可以减少代码量
Channel channel = RabbitMQUtils.getChannel();
System.out.println("消费者1处理很快");
DeliverCallback deliverCallback=(a,m)->{
//模拟处理较慢的场景
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C1接收到的消息"+new String(m.getBody()));
//手动应答,false表示不批量应答,第一个参数是包中的tag标志位
channel.basicAck(m.getEnvelope().getDeliveryTag(),false);
System.out.println("消息被应答");
};
CancelCallback cancelCallback=c->{
System.out.println("取消应答");
};
//手动确认接收
channel.basicConsume(NAME,false,deliverCallback,cancelCallback);
}
}
消费快的可以开启不公平分发的功能谁处理的快,优先将队列中的消息发送给处理快的
public class ConsumerFast {
public static final String NAME="queue1";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
System.out.println("消费者1处理很快");
DeliverCallback deliverCallback=(a,m)->{
//模拟处理较慢的场景
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C1接收到的消息"+new String(m.getBody()));
//手动应答,false表示不批量应答
channel.basicAck(m.getEnvelope().getDeliveryTag(),false);
System.out.println("消息被应答");
};
CancelCallback cancelCallback=c->{
System.out.println("取消应答");
};
/ ///设为不公平分发
//channel.basicQos(1);
//预取值
//channel.basicQos(2);
//手动确认接收
channel.basicConsume(NAME,false,deliverCallback,cancelCallback);
}
}
上面采取的是轮询分发,不管消费者的处理速度如何,都是轮流的处理队列中的消息,可以设定为不公平分发
RabbitMQ的持久化
上面介绍了消费者处理任务时不丢失数据的情况,就是可以重新入队列的操作,但是,如何保证当RabbitMQ服务停掉之后消息生产者发送过来的消息不丢失,要确保不丢失需要做两件事要把队列和消息都标记为持久化。
- 队列实现持久化,只需要在新建信道连接队列时,设定队列的属性durable=true即可。
channel.queueDeclare(队列的名字,true,false,false,null);
如果希望将已经非持久化的队列变成持久化,需要先删除RabbitMQ中那个存在的,然后重新创建一个
2. 消息持久化,要想将消息持久化,需要在信道发布到队列时将属性加上,就可以保证消息持久化
channel.basicPublish("这里是交换机的名字",NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
不公平分发
上面在介绍过程中是采用的轮询分发(为0),也就是不管消费方处理的快不快,都是采用轮流着处理消息,因此为了解决这个问题需要根据速度来处理消费者消费消息的方式,改变原来的公平分发方式
因为是分发任务,所以需要在消费者一方更改代码。在收到消息之前改变一个标志位即可
//设为不公平分发
prefetch=1
channel.basicQos(prefetch);
//手动确认接收
channel.basicConsume(NAME,false,deliverCallback,cancelCallback);
预取值
可以将prefetch的值大于1时,所设置的值就是预取值,消费者可以根据所设定的预取值大小,从队列中取出指定数目的任务到对应的信道中,即使执行的任务比较慢,但是还是会“缓存这么多任务”进行等待执行
发布确认
发布确认原理
是用来确保消息不会丢失,前面介绍了队列持久化(信道连接队列时,将队列置为持久化)和消息持久化(信道发布到队列时,将队列置为持久化),并不能保证数据一定不会丢失,比如信息传送到MQ之后,还没有来得及进行磁盘上的持久化,RabbitMQ就宕机了,解决方案:此时需要引入发布确认,在MQ将消息持久化之后告诉生产者,已经进行了持久化,就可以进行信息的传输了,三步做完之后才可以稳稳的保证数据不会丢失。
因此确认就发生在生产者端,根据每次确认持久化消息的个数,有不同的情况
单个确认
public static void single() throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//单一发布确认
channel.confirmSelect();
//设置队列的属性
channel.queueDeclare(NAME, false, false, false, null);
//为单个发布计时
final String NAME = "queue2";
long l = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
///每发一次就持久化一次,等待确认
channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println(end - l);
}
优点是可以知道那个消息发生了错误,缺点是比较慢
批量确认
public static void publics() throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//发布确认
channel.confirmSelect();
//设置队列的属性
channel.queueDeclare(NAME, false, false, false, null);
//为单个发布计时
final String NAME = "queue3";
long l = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
if((i+1)%100==0){
//每一百条信息确认一次
channel.waitForConfirms();
}
}
long end = System.currentTimeMillis();
System.out.println("批量确认"+(end - l));
}
优点是很快,但是缺点是不知道这一批中哪一个消息有错误
异步确认
异步确认虽然逻辑上比较复杂,但是性价比最高,非常安全,将所有消息都记录到map中。当消息确认接收之后,进行删除map集合就可,如果没有接收成功,可以 从map中取出未成功的,重新发布。他利用回调函数来达到消息可靠性的传递的,这个中间件也是通过回调函数来保证投递是否成功。
public static void nSynPublic() throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//发布确认
final String NAME = "queue4";
//设置队列的属性
channel.queueDeclare(NAME, false, false, false, null);
//开启确认通知
channel.confirmSelect();
//创建一个线程安全的map,来记录下哪些消息发送成功,哪些消息发送不成功,如果成功删除即可,不成功需要进行记录
ConcurrentSkipListMap<Long, String> map = new ConcurrentSkipListMap<>();
//为监听器准备的两个接口。用来监听接收的消息是否成功和失败
//监听哪些消息成功
ConfirmCallback ackCallBack=(deliveryTag,multiple)->{
ConcurrentNavigableMap<Long, String> confirmed = map.headMap(deliveryTag);
if(multiple){
confirmed.clear();
}else {
confirmed.remove(deliveryTag);
}
System.out.println("确认收到的消息"+deliveryTag);
};
//记录下那些消息失败
ConfirmCallback nackCallBack=(deliveryTag,multiple)->{
String s = map.get(deliveryTag);//拿到未确认的消息
System.out.println("未确认的消息"+deliveryTag+":"+s);
};
//准备异步监听器
channel.addConfirmListener(ackCallBack,nackCallBack);
//为单个发布计时
long l = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
//将发布的消息记录到map中,来保存
map.put(channel.getNextPublishSeqNo(),message);
//记录下所有要发布的消息
//channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println("批量确认"+(end - l));
}
交换机
上一节中介绍了工作队列,假设的是每一个消息发给一个消费者,但是这一节要介绍一下将消息发送到多个消费者,这种模式称为发布订阅。
RabbitMQ队列的核心思想是生产者生产的消息从不会直接发送给队列,而是直接发送到交换机上,交换机一方面接收来自生产者的消息,另一方面将他们推入队列,当然交换机必须知道该如何处理接受到的消息如:放到特定的队列中或者放到很多的队列中,或者应该丢弃他们
交换机的类型:无名类型(空字符串),直接类型,主题类型,扇出类型
扇出类型交换机(fanout,即发布订阅)
创建生产者
public class Producer {
//指定交换机的名称
public static final String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取channel
Channel channel = RabbitMQUtils.getChannel();
//设定交换机的属性
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
//第一个参数是交换机名称,第二个是rootingKey,第三个是消息的持久化参数
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
System.out.println("消息发布"+message);
}
}
}
消费者
public class Consumer1 {
//指定交换机的名称
public static final String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//之前是连接队列,现在是加入了交换机,所以要先声明交换机的属性
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//随机声明一个临时的队列,当消费者断开之后,队列就自动删除
String queue = channel.queueDeclare().getQueue();
//将队列和交换机进行绑定,队列名、交换机名,rootingKey
channel.queueBind(queue,EXCHANGE_NAME,"");
System.out.println("队列1准备完毕,准备接收消息。。。。。。。。。。");
//接收回调和取消回调
DeliverCallback deliverCallback=(a,e)->{
System.out.println(new String(e.getBody(),"UTF-8"));
};
channel.basicConsume(queue,true,deliverCallback,c->{});
}
}
结果是当有多个消费者时就可以同样的接收相同的消息。
直接类型路由交换机(Direct Exchange)
上一节介绍了扇出类型的交换机,可以向接收者发布相同的消息,本节将向其中添加一些特别的功能,如只让某个订阅者接收消息,用在将严重错误的日志定向存放到日志文件中来节省空间,同时任然可以在控制台上打印所有的日志消息。
只需要设定不同的rooting Key就行
生产者
public class Prodcer {
//指定交换机的名称
public static final String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
//获取channel
Channel channel = RabbitMQUtils.getChannel();
//设定交换机的属性
//channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"error",null,message.getBytes("UTF-8"));
System.out.println("消息发布"+message);
}
}
}
消费者1
public class Consumer1 {
public static final String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//之前是连接队列,现在是加入了交换机,所以要先声明交换机的属性
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//随机声明一个临时的队列,当消费者断开之后,队列就自动删除
channel.queueDeclare("console",false,false,true,null);
//将队列和交换机进行多重绑定,队列名、交换机名,rootingKey
channel.queueBind("console",EXCHANGE_NAME,"info");
channel.queueBind("console",EXCHANGE_NAME,"waring");
System.out.println("队列1准备完毕,准备接收消息。。。。。。。。。。");
//接收回调和取消回调
DeliverCallback deliverCallback=(a, e)->{
System.out.println(new String(e.getBody(),"UTF-8"));
};
channel.basicConsume("console",true,deliverCallback,c->{});
}
}
//消费者2
public class Consumer2 {
public static final String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//之前是连接队列,现在是加入了交换机,所以要先声明交换机的属性
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//随机声明一个临时的队列,当消费者断开之后,队列就自动删除
channel.queueDeclare("disk",false,false,true,null);
//将队列和交换机进行多重绑定,队列名、交换机名,rootingKey
channel.queueBind("disk",EXCHANGE_NAME,"error");
System.out.println("队列2准备完毕,准备接收消息。。。。。。。。。。");
//接收回调和取消回调
DeliverCallback deliverCallback=(a, e)->{
System.out.println(new String(e.getBody(),"UTF-8"));
};
channel.basicConsume("disk",true,deliverCallback,c->{});
}
}
主题交换机(Topic)
上面两个介绍了扇出形式的交换机和订阅者模式的交换机,可以有选择性的接收日志信息,但是仍有局限性,我们希望一个消息的接收者可以接收多个队列中的任务时,没办法解决,因此引入主题交换机
如
因此主题交换机功能最强大
因为当一个队列中啊绑定的是#,就说明所有的队列都可接收消息,就是fanout扇出交换机,当没有#和*出现时,绑定的就是direct交换机
生产者
public class Producer {
public static final String EXCHANGE_NAME="topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//设定交换机的属性
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
String routingKey="lazy1.orange.erer";
String message="是谁在接收我!";
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));
}
}
消费者1
public class Consumer2 {
public static final String EXCHANGE_NAME="topic_logs";
public static void main(String[] args) throws Exception {
System.out.println("Q2消费者");
Channel channel = RabbitMQUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
channel.queueDeclare("Q2",false,false,false,null);
channel.queueBind("Q2",EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind("Q2",EXCHANGE_NAME,"lazy.#");
DeliverCallback deliverCallback=(a, b)->{
System.out.println(new String(b.getBody(),"UTF-8"));
System.out.println("队列名称Q2,绑定的建为"+b.getEnvelope().getRoutingKey());
};
channel.basicConsume("Q2",true,deliverCallback,c->{});
}
}
消费者2
public class Consumer {
public static final String EXCHANGE_NAME="topic_logs";
public static void main(String[] args) throws Exception {
System.out.println("Q1消费者");
Channel channel = RabbitMQUtils.getChannel();
//设定消费者的属性
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
channel.queueDeclare("Q1",false,false,false,null);
channel.queueBind("Q1",EXCHANGE_NAME,"*.orange.*");
DeliverCallback deliverCallback=(a,b)->{
System.out.println(new String(b.getBody(),"UTF-8"));
System.out.println("队列名称Q1,绑定的建为"+b.getEnvelope().getRoutingKey());
};
channel.basicConsume("Q1",true,deliverCallback,c->{});
}
}
可以根据绑定的不同,让消费者读取不同的队列。达到指定接收的目的。
死信队列
要想知道什么是死信队列,需要先知道什么是死信,故名思意就是无法被消费的信息,是指生产者将消息生产出来之后,会放到交换机中,消费者从队列中取出消息进行消费时,某些时候由于特定的原因导致队列中的消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,而存放死信的队列称为死信队列。
应用的场景为:用户虽然下单成功并且点击去支付之后在指定的时间未支付,就会自动生效,放入死信,防止信息丢失
死信的来源:
消息TTL过期
队列达到最大长度(队列满了,无法再添加数据到mq中)
消息被拒接
死信队列模拟场景
由消息TTL过期而加入死信队列中
生产者
public class Producer {
public static final String EXCHANGE_NAME="normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//设置死信消息,设置超时时间time to live
AMQP.BasicProperties build = new AMQP.BasicProperties()
.builder()
.expiration("10000")
.build();
for (int i = 0; i <10 ; i++) {
Thread.sleep(1000);
String message="我又要开始发送了"+i;
channel.basicPublish(EXCHANGE_NAME,"zhangsan",build,message.getBytes("UTF-8"));
}
}
}
消费者C1
public class Consumer1 {
//声明两个交换机
public static final String EXCHANGE_NAME="normal_exchange";
public static final String DEAD_EXCHANGE_NAME="dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//声明普通交换机和死信交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
channel.exchangeDeclare(DEAD_EXCHANGE_NAME,"direct");
//声明普通队列
Map<String,Object> arguments=new HashMap<>();
//设置正常队列的死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置死信交换机的routingkey
arguments.put("x-dead-letter-routing-key","lisi");
//设置超过最大的长度之后就要加入死信队列
//arguments.put("x-max-length",6);
channel.queueDeclare("normal_queue1",false,false,false,arguments);
// 和死信队列
channel.queueDeclare("dead_queue1",false,false,false,null);
//将队列和交换机绑定
channel.queueBind("normal_queue1",EXCHANGE_NAME,"zhangsan");
channel.queueBind("dead_queue1",DEAD_EXCHANGE_NAME,"lisi");
System.out.println("等待接收消息");
DeliverCallback deliverCallback=(a,b)->{
System.out.println(new String(b.getBody(),"UTF-8"));
};
CancelCallback cancelCallback=s->{
};
channel.basicConsume("normal_queue1",true,deliverCallback,cancelCallback);
}
}
消费者C2就是一个普通的消费者,消费死信队列中的数
public class Consumer2 {
public static final String DEAD_EXCHANGE_NAME="dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//之前是连接队列,现在是加入了交换机,所以要先声明交换机的属性
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//随机声明一个临时的队列,当消费者断开之后,队列就自动删除
channel.queueDeclare("dead_queue1",false,false,false,null);
//将队列和交换机进行多重绑定,队列名、交换机名,rootingKey
channel.queueBind("dead_queue1",DEAD_EXCHANGE_NAME,"lisi");
System.out.println("队列2准备完毕,准备接收消息。。。。。。。。。。");
//接收回调和取消回调
DeliverCallback deliverCallback=(a, e)->{
System.out.println(new String(e.getBody(),"UTF-8"));
};
channel.basicConsume("dead_queue1",true,deliverCallback,c->{});
}
}
由超出队列的最大长度而加入死信队列中
只需要在C1消费者代码中设定普通队列的最大长度即可(别忘了取消生产者的超时时间)
//声明普通队列
Map<String,Object> arguments=new HashMap<>();
//设置正常队列的死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置死信交换机的routingkey
arguments.put("x-dead-letter-routing-key","lisi");
设置超过最大的长度之后就要加入死信队列
arguments.put("x-max-length",6);
channel.queueDeclare("normal_queue1",false,false,false,arguments);
消息被拒绝接收而加入死信队列中
public class Consumer1 {
public static final String EXCHANGE_NAME="normal_exchange";
public static final String DEAD_EXCHANGE_NAME="dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//声明普通交换机和死信交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
channel.exchangeDeclare(DEAD_EXCHANGE_NAME,"direct");
//声明普通队列
Map<String,Object> arguments=new HashMap<>();
//设置正常队列的死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置死信交换机的routingkey
arguments.put("x-dead-letter-routing-key","lisi");
//设置超过最大的长度之后就要加入死信队列
//arguments.put("x-max-length",6);
channel.queueDeclare("normal_queue1",false,false,false,arguments);
// 和死信队列
channel.queueDeclare("dead_queue1",false,false,false,null);
//将队列和交换机绑定
channel.queueBind("normal_queue1",EXCHANGE_NAME,"zhangsan");
channel.queueBind("dead_queue1",DEAD_EXCHANGE_NAME,"lisi");
System.out.println("等待接收消息");
DeliverCallback deliverCallback=(a,b)->{
String me=new String(b.getBody(),"UTF-8");
//如果是指定消息就拒绝
if(me.equals("我又要开始发送了3")){
System.out.println(me+"被拒绝,需要被重新塞回队列中");
//拒绝接收
channel.basicReject(b.getEnvelope().getDeliveryTag(),false);
}
System.out.println(new String(b.getBody(),"UTF-8"));
};
CancelCallback cancelCallback=s->{
};
//开启手动应答
channel.basicConsume("normal_queue1",false,deliverCallback,cancelCallback);
}
}
延迟队列
队列的内部是有序的,最重要的特性就是体现在它的延时属性上,延时队列中的元素是希望在指定的时间到了以后或者之前进行取出和处理,简单来说延时队列就是用来存放在指定时间被处理的元素的队列,如果在上面死信队列中,没有C1消费者,只设定消费者就可以看到消息等待时间之后,再加入死信队列中供C2消费。
使用场景
- 订单在半小时之内未支付则自动取消
- 新创建的店铺,如果在十天之内没有上传商品,则自动发消息提醒
- 用户注册成功之后,如果三天之内没有登录,则需要进行短信提示
- 用户发起退款时,如果三天之内还没有被处理,则需要通知相关人员进行处理
- 预定会议时,提前10分钟通知各个与会人员参加会议
SpringBoot整合rabbitMq队列
Springboot实现按照如下图所示创建交换机和队列
1.首先创建环境加入依赖
<dependencies>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.开始创建属性文件
spring.rabbitmq.host=192.168.154.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
3.创建配置文件类,根据图创建出对应的交换机和队列,并且绑定指定相应的rootingkey.
@Configuration
public class TtlQueueConfig {
//定义交换机的名字和队列的名字
public static final String X_CHANGE="X";
public static final String Y_DEAD_CHANGE="Y";
public static final String QUEUENAMEA="QA";
public static final String QUEUENAMEB="QB";
//死信队列的名称
public static final String QUEUENAMEDEAD="QD";
//开始声明交换机
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_CHANGE);
}
//开始声明交换机
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_CHANGE);
}
//声明正常的队列
@Bean("queueA")
public Queue queueA(){
Map<String,Object> arguements=new HashMap<>();
//配置死信交换机
arguements.put("x-dead-letter-exchange",Y_DEAD_CHANGE);
//配置死信rootingkey
arguements.put("x-dead-letter-routing-key","YD");
//配置ttl
arguements.put("x-message-ttl",10000);
return QueueBuilder.durable(QUEUENAMEA).withArguments(arguements).build();
}
@Bean("queueB")
public Queue queueB(){
Map<String,Object> arguements=new HashMap<>();
//配置死信交换机
arguements.put("x-dead-letter-exchange",Y_DEAD_CHANGE);
//配置死信rootingkey
arguements.put("x-dead-letter-routing-key","YD");
//配置ttl
arguements.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUENAMEB).withArguments(arguements).build();
}
//配置死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(QUEUENAMEDEAD).build();
}
//开始绑定,x绑定QA队列,rootingkey为XA
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//开始绑定,x绑定QB队列,rootingkey为XB
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
//开始绑定,Y绑定QD队列,rootingkey为YD
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueD).to(xExchange).with("YD");
}
}
- 创建消费者和生产者进行模拟生产消费,通过网页的方式提供参数进行消费
生产者
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间{},发送一条消息给TTL队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
}
}
消费者
@Slf4j
@Component
public class MyConsumer {
@RabbitListener(queues = "QD")
public void receive(Message message, Channel channel) throws Exception{
String s = new String(message.getBody(),"UTF-8");
log.info("当前时间{},收到的队列消息{}",new Date().toString(),message);
}
}
- 启动springboot在浏览器端进行测试
http://localhost:8080/ttl/sendMsg/想要发送的消息
- 测试结果
对延时队列的优化
上面代码具有局限性,只能创建出固定时长的延时队列,如果还有其他的需求,比如要延迟一个小时,还需要重新的写,也就是,每新增一个需求就需要写一个队列,比较固定,因此可以对队列进行优化
创建一个队列QC不设置QC,其消息的ttl时间依靠生产者进行设定,传入什么值,就可以创建出指定时间的队列,就比较灵活
实现,首先在配置类中加入QC的信息,设置QC的死亡队列和rootingkey,但是不设置QC的ttl时间
@Bean("queueC")
public Queue queueC(){
Map<String,Object> arguements=new HashMap<>();
//配置死信交换机
arguements.put("x-dead-letter-exchange",Y_DEAD_CHANGE);
//配置死信rootingkey
arguements.put("x-dead-letter-routing-key","YD");
//C不设定时间
//arguements.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUENAMEC).withArguments(arguements).build();
}
将QC绑定X交换机
//开始绑定,x绑定QC队列,rootingkey为XC
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
最重要的一步是需要在前端生产者生产时设定队列C的ttl时间
//开始发消息,并且要发送过期时间
@GetMapping("/sendMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间{},发送一条{}消息给TTL队列:{}",new Date().toString(),ttlTime,message);
rabbitTemplate.convertAndSend("X","XC","消息来自ttl为40s的队列:"+message,msg-> {
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
在浏览器上输入
http://localhost:8080/ttl/sendMsg/你好1/20000
http://localhost:8080/ttl/sendMsg/你好2/2000
结果
可以看出虽然,的确是实现了可以自动调节ttl时间的功能,但是注意到虽然后发的消息只有2秒,但是由于前一个队列消息没有执行完任务,后面这个2秒的消息,就不会到死亡队列中去。因此也是一个弊端。
RabbitMq插件实现延迟队列
为了解决上面这个弊端,因此引入插件,来解决因为前一个消息ttl时间存活过长的问题。安装好插件之后,交换机的类型就多了一种
安装插件之后,在进行延迟发布消息时就会简化成如下情况
1.搭建如图场景的配置类信息
@Configuration
public class DelayQueueConfig {
public static final String DELAY_EXCHANGE_NAME="delay.exchange";
public static final String DELAY_QUEUE_NAME="delay.queue";
public static final String DELAY_ROOTINGKEY_NAME="delay.rootingkey";
//生明队列
@Bean
public org.springframework.amqp.core.Queue delayQueue(){
return new Queue(DELAY_QUEUE_NAME);
}
///自定义延迟交换机
@Bean
public CustomExchange delayExchange(){
Map<String,Object> arguments=new HashMap<>();
arguments.put("x-delayed-type","direct");
return new CustomExchange(DELAY_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
}
//将交换机和队列进行绑定
@Bean
public Binding delayQueueBindingDelayExchange(@Qualifier("delayQueue") Queue delayQueue, @Qualifier("delayExchange") CustomExchange delayExchange){
return BindingBuilder.bind(delayQueue).to(delayExchange).with(DELAY_ROOTINGKEY_NAME).noargs();
}
}
灵活设置延迟时间
//利用插件设置延迟发送消息
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendDelayMsg(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间{},发送一条{}消息给TTL队列:{}",new Date().toString(),delayTime,message);
rabbitTemplate.convertAndSend(DelayQueueConfig.DELAY_EXCHANGE_NAME,DelayQueueConfig.DELAY_ROOTINGKEY_NAME,"消息来自ttl为"+delayTime+"的队列:"+message, msg-> {
//设置延时时间
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
设置消费者
@Controller
@Slf4j
public class DelayConsumer {
@RabbitListener(queues = DelayQueueConfig.DELAY_QUEUE_NAME)
public void receiver(Message message){
String s = new String(message.getBody());
log.info("当前时间:{},收到延迟队列的消息:{}",new Date().toString(),s);
}
}
死信队列和延迟队列的对比总结
延迟队列在需要延时的处理场景下非常有用,使用RabbitMQ来实现延时队列可以很好的利用RabbitMQ的特点,比如消息可靠投递,消息可靠发送。
死信队列可以保障消息至少被消费一次,以及发生消息ttl、拒接接收、队列达到最大长度时,可以进行很好的处理,保证消息不会丢失。
另外通过RabbitMQ的集群特性,可以很好的解决单点故障的问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失,除此之外延时队列还其他的选择,入Java的DelayQueue,利用Redis的Zset,或者kafka的时间轮等
发布确认
前面所介绍的内容全部都是在考虑交换机和队列正常工作的情况,但是如果在工作的过程中,交换机或者队列工作异常了,必定会导致生产者方产生的数据丢失,因为生产者只管发送,并没有得到发送之后的结果,因此需要指定发送过程中的交换机和队列的情况,所以下面介绍交换机和队列发生异常的处理情况
交换机发生异常的解决办法
让发送者实现rabbitMQ的回调接口RabbitTemplate.ConfirmCallback,返回交换机在接收之后的反馈,从而知道交换机的情况.
首先在配置文件中开启回调的接口
#开启发布确认回调的功能
spring.rabbitmq.publisher-confirm-type=correlated
然后编写回调函数的实现类,来处理由于交换机发生故障而不能接收消息的情况
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
//由于ConfirmCallback是一个内部接口,当你写一个实现类实现一个内部接口时,是不在RabbitTemplate的对象里面
// 因此需要使构造方法将实现类注入到接口里
@Autowired
RabbitTemplate rabbitTemplate;
@PostConstruct
void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
*
* @param correlationData 里面会保存着回调之后的消息
* @param b 为true则表示交换机接收成功
* @param s 成功为null,失败为造成失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b){
log.info("消息接收成功,id为{}",correlationData.getId());
}else {
log.info("交换机还没有收到消息,由于{}",s);
}
}
}
实现之后,可以在发送端模拟路由器故障的场景,只需要在生产端把路由器的名字修改错误,就可以实现模拟功能,测试结果:
在发送端调用不同的构造方法,添加消息的信息,如id
//测试交换机出故障的生产者
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message){
CorrelationData data = new CorrelationData("1");
//通过这个构造方法将相关数据传送到交换机,进行验证,需要交换机来反馈这些消息
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROOTINGKEY,message,data);
log.info("发送消息:{}",message);
}
解决因为队列发生故障的问题
在仅开启了生产者确认机制的情况下,交换机接收到消息之后,会直接给消息生产者发送确认消息,但是如果发现交换机到队列过程中发生了故障,消息是会直接被丢弃的,但是生产者并不知道这个事件,解决办法是通过设定mandatory参数,此参数可以在当消息传递过程中不可达目的时将消息返回给生产者
首先在配置文件中开启回退功能
#开启发布退回机制
spring.rabbitmq.publisher-returns=true
和确认接收消息类似,实现回退的接口
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnsCallback{
//由于ConfirmCallback是一个内部接口,当你写一个实现类实现一个内部接口时,是不在RabbitTemplate的对象里面
// 因此需要使构造方法将实现类注入到接口里
@Autowired
RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
//注入回退接口
rabbitTemplate.setReturnsCallback(this);
}
/**
*
* @param correlationData 里面会保存着回调之后的消息
* @param b 为true则表示交换机接收成功
* @param s 成功为null,失败为造成失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b){
log.info("消息接收成功,id为{}",correlationData.getId());
}else {
log.info("交换机还没有收到消息,由于{}",s);
}
}
//重写回退的方法
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("消息{},被交换机{}退回,退回的原因{},路由Key为{}",new String(returnedMessage.getMessage().getBody()),returnedMessage.getExchange(),returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
}
}
在发送消息时,可以在发送端模拟队列发生了故障,只需要把rootingkey设置错误,就可以模拟此故障,然后发送者就可以看到故障消息
//测试交换机和队列出故障的生产者
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message){
CorrelationData data = new CorrelationData("1");
//通过这个构造方法将相关数据传送到交换机,进行验证,需要交换机来反馈这些消息
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROOTINGKEY,message,data);
log.info("发送消息:{}",message);
CorrelationData data2 = new CorrelationData("2");
//通过这个构造方法将相关数据传送到交换机,进行验证,需要交换机来反馈这些消息,设置错误的rootingkey
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROOTINGKEY+"12",message,data2);
log.info("发送消息:{}",message);
}
测试结果
交换机发生故障的解决办法–加入备份交换机和备份的队列
需要在上面的基础之上再加入一个备份的交换机和两个队列,因此需要在配置类信息中声明,
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
public static final String CONFIRM_QUEUE_NAME="confirm.queue";
public static final String CONFIRM_ROOTINGKEY="key1";
public static final String BACKUP_EXCHANGE_NAME="backup.exchange";
public static final String BACKUP_QUEUE_NAME="backup.queue";
public static final String WARNING_QUEUE_NAME="warning.queue";
//声明交换机如果发生了故障就去到备份交换机上
/*@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}*/
@Bean("confirmExchange")//声明构建交换机
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME)
.build();
}
//声明备份交换机
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//声明队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@Bean("backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
//绑定备份交换机和备份队列
@Bean
public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,@Qualifier("backupExchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(backupQueue).to(fanoutExchange);
}
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("warningQueue") Queue warningQueue,@Qualifier("backupExchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(warningQueue).to(fanoutExchange);
}
//绑定
@Bean
public Binding queueBindingExchange(@Qualifier("confirmQueue") Queue backupQueue,@Qualifier("confirmExchange") DirectExchange directExchange){
return BindingBuilder.bind(backupQueue).to(directExchange).with(CONFIRM_ROOTINGKEY);
}
}
然后创建消费者,由图创建之后,发现只是创建警告队列的消费者,创建即可
@Component
@Slf4j
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningConsumer(Message message){
log.error("报警发现不可路由的信息{}",new String(message.getBody()));
}
}
结果发现,的确可以把不可路由的消息进行处理,但是注意并没有把结果回退到生产者,这就说明当存在备份交换机时,备份交换机会优先级更高,处理接收的信息
RabbitMQ的其他知识点
1幂等性问题
消息重复的消费
解决办法
建议是利用redis的原子性,利用redis执行setnx命令,天然的具有幂等性,从而实现不重复消费
优先级队列
在设置消息的优先级时,前提要保证当发布之后,到队列中时不能被消费,要全部都先存放到队列中,在队列中会根据消息的优先级来重新排列顺序。
public class Producer {
//所要连接的队列的名字
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//要先创建RabbitMq工厂对象
ConnectionFactory factory = new ConnectionFactory();
//指定工厂ip,连接存储数据的队列
factory.setHost("192.168.154.128");
//设置队列的密码和用户名
factory.setUsername("guest");
factory.setPassword("guest");
//创建连接
Connection connection = factory.newConnection();
//从原理图可以看出,真正传输消息的是信道,而不是连接,所以要获取连接里面的channel
Channel channel = connection.createChannel();
/**
* 给信道连接一个队列来存储消息。
* 第一个参数是连接队列的名称
* 第二个参数是队列里的消息是否持久化,默认情况下存储在磁盘上,
* 第三个是该队列是否只供一个消费者进行消费,其他消费者是否可以共享
* 第四个是是当最后一个消费者断开连接之后,队列是否自动删除
* 第五个是其他参数
*
*/
Map<String,Object> arguments=new HashMap<>();
//设置最大的优先级
arguments.put("x-max-priority",10);
channel.queueDeclare(QUEUE_NAME,false,false,false,arguments);
//你的消息
for(int i=0;i<10;i++){
String message="info"+i;
if(i==5){
//当发送为5号消息时,把优先级设置最高
AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
}else {
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
System.out.println("消息传送完毕");
}
}
最后读取的过程中,的确是5号消息率先被读取。
惰性队列模式
可以将队列存到磁盘中,就是惰性队列,可以设置队列属性参数参数
其他知识如集群,镜像和远程访问联邦功能,暂时用不上到,待续。。。。。。