RabbitMQ基础
为什么要使用消息中间件?
例流程图单线程:
这是一个简单的下单流程如果每次执行的时间都一样的话每个结点的时间都为10s那么此流程就需要40s
例流程图多线程:
这样多线程的话我也假设每个节点10s那么这个流程需要20s
使用消息队列就是在生成订单的时候将消息发送,然后由个个模块监听接收;这个速度远超线程的开启且占用资源小,当用户访问量过大时线程的速度就会变慢。而且消息队列可以当作定时功能例如延时队列,死信队列;想了解继续看。
RabbitMQ
简单介绍
2007年发布是基于AMQP(高级消息队列协议)的基础来开发的。优点:性能好,吞吐量达万级,易用,稳定,跨平台支持多语言;缺点:商业版收费
RabbitMQ入门准备
安装RabbitMQ环境:
虚拟机:Orcle VM virtualBox 内存:2048 处理器:2 显存:256 系统:centOS 7 系统内核:3.10以上
安装docker:**注意:我为了方便使用的是容器技术你也可以直接安装**
#查看系统内核
[root@localhost ~]# uname -r
[root@localhost ~]# yum install docker
#安装好启动docker
[root@localhost ~]# systemctl start docker
#之后就是从docker 中拉取镜像参考docker hub 官网 [docker hub 官网](https://hub.docker.com/)
[root@localhost ~]# docker pull rabbitmq:版本
#查看 docker 中有哪些
[root@localhost ~]# docker images
#启动rabbitmq --name 启动名字 -p暴露端口 5671映射5672 默认5672 一个服务端口,一个是客户端端口
[root@localhost ~]# docker run -d --name myrabbitmq -p 5671:5672 -p 15671:15672 docker.io/rabbitmq
到这里安装就是完成了还有其他的命令自己查一下这里主要是介绍怎么使用RabbitMQ;
刚启动完成的RabbitMQ客户端的访问地址http://ip:15671,端口根据自己的暴露映射端口来 客户端默认账户密码guest;
RabbitMQ的工作原理
producer:生产者,一个或者多个生产者发送消息
channel:信道每个生产者都一个对应的信道,通过信道发送给交换机
exchange:交换机通过不同的类型和路由(routingKey)发送给对应队列。交换机的类型,direct(直接类型),fanout(扇出),topic(标题),headers(头类型这个基本不用)
queue:队列(先进先出原则)通过binding绑定交换机。收到消息后通过信道交给消费之取出
consumer:消费者,消费队列消息
简单队列
Exchange type 为direct 类型实例
- 使用IDEA创建一个mvn工程导入坐标
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.3.10</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
交换机设置图
- 创建一个提供者
//消费者
public class ProducerDircet {
//定义一个交换机名
private static final String DIRCET_EXCHANGE_NAME="dircet_exchange";
//定义一个队列名
private static final String DIRCET_QUEUE_NAME="dircet_queue";
//定义一个路由
private static final String DIRCET_ROUTING_NAME="DRN";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel= RabbitMQUtil.getChannel();
/**
* 创建一个交换机
* 1.交换机名
* 2.交换机类型
* 3.durable:是否持久化
* 4.autoDelete是否自动删除
* 5.arguments:其他类型绑定
*/
channel.exchangeDeclare(DIRCET_EXCHANGE_NAME, BuiltinExchangeType.DIRECT,false,false,null);
/**
* 创建一个队列
* 1.队列名
* 2.durable:是否持久化
* 3.excusive:是否共享
* 4.autoDelete是否自动删除
* 5.arguments:其他类型绑定
*/
channel.queueDeclare(DIRCET_QUEUE_NAME, false, false, false, null);
/**
* 根据路由绑定
*
*/
channel.queueBind(DIRCET_QUEUE_NAME, DIRCET_EXCHANGE_NAME, DIRCET_ROUTING_NAME);
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
String message=scanner.next();
/**
* 发送消息到交换机根据路由到队列
* 1.交换机名
* 2.路由名
* 3.发消息的其他配置信息
* 4.发送的消息内容
*/
channel.basicPublish(DIRCET_EXCHANGE_NAME, DIRCET_ROUTING_NAME, null, message.getBytes());
System.out.println("发送消息:" + message);
}
}
}
- 创建一个消费者
//Dircet消费者
public class ConsumerDircet {
//定义一个队列名
private static final String DIRCET_QUEUE_NAME="dircet_queue";
public static void main(String[] args) throws Exception {
Channel channel= RabbitMQUtil.getChannel();
//autoAck 是否自动应答
System.out.println("启动获取队列");
channel.basicConsume(DIRCET_QUEUE_NAME, true, (consumerTag,message)->{
System.out.println("队列应答: " + new String(message.getBody(), "UTF-8"));
},consumerTag -> { } );
}
}
创建完交换机队列后查看rabbitmq客户端 由于我设置的都是false所以features没有值实例化设置true这里就显示D
Exchange type为fanout实例
订阅消息exchange 与队列绑定关系
- 订阅消息生产者
//消费者
public class ProducerDircet {
//定义一个交换机名
private static final String DIRCET_EXCHANGE_NAME="fanout_exchange";
//定义一个队列名
private static final String DIRCET_QUEUE_NAME1="fanout_queue1";
//定义一个队列名
private static final String DIRCET_QUEUE_NAME2="fanout_queue2";
private static final String DIRCET_QUEUE_NAME3="fanout_queue3";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel= RabbitMQUtil.getChannel();
/**
* 创建一个交换机
* 1.交换机名
* 2.交换机类型
* 3.durable:是否持久化
* 4.autoDelete是否自动删除
* 5.arguments:其他类型绑定
*/
channel.exchangeDeclare(DIRCET_EXCHANGE_NAME, BuiltinExchangeType.FANOUT,false,false,null);
/**
* 创建一个队列
* 1.队列名
* 2.durable:是否持久化
* 3.excusive:是否共享
* 4.autoDelete是否自动删除
* 5.arguments:其他类型绑定
*/
channel.queueDeclare(DIRCET_QUEUE_NAME1, false, false, false, null);
channel.queueDeclare(DIRCET_QUEUE_NAME2, false, false, false, null);
channel.queueDeclare(DIRCET_QUEUE_NAME3, false, false, false, null);
/**
* 订阅只要绑定就行不需要路由组件
* 应为订阅发布时他绑定的所有队列都会获取消息
*
*/
channel.queueBind(DIRCET_QUEUE_NAME1, DIRCET_EXCHANGE_NAME,"QF");
channel.queueBind(DIRCET_QUEUE_NAME2, DIRCET_EXCHANGE_NAME,"QF");
channel.queueBind(DIRCET_QUEUE_NAME3, DIRCET_EXCHANGE_NAME,"QF");
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
String message=scanner.next();
/**
* 发送消息到交换机根据路由到队列
* 1.交换机名
* 2.路由名
* 3.发消息的其他配置信息
* 4.发送的消息内容
*/
channel.basicPublish(DIRCET_EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("发送消息:" + message);
}
}
}
2. 订阅消费者
public class ConsumerDircet {
//定义一个队列名
//定义一个队列名
private static final String DIRCET_QUEUE_NAME1="fanout_queue1";
//定义一个队列名
private static final String DIRCET_QUEUE_NAME2="fanout_queue2";
private static final String DIRCET_QUEUE_NAME3="fanout_queue3";
public static void main(String[] args) throws Exception {
Channel channel= RabbitMQUtil.getChannel();
//autoAck 是否自动应答
System.out.println("启动获取队列");
channel.basicConsume(DIRCET_QUEUE_NAME1, true, (consumerTag,message)->{
System.out.println("队列应答1: " + new String(message.getBody(), "UTF-8"));
},consumerTag -> { } );
channel.basicConsume(DIRCET_QUEUE_NAME2, true, (consumerTag,message)->{
System.out.println("队列应答2: " + new String(message.getBody(), "UTF-8"));
},consumerTag -> { } );
channel.basicConsume(DIRCET_QUEUE_NAME3, true, (consumerTag,message)->{
System.out.println("队列应答3: " + new String(message.getBody(), "UTF-8"));
},consumerTag -> { } );
}
}
Exchange type为topic实例
路由中有.#或者#. 的意思就是在后面有多个或者0个单词例如:.# 相当于.de, .da.da;#. 相当于da. , da.da.
路由中*.或者.* 的意思就是在后面有1个单词例如:*. 相当于 da.
- topic的消息提供者
public class ProducerTopic {
//定义一个交换机名
private static final String DIRCET_EXCHANGE_NAME = "topic_exchange";
//定义一个队列名
private static final String DIRCET_QUEUE_NAME1 = "queue1";
//定义一个队列名
private static final String DIRCET_QUEUE_NAME2 = "queue2";
private static final String DIRCET_QUEUE_NAME3 = "queue3";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMQUtil.getChannel();
/**
* 创建一个交换机
* 1.交换机名
* 2.交换机类型
* 3.durable:是否持久化
* 4.autoDelete是否自动删除
* 5.arguments:其他类型绑定
*/
channel.exchangeDeclare(DIRCET_EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, false, null);
/**
* 创建一个队列
* 1.队列名
* 2.durable:是否持久化
* 3.excusive:是否共享
* 4.autoDelete是否自动删除
* 5.arguments:其他类型绑定
*/
channel.queueDeclare(DIRCET_QUEUE_NAME1, false, false, false, null);
channel.queueDeclare(DIRCET_QUEUE_NAME2, false, false, false, null);
channel.queueDeclare(DIRCET_QUEUE_NAME3, false, false, false, null);
channel.queueBind(DIRCET_QUEUE_NAME1, DIRCET_EXCHANGE_NAME, "QD.#");
channel.queueBind(DIRCET_QUEUE_NAME2, DIRCET_EXCHANGE_NAME, "#.QD.*");
channel.queueBind(DIRCET_QUEUE_NAME3, DIRCET_EXCHANGE_NAME, "*.QD.*");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
/**
* 发送消息到交换机根据路由到队列
* 1.交换机名
* 2.路由名
* 3.发消息的其他配置信息
* 4.发送的消息内容
* 第一个队列发三条
* 第二队列发三条
* 第三列发一条
*/
channel.basicPublish(DIRCET_EXCHANGE_NAME, "QD", null, message.getBytes());
channel.basicPublish(DIRCET_EXCHANGE_NAME, "QD.qd.dd", null, message.getBytes());
channel.basicPublish(DIRCET_EXCHANGE_NAME, "QD.qa", null, message.getBytes());
channel.basicPublish(DIRCET_EXCHANGE_NAME, "1.QD", null, message.getBytes());
channel.basicPublish(DIRCET_EXCHANGE_NAME, "1.1.QD.2", null, message.getBytes());
channel.basicPublish(DIRCET_EXCHANGE_NAME, "1.QD.2", null, message.getBytes());
System.out.println("发送消息:" + message);
}
}
}
消费者略;
手动应答队列
上面的案例中我是用的都是自动应答,通过设置消费者 basicConsume这个方法的ack为false 时需要手动应答,使用
//前面的值是当前消息的小标,当后multiple值为true是就是当前下表以前的全部应答,为false时就是只当前下标应答
channel.basicAck(message.getEnvelope().getDeliveryTag(), true);
队列的轮询
当同一个队列被两个消费者同时消费,会出现消费者1消费一次,消费这二消费一次,这样公平分发下去直到消费者,消费完队列里所有消息,这里我就不书写代码了可以使用上面的实例开启两个消费者来操作
不公平分发
设置预取值设置basicQos,通过预取值来设置当前的消费者的预取值。
例如:如果一个队列里有10个数据时,这时候有一个消费者1和消费者2。但是消费者2比消费者一慢30s,假设设置了两个消费者的预取值都是1,这个时候的意思就是当消费一个队列里的一个数据时,如果没有应答即不会给此消费者分配新数据;
其实预取值还有一个用法但是这个方法不够严谨;再在测试中发现当我设置预取值时当我设置了消费者1绝对慢于消费者2,在设置消费者2预取值为1,消费者1预取值为4。这时候你会发现消费者1消费了预设值4个消息。但是不绝对当消费者一的处理速度跟消费者二处理速度相差不多时;就会打破这个规则出现谁快谁消费的多,这就是不公平分发以可以说是流量的控制
确认队列
消息发送确认:在发送的时候确认发送成功给生产者一个应答这样也是为了防止消息丢失。当我们应答失败时我们可以设置机制比如从新入队发送,这样就不会出现消息在发送时丢失
这种确认方式有三种第一种和第二种方式都比较简单就是利用在
逐步确认
Channel channel= RabbitMQUtil.getChannel();
//开启发送确认
channel.confirmSelect();
channel.exchangeDeclare(CON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
channel.queueDeclare(CON_QUEUE_NAME, true, false, false, null);
channel.queueBind(CON_QUEUE_NAME, CON_EXCHANGE_NAME, CON_ROUTING_NAME);
//获取异步处理的集合
SortedSet set= Collections.synchronizedSortedSet(new TreeSet<>());
long start= System.currentTimeMillis();
for (int i=0;i<10000;i++){
String message="第"+i+"条消息";
//获取每次发送的编号
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(CON_EXCHANGE_NAME, CON_ROUTING_NAME,null,message.getBytes());
boolean b = channel.waitForConfirms();
if(b){
System.out.println("成功");
}
第二种跟第一种类似
异步操作
package com.rabbitmq.confrim;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.utils.RabbitMQUtil;
import java.io.IOException;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
public class ConfrimProduer {
static final String CON_EXCHANGE_NAME="con_exchange";
static final String CON_QUEUE_NAME="con_queue";
static final String CON_ROUTING_NAME="CRN";
public static void main(String[] args) throws IOException {
Channel channel= RabbitMQUtil.getChannel();
//开启发送确认
channel.confirmSelect();
channel.exchangeDeclare(CON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
channel.queueDeclare(CON_QUEUE_NAME, true, false, false, null);
channel.queueBind(CON_QUEUE_NAME, CON_EXCHANGE_NAME, CON_ROUTING_NAME);
//获取异步处理的map
ConcurrentSkipListMap<Object,Object> skipListMap=new ConcurrentSkipListMap<>();
long start= System.currentTimeMillis();
for (int i=0;i<300;i++){
String message="第"+i+"条消息";
//获取每次发送的编号
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(CON_EXCHANGE_NAME, CON_ROUTING_NAME,null,message.getBytes());
skipListMap.put(nextPublishSeqNo,message);
}
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println(deliveryTag+""+multiple);
if(multiple){
ConcurrentNavigableMap<Object, Object> map = skipListMap.headMap(deliveryTag);
map.clear();
}else{
skipListMap.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
String message= (String) skipListMap.get(deliveryTag);
//未成功的消息
System.out.println("message" + message);
}
});
long end= System.currentTimeMillis();
System.out.println("执行时间:" + (end - start) + "ms");
}
}
代码解析异步操作;当消息发送入队后写入磁盘由于是异步操作所以他啊返回的ack调用可能是多个当是多个是multiple=true 清空所有,但只有一个时multiple=false清空当前
当发生异常时就会请求nack这是会从新入队清空集合
TLL设置过期时间——死信
由于业务的需要当业务量太大时容易造成队列处理的慢这时候我们可以加入一个队列帮助处理同一批数据怎么处理呢?
这时候就用到了TTL设置过期其实时间放入死信队列中来处理这样怎加了一个消费者处理
案例
解释图
public class prodcuer {
private static final String EXCHANGE="exchange";
private static final String TTL_QUEUE="ttl_queue";
private static final String DTL_EXCHANGE="dtl_exchange";
private static final String DTL_QUEUE="dtl_queue";
public static void main(String []args)throws Exception{
Channel channel= RabbitMQUtil.getChannel();
//正常
channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT, false, false, null);
//死信交换机
channel.exchangeDeclare(DTL_EXCHANGE, BuiltinExchangeType.DIRECT, false, false, null);
channel.queueDeclare(DTL_QUEUE, false, false, false, null);
channel.queueBind(DTL_QUEUE, DTL_EXCHANGE, "qq");
HashMap<String,Object> map =new HashMap<>();
//绑定这里的key在mq的客户端可以看到
map.put("x-dead-letter-exchange", DTL_EXCHANGE);
map.put("x-dead-letter-routing-key","qq");
channel.queueDeclare(TTL_QUEUE, false, false, false, map);
channel.queueBind(TTL_QUEUE, EXCHANGE, "cc");
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
channel.basicPublish(EXCHANGE, "cc",properties, "死信队列".getBytes());
}
}
到这里rabbitmq的基础入门就结束了。