队列中存放的内容是 message ,是一种跨进程的通信机制,用于上下游传递消息。
在互联网架构中,MQ 是一种非常常见的上下游 “逻辑解耦 + 物理解耦” 的消息通信服务。
比如某一时刻开放购买的场景,消费者不能一下子消费那么多订单,可以让订单在队列中排队,过几秒钟再处理,总比不能处理的体验要好。
可以实现消费者和生产者之间的解耦。比如说下单的动作需要多个服务配合,如果一个服务出现了问题,下单都无法完成,但是如果是基于消息队列,前一个服务可以将请求发送到队列,继续去做自己的事情。
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完。
这时候只需要在A调用完B,A去做自己的事情,B处理完成后,发送一条消息到MQ,MQ再将次消息转发给A。
Kafka 主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka 了。
天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。
结合 erlang 语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ
对于我的业务来说就是
所以rabbitMQ就足够了。
Producer: 消息生产者,就是投递消息的程序
Connection:producer/consumer 和 broker 之间的 TCP 连接。
Channel信道:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个线程创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销。每个channel代表一个会话任务,由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。
Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker。简单来说就是消息队列服务器实体
Virtual host:出于多用户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等。可以理解为虚拟broker ,即mini-RabbitMQ server。其内部均含有独立的queue、exchange和binding等,但最最重要的是,其拥有独立的权限系统,可以做到vhost范围的用户控制。当然,从RabbitMQ的全局角度,vhost可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。
Exchange:消息交换机。message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。消息最终被送到这里等待 consumer 取走。
Binding:绑定,exchange 和 queue 之间的虚拟连接,即将exchange和queue按照路由规则绑定起来,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
channel.basicQos(1);
来设置不公平分发(0是公平分发),只要有消费者空闲,并且队列中有消息,就接收队列中的消息并执行。channel.basicQos(prefetchCount);
设置详见:https://blog.csdn.net/weixin_39724194/article/details/130050301
消息不可靠的情况可能是消息丢失,劫持等原因;
丢失又分为:
生产者向 exchange 投递了 message ,而由于各种原因导致该message 丢失,但发送者却不知道。
可导致 blackholed 的情况:
从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息;
发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降;
一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后:
channel.queueDeclare(MqConnectUtil.QUEUE_NAME, true, false, false, null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long begin = System.currentTimeMillis();
// 准备消息的监听器,监听哪些消息成功了,哪些消息失败了
// 参数一:确认成功的回调函数
// 参数二:确认失败的回调函数
// deliveryTag:消息的标记
// multiple: 是否为批量确认
channel.addConfirmListener((deliveryTag, multiple) -> {},
(deliveryTag, multiple) -> {log.debug("未确认的消息:{}", deliveryTag);});
// 批量发消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
channel.basicPublish("", UUID.randomUUID().toString(), null, String.valueOf(i).getBytes());
}
最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列, 比如说用 ConcurrentSkipListMap在 confirm callbacks 与发布线程之间进行消息的传递。
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。
这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。
这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
如果消息还没来得及消费,但是rabbitmq挂了,但是因为持久化数据了,所以在重启时,会从日志中读取未消费的消息
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
选用规则是高吞吐量和数据传输安全性方面做权衡
消费者丢数据一般是因为采用了自动确认消息模式,消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;
改为手动确认消息即可!处理消息成功后,手动回复确认消息。
否定确认后如果设置了requeue=true,则消息会重新回到队列头的位置,等待被消费,如果设置了requeue为false,则会被丢弃
手动应答可以通过设置批量应答来减少网络拥堵。
生产者生产的消息从不会直接发送到队列, 生产者只能将消息发送到交换机 (exchange),然后交换机将消息推入到设置的队列中。
如果我们没有指定交换机,用的空字符串的话,表示是默认或无名称交换机。消息从路由发送到队列是由routingKey(bindingKey)绑定key指定的。
交换机的类型包括:
可以通过如下代码创建exchange并设置名称和类型:
/**
* 声明一个 exchange
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。哪怕交换机和队列关联的key是不同的也不影响
消息只去到它绑定的 routingKey 队列中去
如上图的绑定关系下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列 Q1。绑定键为 black或green的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃
如果 exchange 的绑定类型是 direct,但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了,就跟广播差不多
简单说就是routingkey模糊匹配的交换机
当我们的队列想要接收某个类型的消息的时候,比如现在的消息有桔子苹果汽水、桔子橙子汽水、西瓜苹果汽水,我的某个队列只想要有桔子的汽水,这时候使用direct类型的交换机就比较难做到了,这时候我们可以使用主题交换机。
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词。比如说:”stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”. 这种类型的。当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的:
*(星号)
可以代替一个单词#(井号)
可以替代零个或多个单词功能:生产者将消息直接分给延迟交换机,延迟交换机在经过延迟时间后将消息分发。
标题交换机和扇形交换机都不需要路由键routingKey,标题交换机是通过Headers头部来将消息映射到队列的,有点像HTTP的Headers,Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串(string)而是Object类型。
交换机是如何跟队列进行关联的呢?,就是通过bingdings,我们可以在bindings中设置routingkey,并关联一个队列,将交换机和队列进行绑定。
channel.queueBind(queueName, EXCHANGE_NAME, "01")
没有持久化的队列就是临时队列。每当我们连接到mq时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,一旦我们断开了消费者的连接,临时队列将被自动删除。
看Features是否有D的标识,如果没有就说明是临时队列。
producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
死信进入到死信队列后,可以被专门用于处理死信队列消息的消费者消费。
如果出现死信队列和普通队列都满的情况,此时考虑消费者消费能力不足,可以对消费者开多线程进行处理。
应用场景:
概念:延时队列就是用来存放需要在指定时间被处理的元素的队列
这些场景都有一个特点:需要在某个事件发生之后或者之前的指定时间点完成某一项任务。
功能:生产者将消息直接分给延迟交换机,延迟交换机在经过延迟时间后将消息分发。
详见:https://blog.csdn.net/weixin_39724194/article/details/130050301
首先谈谈什么是幂等性:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断, 故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性, 这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。
MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识,比如时间戳或者 UUID ,订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。
业界主流的幂等性有两种操作:
当然也可以根据业务不同去进行操作:
简单说:就是根据队列权重,优先消费权重高的消息
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧。
但是,天猫商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果、小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理。
所以苹果小米等大客户的催付订单我们可以放到优先级高的队列中,其他的催付订单可以放到优先级较低的队列中。
惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因 (比如消费者下线、宕机亦或者是由于维护而关闭等) 而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制
系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人 ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。
系统复杂性提高硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。
A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,最好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10倍。但是关键时刻,用,还是得用的
加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key),消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
Topic 类型的 Exchange 与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型 Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
拆分多个queue(消息队列),每个queue(消息队列) 一个consumer(消费者),就是多一些queue(消息队列)而已,确实是麻烦点;
或者就一个queue (消息队列)但是对应一个consumer(消费者),然后这个consumer(消费者)内部用内存队列做排队,然后分发给底层不同的worker来处理
RabbitMQ是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以RabbitMQ为例子讲解第一种MQ的高可用性怎么实现。RabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式。
就是Demo级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式
意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。
你创建的queue,只会放在一个RabbitMQ实例上,但是每个实例都同步queue的元数据(元数据可以认为是queue的一些配置信息,通过元数据,可以找到queue所在实例)。
你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个queue的读写操作。
这种模式,才是所谓的RabbitMQ的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个RabbitMQ节点都有这个queue的一个完整镜像,包含queue的全部数据的意思。然后每次你写消息到queue的时候,都会自动把消息同步到多个实例的queue上。
RabbitMQ有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
这样的好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个queue的完整数据,别的consumer都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ一个queue的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个queue的完整数据。
临时紧急扩容:
假设你用的是RabbitMQ,RabbtiMQ是可以设置过期时间的,也就是 TTL。如果消息在queue中积压超过一定的时间就会被RabbitMQ给清理掉,这个数据就没了。那这就是第二个坑了。
这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,
然后重新灌入mq里面去,把白天丢的数据给他补回来。也只能是这样了。假设1万个订单积压在mq里面,没有处理,其中 1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再补一次。
不能。第一,你无法控制所创建的 queue 实际分布在 cluster 里的哪个 node 上(一般使用 HAProxy + cluster 模型时都是这样),这可能会导致各种跨地域访问时的常见问题;第二,Erlang 的 OTP 通信框架对延迟的容忍度有限,这可能会触发各种超时,导致业务疲于处理;第三,在广域网上的连接失效问题将导致经典的“脑裂”问题,而RabbitMQ 目前无法处理(该问题主要是说 Mnesia)
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
首先这个mq得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下kafka的设计理念,broker->topic->partition,每个partition放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
其次你得考虑一下这个mq的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
其次你考虑一下你的mq的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的kafka的高可用保障机制。多副本 -> leader&follower->broker挂了重新选举 leader 即可对外服务。能不能支持数据0丢失啊?可以呀,有点复杂的。
比如订单系统通过mq将不同任务分给不同系统如库存、支付、短信,如果在某一个服务出现异常后,保证各个服务的数据一致性问题?
根据不同的业务需求做具体的业务分析,可以使用rocketmq来实现事务消息,也可以通过DB来实现消息状态处理;
看看这个分析:
https://blog.csdn.net/weixin_43466542/article/details/101676944
RabbitMQ就是 AMQP 协议的 Erlang 的实现(当然 RabbitMQ 还支持 STOMP2、 MQTT3 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。
RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。