Rabbitmq消息队列
文章介绍
-
RabbitMQ认识
-
RabbitMQ安装
-
RabbitMQ常用模型(重点)-原生代码
-
SpringBoot集成RabbitMQ(重点)
一. RabbitMQ认识
1. RabbitMQ介绍
1.1. 是什么
MQ全称为Message Queue,即消息队列. 它也是一个队列,遵循FIFO原则 。RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue Protocol高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
官方地址:http://www.rabbitmq.com/
1.2. 特点
开发中消息队列通常有如下应用场景:
1.2.1. 提高系统响应速度
任务异步处理。 将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。 提高了应用程序的响应时间。
1.2.2. 提高系统稳定性
系统挂了没关系,操作内容放到消息队列.
1.2.3. 服务调用异步化
服务没有直接的调用关系,而是通过队列进行服务通信
1.2.4. 服务解耦
应用程序解耦合 MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
1.2.5. 排序保证 FIFO
遵循队列先进先出的特点,秒杀
1.2.6. 消除峰值
异步化提速(发消息),提高系统稳定性(多系统调用),服务解耦(5-10个服务),排序保证,消除峰值
消息队列的特点:削峰,异步,解耦
1.3. 市场上还有哪些消息队列
ActiveMQ,RabbitMQ(轻量级,简易),ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。
1.4. 为什么使用RabbitMQ呢?
1、使用简单,功能强大。
2、基于AMQP协议。-跨语言
3、社区活跃,文档完善。
4、高并发性能好,这主要得益于Erlang语言。
5、Spring Boot默认已集成RabbitMQ
1.5. AMQP协议
1.5.1. AMQP是什么?
AMQP是一套公开的消息队列协议,最早在2003年被提出,它旨在从协议层定义消息通信数据的标准格式, 为的就是解决MQ市场上协议不统一的问题。RabbitMQ就是遵循AMQP标准协议开发的MQ服务。
官方:http://www.amqp.org
1.5.2. JMS是什么 ?
JMS是Java消息服务,是java提供的一套消息服务API标准,其目的是为所有的java应用程序提供统一的消息通信的标准,类似java的 jdbc,只要遵循jms标准的应用程序之间都可以进行消息通信。
它和AMQP有什么 不同,jms是java语言专属的消息服务标准,它是在api层定义标准,并且只能用于java应用;而AMQP是在协议层定义的标准,是跨语言的 。
二. RabbitMQ安装
1. 下载安装
RabbitMQ由Erlang语言开发,Erlang语言用于并发及分布式系统的开发,在电信领域应用广泛,OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件及工具库,安装RabbitMQ需要安装Erlang/OTP,并保持版本匹配,如下图:
RabbitMQ的下载地址:http://www.rabbitmq.com/download.html
本项目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本。
1)下载erlang
地址:http://erlang.org/download/otp_win64_20.3.exe 或去老师提供的软件包中找到 otp_win64_20.3.exe,以管理员方式运行此文件,安装。
erlang安装完成需要配置erlang环境变量:
ERLANG_HOME=D:\Program Files\erl9.3
在path中添加:%ERLANG_HOME%\bin
测试是否安装成功:erl -v
2)安装RabbitMQ
地址:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.3 或去老师提供的软件包中找到 rabbitmq-server-3.7.3.exe,以管理员方式运行此文件,安装。
3)启动RabbitMQ
安装成功会自动创建RabbitMQ服务并且启动
从开始菜单启动RabbitMQ 完成在开始菜单找到RabbitMQ的菜单:
服务启动或停止就ok了
RabbitMQ Service-install :安装服务 RabbitMQ Service-remove 删除服务 RabbitMQ Service-start 启动 RabbitMQ Service-stop 停止
如果没有开始菜单则进入安装目录下sbin目录手动启动:
安装并运行服务
rabbitmq-service.bat install 安装服务
rabbitmq-service.bat stop 停止服务
rabbitmq-service.bat start 启动服务
4)安装管理插件
安装rabbitMQ的管理插件,方便在浏览器端管理RabbitMQ ,进入到Rabbit的sbin目录,使用cmd执行命令: rabbitmq-plugins.bat enable rabbitmq_management , 安装成功后重新RabbitMQ
5)登录RabbitMQ
进入浏览器,输入:http://localhost:15672 ,初始账号和密码:guest/guest
rabbitmq的端口号:
15672 浏览器访问的端口
5672 代码连接的端口
注意:
- 如果安装路径有中文,或者系统登录用户是中文账户,启动RabbitMQ会闪退,需要修改MQ的配置
解决方案:https://blog.csdn.net/leoma2012/article/details/97636859
-
安装erlang和rabbitMQ以管理员身份运行。
-
当卸载重新安装时会出现RabbitMQ服务注册失败,此时需要进入注册表清理erlang 搜索RabbitMQ、ErlSrv,将对应的项全部删除。
三. RabbitMQ的工作原理
1. 组成部分说明如下:
Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue。
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列和某些对了,对消息进行过虑。
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。
2. 消息发布接收流程:
1.发送消息
-
生产者和Broker建立TCP连接。
-
生产者和Broker建立通道。
-
生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
-
Exchange将消息转发到指定的Queue(队列)
2.接收消息
-
消费者和Broker建立TCP连接
-
消费者和Broker建立通道
-
消费者监听指定的Queue(队列)
-
当有消息到达Queue时Broker默认将消息推送给消费者。
-
消费者接收到消息处理自己的业务逻辑。
四. RabbitMQ 常见模型
rabbitMQ提供了6种消息模型,但是第6种其实是RPC(远程调用),并不是MQ,因此不予学习。那么也就剩下5种。但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
1. Helloworld-基本消息模型
一对一
1.1. 搭建环境
创建maven工程 test-rabbitmq,加入RabbitMQ java client的依赖。
<dependencies>
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<!--和springboot2.2.5对应-->
<version>5.7.3</version>
</dependency>
</dependencies>
1.2. 连接工具
package cn.ronghuanet.util;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("127.0.0.1");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
管理端的端口是15672,但是我们代码端口是5672
1.3. 生产者
package cn.ronghuanet._01_base;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer01 {
//队列名称
private static final String QUEUE = "helloworld";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = null;
Channel channel = null;
try {
//获取连接
connection = ConnectionUtil.getConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
*/
channel.queueDeclare(QUEUE, true, false, false, null);
String message = "helloworld小明" + System.currentTimeMillis();
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
/**
* 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显
示绑定或解除绑定
* 默认的交换机,routingKey等于队列名称
*/
channel.basicPublish("", QUEUE, null, message.getBytes());
System.out.println("Send Message is:'" + message + "'");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
}
}
}
启动后管理工具查看:
点击队列名称,进入详情页,可以查看消息:
1.4. 消费者
package cn.ronghuanet._01_base;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer01 {
private static final String QUEUE = "helloworld";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//定义消费方法
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 消费者接收消息调用此方法
* @param consumerTag 消费者的标签,在channel.basicConsume()去指定
* @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
(收到消息失败后是否需要重新发送)
* @param properties
* @param body
*/
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
//交换机
String exchange = envelope.getExchange();
//路由key
String routingKey = envelope.getRoutingKey();
//消息id
long deliveryTag = envelope.getDeliveryTag();
//消息内容
String msg = new String(body, "utf8");
System.out.println("receive message.." + msg);
}
};
/**
* 监听队列String queue, boolean autoAck,Consumer callback
* 参数明细
* 1、队列名称
* 2、是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
为false则需要手动回复
* 3、消费消息的方法,消费者接收到消息后调用此方法
*/
channel.basicConsume(QUEUE, true, consumer);
}
}
我们发现,消费者已经获取了消息,但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印.
1.5. 消息确认机制(ACK)超级重要
通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。那么问题来了:RabbitMQ怎么知道消息被接收了呢?
如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ**无从得知,这样消息就丢失了!*
因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:
-
自动ACK:消息一旦被接收,消费者自动发送ACK
-
手动ACK:消息接收后,不会发送ACK,需要手动调用
大家觉得哪种更好呢?这需要看消息的重要性:
如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码:
1.5.1. 自动确认存在问题
修改消费者,添加异常,如下:
生产者不做任何修改,直接运行,消息发送成功:
运行消费者,程序抛出异常。但是消息依然被消费
1.5.2. 手动确认实现
修改消费者,把自动改成手动(去掉之前制造的异常)
生产者不变,再次运行:
这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。
当我们关掉这个消费者,消息的状态再次称为Ready
1.6. 小结
发送端操作流程
1)创建连接
2)创建通道
3)声明队列
4)发送消息
接收端
1)创建连接
2)创建通道
3)声明队列
4)监听队列
5)接收消息
6)ack回复
2. Work queues
work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
测试:
-
使用入门程序,启动多个消费者。
-
生产者发送多个消息。
结果:
-
一条消息只会被一个消费者接收;
-
rabbit采用轮询的方式将消息是平均发送给消费者的;
-
消费者在处理完某条消息后,才会收到下一条消息。
工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多工人时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。
这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。
接下来我们来模拟这个流程:
P:生产者:任务的发布者
C1:消费者,领取任务并且完成任务,假设完成速度较快
C2:消费者:领取任务并完成任务,假设完成速度慢
2.1. 生产者
省略…
2.2. 消费者1
省略
2.3. 消费者2
消费者 C1 在消费逻辑中睡眠一会儿,代表工作能力慢
2.4. 测试
发送50条消息,2个消费者各自收到25条消息,实现了任务的分发,但是消费者C2处理任务的速度会慢很多,最终2个消费者总共处理完所有任务的速度就更慢了,这个是因为C2有睡眠操作,而这种work-queue模型是吧任务平均分配给所有的消费者,就会到账C2那边堆积了大量的消息,从而整体处理速度比较慢。
2.5. 能者多劳
刚才的实现有问题吗?显然不合理
- 消费者1比消费者2的效率要高,一次任务的耗时较短
- 然而两人最终消费的消息数量是一样的
- 消费者1大量时间处于空闲状态,消费者2一直忙碌
现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。怎么实现呢?
我们可以使用basicQos方法和prefetchCount = 1设置,简单理解就是一个消费者同时只能处理一个消息。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是仍然忙碌的下一个工作人员。
要将应答模式改为手动应答
测试结果
这里可以看到,工作速度快的消费者处理了更多的消息
3. 交换机分类
3.1. 概述订阅模型
在之前的模式中,我们创建了一个工作队列。 工作队列背后的假设是:每个任务只被传递给一个工作人员。 在这一部分,我们将做一些完全不同的事情 - 我们将会传递一个信息给多个消费者。 这种模式被称为“发布/订阅”。
订阅模型示意图:
解读:
1、1个生产者,多个消费者
2、每一个消费者都有自己的一个队列
3、生产者没有将消息直接发送到队列,而是发送到了交换机
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
X*(Exchanges**):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange**的类型。*
3.2. 分类
Exchange类型有以下几种:
-
Fanout:广播,将消息交给所有绑定到交换机的队列 all
-
Direct:定向,把消息交给符合指定routing key的队列一堆或一个
Emploee_add
Employee_del
Employee_xxx
…
100
-
Topic:通配符,把消息交给符合routing pattern(路由模式)的队列* 一堆或者一个
Employee_*
我们这里先学习 Fanout:即广播模式
Exchange*(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange**绑定,或者没有符合路由规则的队列,那么消息会丢失!*
3.3. 订阅模型-FANOUT(广播模式)
3.3.1. 工作模式
发布订阅模式:
在广播模式下,消息发送流程是这样的:
1) 可以有多个消费者
2) 每个消费者有自己的queue(队列)
3) 每个队列都要绑定到Exchange(交换机)
4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
5) 交换机把消息发送给绑定过的所有队列
6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
3.3.2. 生产者
两个变化:
1)声明Exchange,不再声明Queue
2)发送消息到Exchange,不再发送到Queue
package cn.ronghuanet._02_fanout;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class Send {
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "Hello everyone";
// 发布消息到Exchange
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
3.3.3. 消费者1
package cn.ronghuanet._02_fanout;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
private final static String QUEUE_NAME = "fanout_exchange_queue_1";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.3.4. 消费者2
package cn.ronghuanet._02_fanout;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv2 {
private final static String QUEUE_NAME = "fanout_exchange_queue_2";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.3.5. 测试
注意:先启动一下生产者,来创建交换机
在哪儿用?
-
注册成功发送邮件和短信
-
消息推送
3.3. 订阅模型-Direct
3.3.1. 工作模式
这种模式是有选择性的接收消息,在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
² P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
² X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
² C1:消费者,其所在队列指定了需要routing key 为 error 的消息
² C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
3.3.2. 生产者
此处我们模拟商品的增删改,发送消息的RoutingKey分别是:insert、update、delete
package cn.ronghuanet._03_direct;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class Send {
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息内容
String message = "商品新增了, id = 1001";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
3.3.3. 消费者1
我们此处假设消费者1只接收两种类型的消息:更新商品和删除商品。
package cn.ronghuanet._03_direct;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
private final static String QUEUE_NAME = "direct_exchange_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.3.4. 消费者2
我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。
package cn.ronghuanet._03_direct;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv2 {
private final static String QUEUE_NAME = "direct_exchange_queue_2";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.3.5. 测试
我们分别发送增、删、改的RoutingKey,发现结果:
我们项目中就要用,特定站点静态页面,只能发到特定站点服务器
sendMsg routingKey:sitesn rev:绑定到交换机要以sitesn来作为routingkey
3.4. 订阅模型-Topic
3.4.1. 工作模式
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以*”.”*分割,例如: goods.insert pet.insert *.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs
路由模式:
1、每个消费者监听自己的队列,并且设置带统配符的routingkey。
2、生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。
3.4.2. 生产者
package cn.ronghuanet._04_topic;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "新增商品 : id = 1001";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
3.4.3. 消费者1
消费者1:我们此处假设消费者1只接收两种类型的消息:更新商品和删除商品
package cn.ronghuanet._04_topic;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.4.4. 消费者2
消费者2:我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。
package cn.ronghuanet._04_topic;
import cn.ronghuanet.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* 消费者2
*/
public class Recv2 {
private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.4.5. 测试
3.5. Header模式
header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配 队列。
案例: 根据用户的通知设置去通知用户,设置接收Email的用户只接收Email,设置接收sms的用户只接收sms,设置两种 通知类型都接收的则两种通知都有效。
3.5.1. 生产者
队列与交换机绑定的代码与之前不同,如下:
String message = "email inform to user"+i;
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type", "email");//匹配email通知消费者绑定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
//Email通知
channel.basicPublish(EXCHANGE_HEADERS_INFORM, "", properties.build(), message.getBytes());
3.5.2. 消费者
channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS);
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_type", "email");
//交换机和队列绑定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
//指定消费队列
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer);
3.5.3. 测试
3.6. RPC
RPC即客户端远程调用服务端的方法 ,使用MQ可以实现RPC的异步调用,基于Direct交换机实现,流程如下:
1、客户端即是生产者就是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列。
2、服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果
3、服务端将RPC方法 的结果发送到RPC响应队列
4、客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。
3.7. 持久化-解决数据安全
如何避免消息丢失?
1) 消费者的ACK机制。可以防止消费者丢失消息。-手动ack
2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。-持久化
3) 因为网络问题,消息发送失败 -回调
4) 代码BUG,交换机或队列的routingkey找不到
是可以将消息进行持久化呢?要将消息持久化,前提是:队列、Exchange都持久化
3.7.1. 交换机持久化
3.7.2. 队列持久化
3.7.3. 消息持久化
五. Springboot整合Rabbitmq
1. 搭建boot环境
我们选择基于Spring-Rabbit去操作RabbitMQ
https://github.com/spring-projects/spring-amqp 使用spring-boot-starter-amqp会自动添加spring-rabbit依赖,如下:
2. pom
<dependencies>
<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>
</dependency>
<!--spirngboot集成rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. 配置
3.1. Yml
配置连接rabbitmq的参数
server:
port: 44000
spring:
application:
name: test‐rabbitmq‐producer
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
listener:
simple:
acknowledge-mode: manual #手动签收
prefetch: 1
publisher-confirms: true #消息发送到交换机回调
publisher-returns: true #消息发送到队列失败回调
template:
mandatory: true # 必须设置成true 消息路由失败通知监听者,而不是将消息丢弃
3.2. 配置类
package cn.ronghuanet.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqConfig {
public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
public static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
/**
* 交换机配置
* ExchangeBuilder提供了fanout、direct、topic、header交换机类型的配置
*
* @return the exchange
*/
@Bean(EXCHANGE_TOPICS_INFORM)
public Exchange EXCHANGE_TOPICS_INFORM() {
//durable(true)持久化,消息队列重启后交换机仍然存在
return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
}
//声明队列
@Bean(QUEUE_INFORM_SMS)
public Queue QUEUE_INFORM_SMS() {
Queue queue = new Queue(QUEUE_INFORM_SMS,true);
return queue;
}
//声明队列
@Bean(QUEUE_INFORM_EMAIL)
public Queue QUEUE_INFORM_EMAIL() {
Queue queue = new Queue(QUEUE_INFORM_EMAIL,true);
return queue;
}
/**
* channel.queueBind(INFORM_QUEUE_SMS,"inform_exchange_topic","inform.#.sms.#");
* 绑定队列到交换机 .
*
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
@Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("inform.#.sms.#").noargs();
}
@Bean
public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
@Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("inform.#.email.#").noargs();
}
}
3.3. 入口类
package cn.ronghuanet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author yaohuaipeng
* @date 2019/9/8-22:55
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
4. 生产者
使用RabbitTemplate发送消息
package cn.ronghuanet;
import cn.ronghuanet.config.RabbitmqConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest(classes = App.class)
@RunWith(SpringRunner.class)
public class Producer05_topics_springboot {
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void testSendByTopics() {
for (int i = 0; i < 5; i++) {
String message = "sms email inform to user" + i;
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM, "inform.sms.email", message);
System.out.println("Send Message is:'" + message + "'");
}
}
}
设置失败回调
rabbitTemplate.setMandatory(true); //发送失败,把消息返回给回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{
if(ack){
System.out.println("消息发送到交换机成功...");
}else{
System.out.println("消息发送到交换机失败...");
}
System.out.println("correlationData:"+correlationData);
System.out.println("ack:"+ack);
System.out.println("cause:"+cause);
});
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey)->{
System.out.println("消息发送到队列失败...");
System.out.println("message:"+message);
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
if(i > 0){ //最大重试次数
System.out.println("再次发送...");
rabbitTemplate.convertAndSend(
RabbitmqConfig.EXCHANGE_TOPICS_INFORM, //交换机
"sms.xxxxxxxxxxx",
message);
}
});
5. 消费端
使用@RabbitListener注解监听队列。
package cn.ronghuanet.handler;
import cn.ronghuanet.config.RabbitmqConfig;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class ReceiveHandler {
//监听email队列
@RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
public void receive_email(String msg, Message message, Channel channel) {
System.out.println(msg);
}
//监听sms队列
@RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_SMS})
public void receive_sms(String msg, Message message, Channel channel) {
System.out.println(msg);
}
}
6. 测试
7. 手动签收
7.1. 修改签收模式
spring:
application:
name: rabbitmq
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
listener:
simple:
acknowledge-mode: manual #手动签收
prefetch: 1
7.2. 触发签收
//消费短信
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME_SMS)
public void handlerSMS(String content, Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("消息ID:"+deliveryTag);
System.out.println("短信收到消息:"+content);
channel.basicAck(deliveryTag , false);
}
六. 课程总结
1. 总结5种模式
1.1. HelloWorld
生产者:
-
通过工具创建连接
-
通过连接创建通道
-
通过通道声明队列
-
准备消息
-
发布消息:使用默认交换机,指定队列名
消费者
-
通过工具创建连接
-
通过连接创建通道
-
通过通道声明队列
-
创建一个消息消费回调
-
接受消息:指定队列,签收模式(true,false),回调
签收模式:
-
自动签收
-
手动签收:消息消费成功之后,触发手动签收
channel.basicAck(deliveryTag ,false);
1.2. WorkQueue
生产者:
- 跟helloworld一样
消费者:
- 多个消费者,代码一模一样
能者多劳 :一个消费者处理一个消息
channel.basicQos(1);
上面两种方式都是一个消息只能被一个消费者消费
1.3. Fanout广播
生产者
-
创建交换机:类型是fanout
-
不创建队列
-
发送消息:指定交换机,不指定队列名
消费者
-
创建队列
-
把队列绑定到交换机:不需要指定队列名
1.4. Direct定向
生产者:
-
创建交换机:类型direct
-
不创建队列
-
发送消息:指定交换机,要指定队列名
消费者:
-
创建队列
-
把队列绑定到交换机:需要指定队列名
1.5. topoc通配符
生产者:
-
创建交换机:类型topic
-
不创建队列
-
发送消息:指定交换机,要指定队列名(队列不适用通配符)
消费者:
-
创建队列
-
把队列绑定到交换机:需要指定队列名(队列可以用通配符)
2. 重点
-
RabbitMQ安装
-
RabbitMQ常用模型
-
手动确认机制
-
消息持久化
-
课程发布消息推送
3. 难点
-
RabbitMQ常用模型
-
消息推送
面试题
-
什么情况下消息会丢失,如何防止消息丢失
-
你说4种消息模型,以及他们的区别