RabbitMQ
应用场景以及简单的原理
异步处理
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种1.串行的方式;2.并行的方式
(1)串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.
(2)并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
(3)消息队列
引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
应用解耦
将各种业务系统拆开 通过队列来交互
流量削峰
用于秒杀活动之类 控制活动人数 当活动请求到达一定阈值直接丢弃
可以缓解短时间的高流量压垮应用
下面是rabbitmq的一些基本操作
安装 erlang 和 mq
通过命令 net start RabbitMQ启动
在http://127.0.0.1:15672/ 访问 guset guset
添加角色 admin角色
共有5种角色
1、超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
2、监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
3、策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
4、普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
5、其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
通过设定消费数量来限制消费者人数 channel.basicQos(1);
工作模式 轮询分发
消费者1和消费者2获取到的消息内容是不同的,同一个消息只能被一个消费者获取。
消费者1和消费者2获取到的消息的数量是相同的,一个是消费奇数号消息,一个是偶数。
RabbitMQ 默认将消息顺序发送给下一个消费者,这样,每个消费者会得到相同数量的消息。即轮询(round-robin)分发消息。
怎样才能做到按照每个消费者的能力分配消息呢?联合使用 Qos 和 Acknowledge 就可以做到。
basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。
2个概念
轮询分发 :使用任务队列的优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。这种方式分发消息机制称为Round-Robin(轮询)。
公平分发 :虽然上面的分配法方式也还行,但是有个问题就是:比如:现在有2个消费者,所有的奇数的消息都是繁忙的,而偶数则是轻松的。按照轮询的方式,奇数的任务交给了第一个消费者,所以一直在忙个不停。偶数的任务交给另一个消费者,则立即完成任务,然后闲得不行。而RabbitMQ则是不了解这些的。这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将消息发给轮询指定的消费者。
为了解决这个问题,我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送。
还有一点需要注意,使用公平分发,必须关闭自动应答,改为手动应答。
Work模式的“能者多劳”
Connection connection = ConnectionMq.getConnection(); // 获取连接
//连接创建通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(1);//只接受一条消息
//定义队列的消费者 并将通道赋值给消费者
QueueingConsumer consumer= new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer); // 监听队列 有消息就消费 开启手动监听
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody()); // 获取队列消息
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(1000);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // 开启手动模式
}
消息的确认模式
模式1 自动确认 只要消息从队列中获取了 那么无论消费者是否成功获取消息都认为消息已经成功发送并消费
模式2 手动确认 消费者从队列中获取了消息,服务器会将该消息标记为不可用状态,等待消费者的反馈。如果消费者没有反馈该消费会一直处于不可用状态
channel.basicConsume("queueName",false,consumer);
订阅模式 即交换机状态
大致是
- 1个生产者,多个消费者
- 每一个消费者都有自己的一个队列
- 生产者没有将消息直接发送到队列,而是发送到了交换机
- 每个队列都要绑定到交换机
- 生产者发送的消息,经过交换机,到达队列,实现,一个消息被多个消费者获取的目的
注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
创建生产者
//获取连接以及通道
Connection connection = ConnectionMq.getConnection();
//获取 连接并创建通道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Exchange_NAME,"fanout");
String message = "this is change message";
channel.basicPublish(Exchange_NAME,"",null,message.getBytes());
//创建消息内容
//关闭通道和连接
channel.close();
connection.close();
创建消费者
private final static String QUEUE_NAME = "test_queue_work";
private final static String Change_NAME = "test_queue_exchange";
public static void main(String[] args) throws IOException, InterruptedException {
// String message ="这是第"+i+"this is new massage";
Connection connection = ConnectionMq.getConnection(); // 获取连接
//连接创建通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,Change_NAME,"");
channel.basicQos(1);
//定义队列的消费者 并将通道赋值给消费者
QueueingConsumer consumer= new QueueingConsumer(channel);
//监听队列 true 表示自动返回队列
channel.basicConsume(QUEUE_NAME,false,consumer); // 监听队列 有消息就消费
//获取信息
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody()); // 获取队列消息
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);//开启手动模式
}
}
创建消费者2
private final static String QUEUE_NAME = "test_queue_work";
private final static String Change_NAME = "test_queue_exchange";
public static void main(String[] args) throws IOException, InterruptedException {
Connection connection = ConnectionMq.getConnection(); // 获取连接
//连接创建通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,Change_NAME,"");
channel.basicQos(1);//只接受一条消息
//定义队列的消费者 并将通道赋值给消费者
QueueingConsumer consumer= new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer); // 监听队列 有消息就消费
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody()); // 获取队列消息
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(1000);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // 开启手动模式
}
}
注意的是 消息发送到没有队列绑定的交换机时,消息将会丢失,因为,交换机并没有储存消息的能力消息只能存在队列中
结论是 交换机的消息 被多个消费者消费 一个队列可以有多个消费者 但是只有一个消费者实例才能消费到
路由模式
例子如下
主题模式(通配符模式)
生产者
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "Hello World!!";
channel.basicPublish(EXCHANGE_NAME, "routekey.1", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
消费者1
private final static String EXCHANGE_NAME = "test_exchange_topic";
private final static String QUEUE_NAME = "test_queue_topic_work_1";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "routekey.*");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv_x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
消费者2
private final static String EXCHANGE_NAME = "test_exchange_topic";
private final static String QUEUE_NAME = "test_queue_topic_work_2";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2_x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}