异步: 同步是发出一个调用请求之后,在没有得到结果之前,就不返回,调用者主动等待这个调用结果。异步通信不需要客户端等待,可以减少客户端性能消耗,大大地提升用户的体验。例如A系统向B,C,D三个系统发送消息,如果其中一个失败,那么导致整个逻辑失败。(火车退票为例,增库存,支付接口,系统API通知)
解耦: 降低系统的耦合性,(耦合的意思是模块间的紧密联系,修改一个模块,多个模块都需要修改)A->B,B->C,C->D
削峰: 电商系统中,有一个瞬间流量达到峰值的情况,普通服务器无法支撑百万或者千万级别的并发量。MQ可以先把所有的流量承接下来。
上面说了引入消息队列的一些场景和优点,那么使用消息队列有什么缺点呢?
系统的可用性降低:比如第一个例子,本来好好的调用三个接口,这个时候接入一个消息队列,凑巧消息队列挂掉了,系统不是直接不能用了,崩溃掉了。
系统的复杂性:加入一个消息队列,若是消息丢失怎么办,消息被重复消费了,消息没有按找既定的顺序消费,都会导致系统出问题。
一致性问题:A系统处理完了直接返回成功了,那么用户就以为成功了,实际上,BC消费成功了,D消费失败了,数据就不一致了。
1,消息生产者连接到RabbitMQ Broker,建立链接(Connection),在链接(Connection)上开启一个信道(Channel)
2,声明一个交换机(Exchange),并设置相关属性,比如交换机类型、是否持久化等
3,声明一个队列(Queue),并设置相关属性,比如是否排他、是否持久化、是否自动删除等
4,使用路由键(RoutingKey)将队列(Queue)和交换机(Exchange)绑定起来
5,生产者发送消息至 RabbitMQ Broker,其中包含路由键、交换器等信息,根据路由键(RoutingKey)发送消息到交换机(Exchange)
6,相应的交换器(Exchange)根据接收到的路由键(RoutingKey)查找相匹配的队列如果找到 ,则将从生产者发送过来的消息存入相应的队列中
7,如果没有找到 ,则根据生产者配置的属性选择丢弃还是回退给生产者
8,关闭信道(Channel)
9,关闭链接(Connection)
消费者生产消息
1,建立链接(Connection)
2,在链接(Connection)上开启一个信道(Channel)
3,请求消费指定队列(Queue)的消息,并设置回调函数(onMessage)
4,[MQ]将消息推送给消费者,消费者接收消息
5,消费者发送消息确定(Ack[acknowledge])
6,[MQ]删除被确认的消息
7,关闭信道(Channel)
8,关闭链接(Connection)
生产者(Producer):发送消息的应用。
消费者(Consumer):接收消息的应用。
队列(Queue):存储消息的缓存。
消息(Message):由生产者通过RabbitMQ发送给消费者的信息。
连接(Connection):连接RabbitMQ和应用服务器的TCP连接。
通道(Channel):连接里的一个虚拟通道。当你通过消息队列发送或者接收消息时,这个操作都是通过通道进行的。
交换机(Exchange):交换机负责从生产者那里接收消息,并根据交换类型分发到对应的消息列队里。要实现消息的接收,一个队列必须到绑定一个交换机。
绑定(Binding):绑定是队列和交换机的一个关联连接。
路由键(Routing Key):路由键是供交换机查看并根据键来决定如何分发消息到列队的一个键。路由键可以说是消息的目的地址。
一种是Pull模式,对应的方法是basicGet。消息存放在服务端,只有消费者主动获取才能拿到消息。如果每搁一段时间获取一次消息,消息的实时性会降低。但是好处是可以根据自己的消费能力决定消息的频率。
另一种是push,对应的方法是BasicConsume,只要生产者发消息到服务器,就马上推送给消费者,消息保存客户端,实时性很高,如果消费不过来有可能会造成消息积压。Spring AMQP是push方式,通过事件机制对队列进行监听,只要有消息到达队列,就会触发消费消息的方法。
RabbitMQ中pull和push都有实现,kafka和RocketMQ只有pull。
4.1 Direct 直连类型 (点对点)
routing key与binding key是一一对应的
一个队列与直连类型的交换机绑定,需指定一个明确的绑定建(binding key)。
生产者发送消息时恢携带一个路由键(routing key)
当消息的路由键与某个队列的绑定建完全匹配时,这条消息才会从交换机路由到这个队列上。多个队列也可以使用相同的绑定建。
直连类型的交换机,适用于一些业务用途明确的消息。
4.2 Topic 主题
一个routing key对应多个符合规则binding key
一个队列与主题烈性的交换机绑定时,可以在绑定键中使用通配符。支持两个通配符:
#代表0个或者多个单词
*代表不多不少一个单词
单词(Word)指的是用英文的"."隔开的字符,例如:a.bc.def是3个单词。
主题烈性的交换机适用于一些根据业务主题或者消息等级过滤消息的场景,比如说一条消息可能既跟资金有关,又跟风控有关,那就可以让这个消息制定一个多级的路由键。第一个单词代表跟资金相关,第二个单词代表跟风控相关,下游的业务系统的队列就可以使用不同的绑定键去接受消息了。
4.3 Fanout 广播
这种情况下不需要分析锁接受到的Routing key
广播类型的交换机与队列绑定时,不需要制定绑定键,因此生产者发送消息到广播类型交互机上,也不需要携带路由键。消息到达交换机时,所有与之绑定了的队列,都会受到相同的消息的副本。
业务场景:超过30分钟未付款的订单自动关闭,这个功能应该怎么实现?
RabbitMQ本身不支持延迟投递,总的来说有2种实现方案:
1,先存储到数据库,用定时任务扫描
(定时任务比较容易实现,比如每搁1分钟扫描一次,查出30分钟之前未付款的订单,把状态改为关闭。但是如果数据量过大,比如:10万条,把这些全部的数据查询到内存中逐条处理,也会给服务器带来很大的压力,影响正常的业务运行)
2,利用RabbitMQ的死信队列(Dead Letter Queue)实现
(借助RabbitMQ消息的特性),
(1)队列有一个消息过期属性,这个属性叫x-messge-ttl,所有队列中的消息超过时间未被消费时,都会过期。TTL(Time To Live)。
(2)RabbitMQ中消息的单独的过期时间
在发送消息的时候通过MessageProperities指定消息属性
问题:如果同时指定了msg TTL和Queue TTL,则小的那个时间生效。有了过期时间还不够,这个消息不能直接丢弃,不然就没办法消费了。最好是丢到一个容器里边,这样就可以实现延迟消费了。还需要用到死信队列
死信:消息过期以后,如果没有任何配置,是会直接丢弃的。我们可以通过配置让这样的消息变成死信(Dead Letter),在别的地方存储。
队列在创建的时候可以指定一个死信交换机DLX(Dead Letter Exchange)。死信交换机绑定的队列被称为死信队列DLQ(Dead Letter Queue),DLX实际上也是普通的交换机,DLQ也是普通的交换机(例如替补球员也是普通球员)。
也就是说如果消息过期了,队列制定了DLX,就会发送到DLX,如果DLX绑定了DLQ,就会路由到DLQ,路由到DLQ之后,我们就可以消费了。
总结一下:利用过期时间,过期之后投递到DLX ,路由到DLQ,监听DLQ,实现了延迟队列。
消息的流转流程:
生产者-原交换机-原队列(超过TTL之后)——死信交换机——死信队列——最终消费者。
Message消息在什么情况会变成死信?
(1)消息的时间过期
(1)消息被消费者拒绝并且未设置重回队列:(NACK||Reject)&&requeue==false
(2)队列达到最大长度,超过了Max Length(消息数)或者Max Length bytes(字节数) ,最先入队的消息会被发送到DLX。
3.用插件rabbitmq-delayed-messge-exchange
六,消息可靠性
1代表消息从生产者发送到Broker? 如何确认?
RabbitMQ里边提供了两种机制,服务端确认机制,第一种是Transaction(事务)模式,第二种是Confirm(确认)模式。
Confirm确认模式
(2)代表消息从Exchange路由到Queue,找不到怎么办?
可能因为Routingkey错误,或者消息队列不存在
两种方式处理无法路由的消息,
一种是让服务端重新发送给生产者
(3)代表消息在Queue中存储,存储消息,是否会丢
可以对队列持久化,交换机持久化,消息持久化。如果消息没有持久化,保存在内存中,队列还在,但是消息重启后会消失。
消息持久化
1,调用生产者API,修改数据库中表的状态,只要API没被调用,数据状态没有被修改,则认为下游系统没有收到这条消息
2,发送相应消息给生产者
如果生产者的API没有被调用,也没有收到消费者的响应消息,如何处理?
建立补偿机制
后台代码中创建定时任务,
我们一般把应用连接诶到内存节点(读写快),磁盘节点用来备份。
集群的配置步骤:
1,配置Hosts以便互相通信
2,同步erlang.cookie
3,加入集群(join cluster命令)
Rabbit有两种集群模式:普通集群模式和镜像队列模式。
1,普通集群模式下,不同的节点之间只会互相同步元数据(交换机,队列,绑定关系,Vhost的定义),而不会同步消息
同理,如果消费者连接的是节点2,要从队列1上拉取消息,消息会从节点1抓饭到节点2,。其它节点起到了一个路由的作用,类似于指针。
2,镜像集群模式
3,对于高可用集群部署
基于Docker安装HAproxy负载+keepalived高可用
1,我们规划了两个内存节点,一个磁盘节点。所有的节点之间通过镜像队列的方式同步数据。内存节点用来给应用访问,磁盘节点用来持久化数据。
消息队列整体设计思路
主要是设计一个整体的消息被消费的数据流。
这里会涉及到:消息生产Producer、Broker(消息服务端)、消息消费者Consumer。
1.Producer(消息生产者):发送消息到Broker。
2.Broker(服务端):Broker这个概念主要来自于Apache的ActiveMQ,特指消息队列的服务端。
主要功能就是:把消息从发送端传送到接收端,这里会涉及到消息的存储、消息通讯机制等。
3.Consumer(消息消费者):从消息队列接收消息,consumer回复消费确认。
Broker(消息队列服务端)设计重点
1)消息的转储:在更合适的时间点投递,或者通过一系列手段辅助消息最终能送达消费机。
2)规范一种范式和通用的模式,以满足解耦、最终一致性、错峰等需求。
3)其实简单理解就是一个消息转发器,把一次RPC做成两次RPC,发送者把消息投递到broker,broker再将消息转发一手到接收端。
总结起来就是两次RPC加一次转储,如果要做消费确认,则是三次RPC。
为了实现上述消息队列的基础功能:
1)消息的传输
2)存储
3)消费
就需要涉及到如下三个方面的设计:
1)通信协议
2)存储选择
3)消费关系维护
通讯协议
消息Message:既是信息的载体,消息发送者需要知道如何构造消息,消息接收者需要知道如何解析消息,它们需要按照一种统一的格式描述消息,这种统一的格式称之为消息协议。
传统的通信协议标准有XMPP和AMQP协议等,现在更多的消息队列从性能的角度出发使用自己设计实现的通信协议。
1.JMS
JMS(Java MessageService)实际上是指JMS API。JMS是由Sun公司早期提出的消息标准,旨在为java应用提供统一的消息操作,包括创建消息、发送消息、接收消息等。
JMS提供了两种消息模型:
1)点对点
2)以及publish-subscribe(发布订阅)模型。
当采用点对点模型时,消息将发送到一个队列,该队列的消息只能被一个消费者消费。
而采用发布订阅模型时,消息可以被多个消费者消费。
在发布订阅模型中,生产者和消费者完全独立,不需要感知对方的存在。
2.AMQP
AMQP是 Advanced Message Queuing Protocol,即高级消息队列协议。
AMQP不是一个具体的消息队列实现,而 是一个标准化的消息中间件协议。
目标是让不同语言,不同系统的应用互相通信,并提供一个简单统一的模型和编程接口。 目前主流的ActiveMQ和RabbitMQ都支持AMQP协议。
AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。
JMS和AMQP比较
JMS: 只允许基于JAVA实现的消息平台的之间进行通信
AMQP: AMQP允许多种技术同时进行协议通信
3.Kafka的通信协议
Kafka的Producer、Broker和Consumer之间采用的是一套自行设计的基于TCP层的协议。Kafka的这套协议完全是为了Kafka自身的业务需求而定制的。
对于分布式系统,存储的选择有以下几种
1.内存
2.本地文件系统
3.分布式文件系统
4.nosql
5.DB
消费关系处理
除了上述的消息队列基本功能以外,消息队列在某些特殊的场景还需要支持事务,消息重试等功能。
消息队列需要支持高级特性
消息的顺序
投递可靠性保证
消息持久化
支持不同消息模型
多实例集群功能
事务特性等
以上,是从0到1设计一个MQ消息队列的经验分享。
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。