适用场景:有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列
主题交换机
直连交换机的routing_key
方案非常简单,如果我们希望一条消息发送给多个队列,那么这个交换机需要绑定上非常多的routing_key
,假设每个交换机上都绑定一堆的routing_key
连接到各个队列上。那么消息的管理就会异常地困难。
所以RabbitMQ
提供了一种主题交换机,发送到主题交换机上的消息需要携带指定规则的routing_key
,主题交换机会根据这个规则将数据发送到对应的(多个)队列上。
主题交换机的routing_key
需要有一定的规则,交换机和队列的binding_key
需要采用*.#.*.....
的格式,每个部分用.
分开,其中:
*
表示一个单词#
表示任意数量(零个或多个)单词
当一个队列的绑定键为#
的时候,这个队列将会无视消息的路由键,接收所有的消息
首部交换机
首部交换机是忽略routing_key
的一种路由方式。路由器和交换机路由的规则是通过Headers
信息来交换的,这个有点像HTTP
的Headers
。
将一个交换机声明成首部交换机,绑定一个队列的时候,定义一个Hash
的数据结构,消息发送的时候,会携带一组hash数据结构的信息,当Hash
的内容匹配上的时候,消息就会被写入队列。
绑定交换机和队列的时候,Hash结构中要求携带一个键x-match
,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了
相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串
- any: 只要在发布消息时携带的有一对键值对headers满足队列定义的多个参数
arguments
的其中一个就能匹配上,注意这里是键值对的完全匹配,只匹配到键了,值却不一样是不行的; - all:在发布消息时携带的所有
Entry
必须和绑定在队列上的所有Entry完全匹配
Binding
Exchange和Queue之间的虚拟连接,Exchange在与多个Message Queue发生Binding后会生成一张路由表,路由表中存储着Message Queue
所需消息的限制条件即Binding Key。当Exchange收到Message时会解析其Header得到Routing Key,Exchange根据Routing Key与Exchange Type将Message路由到Message Queue。Binding Key由Consumer在Binding Exchange
与Message Queue时指定,而Routing Key由Producer发送Message时指定,两者的匹配方式由Exchange Type决定
Routing Key
一个路由规则,虚拟机可用它来确定如何路由一个特定消息。
Queue
也称为Message Queue
,消息队列,保存消息并将它们转发给消费者。
消息发布流程:
- 生产者和Broker建立TCP连接。
- 生产者和
Broker
建立通道。 - 生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
- Exchange将消息转发到指定的Queue(队列)
消息接收流程:
- 消费者和Broker建立TCP连接 。
- 消费者和
Broker
建立通道。 - 消费者监听指定的Queue(队列)
- 当有消息到达Queue时Broker默认将消息推送给消费者。
- 消费者接收到消息。
消息流转过程
生产者生产出Message并投递到Exchange
上
一个Exchange可以绑定多个Message Queue
,它根据路由策略(routing key
)路由到指定的队列,最后由消费端去监听队列
工作模式
队列模式:
对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
1、一条消息只会被一个消费者接收;
2、rabbitmq采用轮询的方式将消息是平均发送给消费者的;
3、消费者在处理完某条消息后,才会收到下一条消息。
发布订阅模式:
1、每个消费者监听自己的队列。
2、生产者将消息发给broker
,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息
对应交换机中的fanout
类型
路由模式:
1、每个消费者监听自己的队列,并且设置routingkey。
2、生产者将消息发给交换机,由交换机根据routingkey
来转发消息到指定的队列。
对应交换机中的direct
类型
通配符模式:
对应交换机中的topics
类型
Header转发器模式:
对应交换机中的header
类型
远程过程调用模式:
RPC即客户端远程调用服务端的方法,使用MQ可以实现RPC的异步调用,基于Direct交换机实现,流程如下:
- 客户端即是生产者就是消费者,向
RPC
请求队列发送RPC调用消息,同时监听RPC响应队列。 - 服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果。
- 服务端将RPC方法的结果发送到
RPC
响应队列。 - 客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。
基本使用
基本命令行操作
关于服务的操作:
-
服务启动:
rabbitmqctl start_app
/rabbitmq-server start &
-
服务停止:
rabbitmqctl stop_app
/rabbitmq-server stop
-
服务重启:
service rabbitmq-server restart
-
节点状态:
rabbitmqctl status
关于用户的操作:
- 添加用户:
rabbitmqctl add_user username password
- 列出所有用户:
rabbitmqctl list_users
- 删除用户:
rabbitmqctl delete_user username
- 清除用户权限:
rabbitmqctl clear_permissions -p vhostpath username
- 列出用户权限:
rabbitmqctl list_user_permissions username
- 修改密码:
rabbitmqctl change_password username newpassword
- 设置用户权限:
rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*"
关于虚拟主机的操作:
- 创建虚拟主机:
rabbitmqctl add_vhost vhostpath
- 列出所有虚拟主机:
rabbitmqctl list_vhost
- 列出虚拟主机上所有权限:
rabbitmqctl list_permissions -p vhostpath
- 删除虚拟主机:
rabbitmqctl delete_vhost vhostpath
关于消息队列的操作:
- 查看所有队列信息:
rabbitmqctl list_queues
- 清除队列里的消息:
rabbitmqctl -p vhostpath purge_queue blue
高级命令
rabbitmqctl reset
:移除所有数据,要在rabbitmqctl stop_app
之后使用- 组成集群命令:
rabbitmqctl join_cluster [--ram]
(ram内存级别存储,disc磁盘) - 查看集群状态:
rabbitmqctl cluster_status
- 修改集群节点的存储形式:
rabbitmqctl change_cluster_node_type disc | ram
- 忘记(摘除)节点:
rabbitmqctl forget_cluster_node [--offline]
(offline服务不启动的情况下) - 修改节点名称:
rabbitmqctl rename_cluster_node oldnode1 newnode1 [oldnode2] [newnode2 ...]
集群配置失败,故障转移等情况下可以将启动失败的节点给移除掉。它可以在不启动的情况下对节点的摘除
入门使用
引入RabbitMQ依赖:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5</version>
</dependency>
创建一个生产者
public class Producer {
private static final String QUEUE_NAME = "test01";
public static void main(String[] args) throws IOException, TimeoutException {
// 1\. 创建连接工厂并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
// 设置虚拟机
connectionFactory.setVirtualHost("/test");
// 2\. 通过连接工厂建立连接
Connection connection = connectionFactory.newConnection();
// 3\. 通过connection创建Channel
Channel channel = connection.createChannel();
// 4\. 通过Channel发送数据 (exchange, routingKey, props, body)
// 不指定Exchange时, 交换机默认是AMQP default, 此时就看RoutingKey
// RoutingKey要等于队列名才能被路由, 否则消息会被删除
for (int i = 0; i < 5; i++) {
String msg = "Learn For RabbitMQ-" + i;
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
System.out.println("Send message : " + msg);
}
// 5.关闭连接
channel.close();
connection.close();
}
}
创建一个消费者
public class Consumer {
private static final String QUEUE_NAME = "test01";
public static void main(String[] args) throws IOException, TimeoutException {
// 1\. 创建连接工厂并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
// 设置虚拟机
connectionFactory.setVirtualHost("/test");
// 2\. 通过连接工厂建立连接
Connection connection = connectionFactory.newConnection();
// 3\. 通过connection创建Channel
Channel channel = connection.createChannel();
// 4\. 声明队列 (queue, durable, exclusive, autoDelete, args)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 5\. 创建消费者
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
/**
* 获取消息 (监听到有消息时调用)
* @param consumerTag 消费者标签, 在监听队列时可以设置autoAck为false,即手动确认(避免消息的丢失), 消息唯一性处理
* @param envelope 信封
* @param properties 消息的属性
* @param body 消息的内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("Received message : " + msg);
}
};
// 6\. 设置Channel, 监听队列(String queue, boolean autoAck,Consumer callback)
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
参数:
queue
:队列名称durable
:持久化,true 即使服务重启也不会被删除exclusive
:独占,true 队列只能使用一个连接,连接断开队列删除autoDelete
:自动删除,true 脱离了Exchange(连接断开),即队列没有Exchange关联时,自动删除arguments
:扩展参数autoAck
:是否自动签收(回执)
不指定Exchange时,交换机默认是AMQP default,此时就看RoutingKey
,RoutingKey要等于队列名才能被路由,否则消息会被删除
交换机属性
Name
:交换机名称
Type
:交换机类型—— direct、topic、fanout、header
Durability
:是否需要持久化,true为持久化
Auto Delete
:当最后一个绑定到Exchange上的队列删除后,即Exchange上没有队列绑定,自动删除该Exhcange
Internal
:当前Exchange是否用于RabbitMQ内部使用,大多数使用默认False
Arguments
:扩展参数,用于扩展AMQP协议定制化使用
Direct Exchange:
// Consumer
// 声明交换机:
// (String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object) arguments)
channel.exchangeDeclare("exchangeName", BuiltinExchangeType.DIRECT, true, false, false, null);
// 声明队列 (String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object) args)
channel.queueDeclare("queueName", true, false, false, null);
// 建立绑定关系:
channel.queueBind("queueName", "exchangeName", "routingKey");
// ===================================================================
// Producer
// 发送消息 (String exchange, String routingKey, BasicProperties props, Bytes[] body)
channel.basicPublish("exchangeName", "routingKey", null, "msg".getBytes());
Topic Exchange:
// Consumer
// 声明交换机:
// (String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object) arguments)
channel.exchangeDeclare("exchangeName", BuiltinExchangeType.TOPIC, true, false, false, null);
// 声明队列 (String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object) args)
channel.queueDeclare("queueName", true, false, false, null);
// 建立绑定关系:
channel.queueBind("queueName", "exchangeName", "routingKey.#");
// ===================================================================
// Producer
// 发送消息 (String exchange, String routingKey, BasicProperties props, Bytes[] body)
channel.basicPublish("exchangeName", "routingKey.hi", null, "msg".getBytes());
channel.basicPublish("exchangeName", "routingKey.save", null, "msg".getBytes());
channel.basicPublish("exchangeName", "routingKey.save.hi", null, "msg".getBytes());
因为使用了模糊匹配的"#
",可以匹配到发送的三条消息。因此可以收到三条消息
Fanout Exchange:
// Consumer
// 声明交换机:
// (String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object) arguments)
channel.exchangeDeclare("exchangeName", BuiltinExchangeType.FANOUT, true, false, false, null);
// 声明队列 (String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object) args)
channel.queueDeclare("queueName", true, false, false, null);
// 建立绑定关系:
//(不设置routingKey, 这里为空)
channel.queueBind("queueName", "exchangeName", "");
// ===================================================================
// Producer
// 发送消息 (String exchange, String routingKey, BasicProperties props, Bytes[] body)
// 同样routingKey为空 (也可以是任意字符串, 因为fanout并不依据routingKey)
channel.basicPublish("exchangeName", "", null, "msg".getBytes());
高级特性
可靠性投递
什么是生产端的可靠性投递
- 保障消息的成功发出
- 保障MQ节点成功接收
- 发送端收到MQ节点(Broker)确认应答(已收到)
- 完善消息进行补偿机制
可靠性投递的方案一
消息落库(持久化至数据库),对消息状态进行打标,如若消息未响应,进行轮询操作
1.把业务消息落库,再生成一条消息落库到消息DB用来记录(譬如消息刚创建,正在发送中 status: 0)
缺点:对数据库进行两次持久化
2.生产端发送消息。
3.Broker端收到后,应答至生产端。Confirm Listener
异步监听Broker的应答。
4.应答表明消息投递成功后,去消息DB中抓取到指定的消息记录,更新状态,如status: 1
5.如在3中出现网络不稳定等情况,导致Listener未收到消息成功确认的应答。
那么消息数据库中的status就还是0,而Broker可能是接收到消息的状态。
因此设定一个规则(定时任务),例如消息在落库5分钟后(超时)还是0的状态,就把该条记录抽取出来。
6.重新投递
7.限制一个重试的次数,譬如3次,如果大于3次,即为投递失败,更新status的值
用人工补偿机制去查询消息失败的原因
高并发场景消息的延迟投递,做二次确认,回调检查
Upstream service:生产端
Downstream service:消费端
1:业务消息落库后,发送消息至Broker。
2:紧接着发送第二条延迟(设置延迟时间)检查的消息。
3:消费端监听指定的队列接收到消息进行处理
4:处理完后,生成一条响应消息发送到Broker。
5:由Callback服务去监听该响应消息,收到该响应消息后持久化至消息DB(记录成功状态)。
6:到了延迟时间,延迟发送的消息也被Callback服务的监听器监听到后,去检查消息DB。如果未查询到成功的状态,Callback服务需要做补偿,发起RPC通讯,让生产端重新发送。生产端通过接收到的命令中所带的id去数据库查询该业务消息,再重新发送,即跳转到1。
该方案减少了对数据库的存储,保证了性能
消费端幂等性
避免消息的重复消费
消费端实现幂等性,接收到多条相同的消息,但不会重复消费,即收到多条一样的消息。
方案:
1.唯一ID + 指纹码机制
- 唯一ID + 指纹码(业务规则、时间戳等拼接)机制,利用数据库主键去重
SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码
未查询到就insert
,如有说明已处理过该消息,返回失败- 优点:实现简单
- 缺点:高并发下有数据库写入的性能瓶颈
- 解决方案:根据ID进行分库分表、算法路由
2.利用Redis的原子性
需要考虑的问题:
- 是否要落库数据库,如落库,数据库和缓存如何做到数据的一致性
- 不落库,数据存储在缓存中,如何设置定时同步的策略(可靠性保障)
Confirm确认消息
生产者投递消息后,如果Broker收到消息,则会给生产者一个应答。
写在最后
学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!
最后再分享的一些BATJ等大厂20、21年的面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。
领取方式:戳这里即可免费领取
Mybatis面试专题
MySQL面试专题
Confirm确认消息
生产者投递消息后,如果Broker收到消息,则会给生产者一个应答。
写在最后
学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!
最后再分享的一些BATJ等大厂20、21年的面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。
领取方式:戳这里即可免费领取
[外链图片转存中…(img-h2MY3VkP-1628396048402)]
Mybatis面试专题
[外链图片转存中…(img-pQTLbFBJ-1628396048402)]
MySQL面试专题
[外链图片转存中…(img-12MBUNrg-1628396048403)]
并发编程面试专题