笔者去年刚醒悟(觉得自己太菜)的时候,实在不知道从上面学起,偶然看到厮大的RabbitMQ的文章觉得,rabbitMQ很强大,刚好也是中间件,大部分场景都用得到,所以就从中间件下手了。
-
RabbitMQ是使用最广泛的消息代理之一
名词概念
-
Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
-
Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
-
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
-
Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
-
Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
-
Connection
网络连接,比如一个TCP连接。
-
Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
-
Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
-
Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
-
Broker
表示消息队列服务器实体。
2..消息队列解决了什么问题?
异步处理 (节省大量时间)
应用解耦 (MQ作为两个系统的连接点)
流量消锋 (使用MQ过滤掉一些请求,达到请求消减)
日志处理(kafka在这方面比较强大)
这是MQ中最经典的模式点对点,一个消息的发布者,一个消息的订阅者,中见红色的为队列
不足之处:耦合性高,生产者一一对应消费者。多个消费者无法同时消费队列中的消息
队列名字变更,对应的P和C 也要进行修改
于是就出现了Work Queues(工作队列) 模型
Simple 队列 是一一对应的,而且实际开发中 发消息的时间非常短的,但是根据实际业务的需求,消费者处理消息是需要时间的。如果消息的提供者一直等着一个消费者处理完一个消息后,在处理另一个消息,这种是比较耗费效率的。
出现多个消费者的方式,MQ使用了轮询分发(round-robin)
就是 任务是平均分配的,不会说某个消费者处理的块,就多发给它一个消息。
上面的分配方法,虽然解决了一点压力,但是对系统的压力还是比较大的。处理块的消费者处理完之后就不处理队列了,导致了资源浪费,这时候用以下的方式就行优化。
公平分发(即为消费者处理完后,自己确认已处理队列,并像队列要新的消息)
要求,必须关闭自动确认消息,需要手动确认消息
消息应答机制(默认为false)
将消息的应答机制改为false,表示不自动应答。(这个在集成spring 的时候可以做到消息在消费者消费完成之后,手动应答可以保证消息不会再消费端丢失,但是要注意消费端的幂等性)
核心代码为3个参数里面中间的那个参数,
为ture就是自动应答,
自动应答风险,一旦MQ将消息发送出去后,就会从内存在删除这条消息,如果这时消费者在消费的时候突然宕机了,对应的消息就会消失,存在一定的风险。
为false就是手动应答。
手动应答相比于自动应答优点在于,消息处理完成后需要消费者手动确认,此条消息处理完毕,内存中就会删除。当消费者宕机的时候,MQ会自动的发一条消息,给其它消费者。这样就避免了消息的丢失。
持久化机制(已声明的队列,无法修改是否持久化.如果想使用持久化,必须删除队列,重新声明,持久化的队列信息,放在MQ安装目录下,具体哪个地方不知道。)
出现原因,当mq服务器宕机了,我们要避免在服务器中的消息丢失,就要把我们的消息,存入到数据库中。等Mq再次重启的时候,就从数据库中读取未处理的消息.
RabbitMQ推荐的方式,订阅模式(publish/subscribe),必须要先创建一个交换机(,先创建消息的提供者)
1.一个生产者,多个消费者
2.每一个消费者都有自己的队列。
3.生产者不把消息直接发送到队列,而是把消息发送到交换机中
4.每个队列都绑定了交换机
5.生产者发送消息经过交换机,到达队列,实现一个消息被多个消费者消费。
这个模型的原理也是Rabbit的工作原理,消息的提供者先将消息发送到交换机里面,这个交换机必须是提前声明的,否则消息会丢失。我们在创建交换机的时候需要绑定RoutingKey,而消费者在消费消息的时候也是通过队列绑定routingkey进行消费的。这样也就起到了松耦合的作用,上游不知道消息被谁消费,只管将消息发送到Broker中即可,下游也不需要指定是谁发的消息,只需要对自己感兴趣的队列进行订阅即可。(下图为消息流转模型和MQ消费原理)
上面 的图可适用于下列场景
1.注册---发邮件----短信(可以叫在注册成功后叫消息发送至一个队列,然后声明两个消费者,实现发邮件和发短信逻辑)
2.延时消费,在规定的时间内是否支付,如果超过规定时间,则认为放弃了此次订单。(可通过队列来设置延时时间)
3.异步下单,( 两个系统,一个系统下单成功,另一个系统减去库存。相当于MQ解决分布式事务案例。)
在微服务项目中由于粒度过于细,所以我们通常会遇到涉及到两个数据源的情况,这时候我们就需要考虑分布式事务的情况了,因为spring的事务管理器无法帮我们同时管理两个数据源,这个时候有人会想说使用2pc,3pc等,这些都是可以的,使用MQ实现分布式事务是base理论中的软状态来实现最终一致性,这个我后续也会写一篇博客
4.流量削锋(消费的时候采取拉的模式,自己去到broker中去拉取消息然后消费,因为我们平时用得最多的模式就是订阅队列, 然后又消息来的时候直接将消息推送给对应的消费者(这也是设计模式中,观察者模式的机制实现))
上面我们也说了RabbitMQ中的通信机制是将Message发送到交换机中(Exchange)然后由Exchange来发送消息到Broker中。下面我们来讲一下MQ中的交换机。
Rabbit中的交换机主要分为4种
- direct(默认)
- headers
- fanout
- topic
fanout交换机相当于路由的意思。大致结构如下
Fanout (不处理路由键,只要有消息就发送给所有消费者(监听fanout交换机的队列,可以理解为广播)
使用场景可以对个队列绑定同一交换机,当次交换机有消息推送的时候,每个不同的队列,各自处理各自的业务逻辑。
Direct(处理路由键,指定单个消费者,进行消息处理)
使用场景,使用单独的路由key来指定唯一的一个的消费者。比如pro.add,pro.update(每个路由key处理各自的逻辑)
图中的error,info,warning等消息,都是路由key
Topic Exchange(主题)模式 路由模式的升级版
tt.add 消费者的key为tt.*的时候可以匹配
tt.add 消费者的ket为tt.#可以全部匹配
"*"匹配一个分段(用“.”分割)的内容;
"#"匹配0和多个字符;
使用场景相当于是direct升级版,前者是单个匹配,后者是模糊匹配。使用的时候感觉差的不是太多。
消息如何保证100%保证投递成功。
1.保证消息的成功发出
2.保证MQ节点成功接收
3.发送端收到MQ确认,确认到达
4.落库和完善的补偿机制(消息落库,对消息状态进行标记,消息的延迟投递,做二次确认回调检查)
此场景适用于银行等系统,因为入库两次对性能有影响,但是对消息的到达起了一定的作用
上图中的最终解决方案就是开发人员读日志,然后确认并解决问题,上面的所有步骤异常都会存入日志中。读日志的时候可以通过elk+kafka的形式来实现写入快速性和读的可视化。后续我也会说明elk如何搭建。
上图中可以优化的点就是把两次写入MSG的操作改成一次,写入的时候在4步之后执行,如果消息成功到达,那么将此条消息,写入库,这样定时任务,就可以改为查询是否有那条记录,可通过记录id,定时任务开启的时间可以在第一步中消息的唯一id生成之后开始执行。如果没id就说明没写入成功继续执行补偿逻辑。
MQ架构
上面的架构可以解决以下问题:
1.支持消息高性能的序列化转换器,异步发送消息
2.支持消息生产者实例与消费实例,缓存化提升性能
3.支持可靠性消息投递,消息不丢失
4.支付消费端幂等性操作,避免消费端重复消费。
5.支持迅速发送消息,在一些日志场景使用
6.支持延迟消息发送
7.支持事务。
---------本文知识储备来自于蚂蚁课堂,感谢余总的指点。