消息队列MQ

消息队列的作用

异步

将比较耗时而且不需要即时 (同步) 返回结果的操作作为消息放入消息队列随后再消费处理,先集中资源处理核心逻辑,提升核心逻辑的处理效率。

举例:
比如秒杀系统中,需要完成 :风险控制;库存锁定;生成订单;短信通知;更新统计数据 五个步骤
如果不优化,则完成五步才能给客户端返回结果,但能否决定秒杀成功,实际上只有风险控制和库存锁定这 2 个步骤。所以当服务端完成前面 2 个步骤,确定本次请求的秒杀结果后,就可以马上给用户返回响应,然后把请求的数据放入消息队列中,由消息队列异步地进行后续的操作。如下图:
在这里插入图片描述

流量削峰

将短时间高并发请求存入MQ,消费者按自己的能力从MQ中取数据即可,从而削平高峰期的并发流量,提升系统性能。

优点:能根据下游的处理能力自动调节流量,达到“削峰填谷”的作用。

代价:增加了系统调用链环节,导致总体的响应时延变长。上下游系统都要将同步调用改为异步消息,增加了系统的复杂度。

举例:
继续以秒杀系统为例,虽然利用消息队列实现了部分工作的异步处理,但我们还面临一个问题:如何避免过多的请求压垮秒杀系统?

设计思路:使用消息队列隔离网关和秒杀服务,以达到流量控制和保护后端服务的目的。

加入消息队列后,整个秒杀流程变为:

在这里插入图片描述
网关在收到请求后,将请求放入请求消息队列;
后端服务从请求消息队列中获取 APP 请求,完成后续秒杀处理过程,然后返回结果。

这样一来:
当短时间内大量的秒杀请求到达网关时,不会直接冲击到后端的秒杀服务,而是先堆积在消息队列中,后端服务按照自己的最大处理能力,从消息队列中消费请求进行处理。
对于超时的请求可以直接丢弃,APP 将超时无响应的请求处理为秒杀失败即可。

解耦

比如A 系统发送数据到 BCD 三个系统,如果直接采用接口调用发送的话。现在新增 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?只要有一点改动,A系统都需要进行更改并重新发布上线,A 系统负责人几乎崩溃,

但是采用MQ的话,A系统将消息放到MQ中即可,谁要数据就到MQ中取即可。

所以通过一个 MQ,采用发布订阅模式,A 系统就跟其它系统彻底解耦了

消息队列适用场景

适用场景:秒杀、发邮件、发短信、高并发订单等
不适合场景:银行转账、电信开户、第三方支付等对数据一致性要求极高的。

消息队列的问题

增加了系统的延迟

增加了系统的复杂度;

可能产生数据不一致的问题:
本地事务回滚了,但是消息发出去了,不一致!
本地事务提交了,但是消息未发出去,不一致!

RocketMQ VS Kafka

RocketMQ

RocketMQ 对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应,如果你的应用场景很在意响应时延,那应该选择使用 RocketMQ。

RocketMQ 的性能极高,每秒钟大概能处理几十万条消息。

kafka

Kafka在设计上采用批量和异步的思想,关注的是整体的吞吐量,会将消息缓存在本地,然后批量发送,所以Kafka的异步收发性能比rocketMQ好,但是实时处理能力比不上RocketMQ ,所以Kafka更适合大数据场景,不太适合对响应延迟低的在线业务场景

消息队列模型

队列模型

对于 MQ 来说,其实不管是 RocketMQ、Kafka 还是其他消息队列,它们的本质都是:一发一存一消费。
如下就是最基础的消息队列模型。
在这里插入图片描述
这便是队列模型:它允许多个生产者往同一个队列发送消息。但是,如果有多个消费者,实际上是竞争的关系,也就是一条消息只能被其中一个消费者接收到

发布-订阅模型

如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。
为了解决这个问题,就演化出了另外一种消息模型:发布-订阅模型
在这里插入图片描述

在发布-订阅模型中,存放消息的容器变成了 “主题”,订阅者在接收消息之前需要先 “订阅主题”。每个订阅者都可以收到同一个主题的全量消息。

RocketMQ 的消息模型

RocketMQ 使用的消息模型是标准的发布 - 订阅模型
另外,为了提升消费端总体的消费性能,RocketMQ 在主题下面增加了队列的概念(kafka中叫分区)。
为什么新增队列?
因为如果不新增,每个主题在任意时刻,只能有一个消费者实例进行消费(否则就消息乱序了),消费性能很低。为了提高消费性能,RocketMQ 在主题下面增加了队列的概念。
在这里插入图片描述
1、主题(topic)中有多个队列(队列数量可以水平进行扩容),一个生产者将生产的消息发送给主题中的某一个队列(注意是只发给一个队列,根据一定的路由规则,比如取模)
注意:只有队列中的消息是有序的,主题层面消息无序

2、消费者有一个消费组的概念,消费组里面有多个消费者,可以同时消费多个队列,比如三个消费线程同时分别消费三个队列,并发度由队列数量决定,通过增加队列数量提升并发度。
比如消费组1里面有consumer1,consumer2两个消费者,那么12两个消费者可以同时消费一个队列,也可以消费多个队列,组内是竞争关系,1消费了某个消息,2就不能再消费这条消息,但是组之间是共享关系,一条消息只有被所有组都消费了才能删除。

3、由于主题中的一个队列会被多个消费组进行消费,为此需要为不同消费组的消费队列分别记录一个下标offset,这样组之间的消费就可以互不干涉
可以用CurrentHashMap来记录每个消费组的消费队列的下标,这样的话可以防止锁的竞争。
这个offset是非常重要的概念,我们在使用消息队列的时候,丢消息的原因大多是由于offset处理不当导致的。

4、如果两个业务都需要消费完整的topic消息,那么他们这两个业务的消费者不能在一个消费组内,需要分开。

5、消费组里面的消费者数量可以水平扩容,是为了增大消费组的消费能力,一个消费组内消息给任意一个消费者消费意义是一样的。

Kafka 的消息模型

Kafka 的消息模型和 RocketMQ 是完全一样的,
唯一的区别是:RocketMQ中的队列 在Kafka 中叫 分区(Partition)

MQ如何保证顺序消费

每个主题包含多个队列|分区,但由于是并行消费,导致一个消费者组在消费时并不保证消息严格有序。
解决办法:在发送端,我们使用比如用户ID/订单ID/账户 ID 作为 Key,采用一致性哈希算法(考虑队列扩容,队列不扩容则之间取模即可)计算出队列编号,保证将同一组消息发送到同一队列上。接着指定一个消费者专门来消费某个队列|分区的数据,这样就能保证消息的顺序消费了。

其实,大部分情况下,我们并不需要全局严格顺序,只要保证局部有序就可以满足要求了。比如,在传递账户流水记录的时候,只要保证每个账户的流水有序就可以了,不同账户之间的流水记录是不需要保证顺序的。

以上的措施虽然能保证消息顺序,但是必须规定一个消费者组内的多个消费者只能串行消费同一队列,无法做到多个消费者并发消费同一个队列,否则会出现消费错乱的问题。
那如果放宽一下限制,不要求严格顺序,只需要保证消息消费不重不漏即可,如何做到单个队列的并行消费呢?
——队列可以维护一个全局的下标,多个消费者线程同时消费队列时,使用CAS可以保证消息消费的不重不漏

如何保证消息不丢失

检测消息丢失的方法

可以利用消息队列的有序性来验证是否有消息丢失。
原理非常简单:在 Producer 端,我们给每个发出的消息附加一个连续递增的序号,然后在 Consumer 端来检查这个序号的连续性。

确保消息可靠传递

知道了检测消息丢失的方法,接下来看一下,整个消息从生产到消费的过程中,哪些地方可能会导致丢消息,以及应该如何避免消息丢失。
下图中三个阶段都可能丢失消息,下面一一分析
在这里插入图片描述
1、 生产阶段
在生产阶段,消息队列通过最常用的请求应答机制,来保证消息的可靠传递
当你的代码调用发消息方法时,消息队列的客户端会把消息发送到 Broker,Broker 收到消息后,会给客户端返回确认响应,表明消息已经收到了

如果返回确认响应失败,会自动重试,如果重发一直失败,有以下几种处理策略:

  1. 无限重试(当然不推荐使用)
  2. 双主题:A主题发送失败,向B主题发送;
  3. 记录错误:反复发送失败的消息,记录到数据库或日志中,后续定时任务或者人工处理。

2、存储阶段
在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。

如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。

  • 如果是单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息写入磁盘后再给 Producer 返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,就不会丢失消息,恢复后还可以继续消费。
  • 如果是 Broker集群,需要将 Broker 集群配置成:至少将消息发送到 2 个以上的节点,再给客户端回复发送确认响应。这样当某个 Broker 宕机时,其他的 Broker 可以替代宕机的 Broker,也不会发生消息丢失。

3、消费阶段
消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递,客户端从 Broker 拉取消息后,执行用户的消费业务逻辑,成功后,才会给 Broker 发送消费确认响应。
如果 Broker 没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会丢失。

死信队列

Broker等待消费者的ack会有一个超时时间,超时之前会阻塞,不让其他的消费者消费该消息,如果因为网络延迟等因素,未收到消费组的ack响应,超时之后就解除锁定,允许其他消费者来拉消息,由于消费位置没变,下次再有消费者来这个队列拉消息,返回的还是上一条消息。
一条消息的消费失败超过一定的次数,消息队列会把这种“坏消息”放到一个特殊死信队列中,避免卡主整个队列消费
当然,使用死信队列之后就不能保证消息的严格顺序了。

总结

在生产阶段,采用请求应答模式,如果应答消息发送失败的话,需要重发消息。

在存储阶段,单机则刷盘,多机则复制,让消息写入到多个副本的磁盘上,来确保消息不会因为某个 Broker 宕机或者磁盘损坏而丢失。

在消费阶段,可以在处理完所有消费业务逻辑之后,再发送消费确认。

如何避免重复消费

如果出现传递失败的情况,为了保证消息不丢失,发送方会执行重试,重试就有可能会产生重复消费

一般有三种传递消息时能够提供的服务质量标准,这三种服务质量从低到高依次是:
1、At most once: 至多一次。
消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。
2、 At least once: 至少一次。
消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。
3、Exactly once:恰好一次。
消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。

如果选择恰好一次或者最多一次,那不会重复消费,但是绝大部分消息队列都选择至少一次,因为选择前两种的性能开销过大,为了保证性能选择至少一次,然后用幂等消费来保证不重复消费

幂等操作解决重复消费问题

幂等操作:其任意多次执行所产生的影响均与一次执行的影响相同。

几种常用的设计幂等操作的方法:

1、利用数据库的唯一索引实现幂等

举例:“将账户 X 的余额加 100 元”,这个+100的操作就不是幂等的,
可以利用数据库的唯一约束实现次操作的幂等
具体:在数据库中建一张转账流水表,这个表有三个字段:转账单 ID、账户 ID 和变更金额,然后给转账单 ID 和账户 ID 这两个字段联合起来创建一个唯一索引,这样对于相同的转账单 ID 和账户 ID,表里至多只能存在一条记录。

基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如Redis 的 SETNX 也实现幂等消费。

2、为更新的数据设置前置条件

给数据变更设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。

用什么作为前置判断条件呢?——版本号
通用的方法是,给你的数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 +1,可以实现幂等更新。

3、记录并检查操作

实现思路:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。

具体的实现方法:在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。

总结:
以上实现幂等的方法,不仅可以用于解决重复消息的问题,也同样适用于在其他场景中来解决重复请求或者重复调用的问题。比如,我们可以将 HTTP 服务设计成幂等的,解决前端或者 APP 重复提交表单数据的问题;也可以将一个微服务设计成幂等的,解决 RPC 框架自动重试导致的重复调用问题。这些方法都是通用的,希望你能做到触类旁通,举一反三。

如何避免消息积压

据我了解,在使用消息队列遇到的问题中,消息积压这个问题,应该是最常遇到的问题了,并且,这个问题还不太好解决。

消息积压的原因:
一定是系统中的某个部分出现了性能问题,来不及处理上游发送的消息,才会导致消息积压。所以需要优化性能来避免消息积压。

优化什么地方呢?
主要是在消息的发送端和接收端
我们的业务代码怎么和消息队列配合,达到一个最佳的性能。

发送端性能优化

发送端业务代码的处理性能,实际上和消息队列的关系不大,因为一般发送端都是先执行自己的业务逻辑,最后再发送消息。
所以如果是发送消息的性能上不去,你需要优先检查一下,是不是发消息之前的业务逻辑耗时太多导致的。

具体如何优化?——并发和批量
对于发送消息的业务逻辑,只需要设置合适的并发和批量大小,就可以达到很好的发送性能。至于到底是选择批量发送还是增加并发,主要取决于发送端程序的业务性质。

  • 一般是在线业务选择增加并发,因为选择批量发送必然会影响服务的时延
  • 离线业务选择批量,因为它不关心时延

消费端性能优化

使用消息队列的时候,大部分的性能问题都出现在消费端,如果消费的速度跟不上发送端生产消息的速度,就会造成消息积压

如果这种性能倒挂的问题只是暂时的,那问题不大,只要消费端的性能恢复之后,超过发送端的性能,那积压的消息是可以逐渐被消化掉的。

要是消费速度一直比生产速度慢,时间长了,整个系统就会出现问题,要么,消息队列的存储被填满无法提供服务,要么消息丢失,这对于整个系统来说都是严重故障。

两点优化方法:
1、在设计系统的时候,就保证消费端的消费性能要高于生产端的发送性能,这样的系统可以健康的持续运行

2、可以通过水平扩容 Consumer和分区的实例数量(注意二者需要同步扩容,只扩容一个是不够的),增加消费端的并发数来提升总体的消费性能。

3、对实时性要求不高的场景,可以将消费端设置成批量消费的方式来提升消费性能,kafka默认就是异步批量消费,其他MQ可以选择批量

真的积压了怎么办

在优化发送端和接收端之后,还是积压了该怎么办呢?

先解决问题,再查监控排查原因

解决问题:
通过消费降级来降低生产速度,关闭一些不重要的业务,减少发送方发送的数据量,最低限度让系统还能正常运转,服务一些重要业务

查看监控来排查原因:

  • 如果监控到发送消息和消费消息的速度和原来都没什么变化,可能是消费失败导致的一条消息反复消费
  • 如果监控到消费变慢了,优先检查一下日志是否有大量的消费错误,如果没有错误的话,可以通过打印堆栈信息,看一下你的消费线程是不是卡在什么地方不动了,比如触发了死锁或者卡在等待某些资源上了。

总结

一、如何预防消息积压?
1、发送端优化,增加批量和线程并发
2、消费端优化,优化业务逻辑代码、水平扩容增加并发并同步扩容分区数量

二、真的挤压怎么处理?
解决问题:
1、消费端扩容;
2、服务降级;
3、异常监控。

排查原因:
1、消息队列内置监控,查看发送端发送消息与消费端消费消息的速度变化
2、查看日志是否有大量的消费错误
3、打印堆栈信息,查看消费线程卡点信息

如何保证高可用:主从

前面讲过,消息队列在收发两端,主要是依靠业务代码,配合请求确认的机制,来保证消息不会丢失的。
而在服务端,一般采用持久化和复制的方式来保证不丢消息。

把消息复制到多个节点上,不仅可以解决丢消息的问题,还可以保证消息服务的高可用。即使某一个节点宕机了,还可以继续使用其他节点来收发消息。所以大部分生产系统,都会把消息队列配置成集群模式,并开启消息复制,来保证系统的高可用和数据可靠性。

下面讲一下 RocketMQ 和 Kafka 是如何实现复制的。

RocketMQ 如何实现复制?

在 RocketMQ 中,复制的基本单位是 Broker,也就是服务端的进程。复制采用的也是主从方式,通常情况下配置成一主一从,也可以支持一主多从。

RocketMQ 提供了两种复制方式,

  • 异步复制,消息先发送到主节点上,就返回“写入成功”,然后消息再异步复制到从节点上。
  • 同步双写,消息同步双写到主从节点上,主从都写成功,才返回“写入成功”。

在 RocketMQ 中,Broker 的主从关系是通过配置固定的,不支持动态切换。如果主节点宕机,生产者就不能再生产消息了,消费者可以自动切换到从节点继续进行消费。这时候,即使有一些消息没有来得及复制到从节点上,这些消息依然躺在主节点的磁盘上,除非是主节点的磁盘坏了,否则等主节点重新恢复服务的时候,这些消息依然可以继续复制到从节点上,也可以继续消费,不会丢消息,消息的顺序也是没有问题的。

从设计上来讲,RocketMQ 的这种主从复制方式,牺牲了可用性,换取了比较好的性能和数据一致性。

那 RocketMQ 又是如何解决可用性的问题的呢?
一对主从节点可用性不行,多来几对儿主从节点不就解决了。RocketMQ 支持把一个主题分布到多对主从节点上去,每对主从节点中承担主题中的一部分队列,如果某个主节点宕机了,会自动切换到其他主节点上继续发消息,这样既解决了可用性的问题,还可以通过水平扩容来提升 Topic 总体的性能。

Kafka 是如何实现复制的?

Kafka 中,复制的基本单位是分区。每个分区的几个副本之间,构成一个小的复制集群,Broker 只是这些分区副本的容器,所以 Kafka 的 Broker 是不分主从的。

Kafka 在写入消息的时候,采用的也是异步复制的方式。消息在写入到主节点之后,并不会马上返回写入成功,而是等待足够多的节点都复制成功后再返回。
在 Kafka 中这个“足够多”是多少呢?
Kafka 的设计哲学是,让用户自己来决定。Kafka 为这个“足够多”创造了一个专有名词:ISR(In Sync Replicas保持数据同步的副本)

Kafka 使用 ZooKeeper 来监控每个分区的多个节点,如果发现某个分区的主节点宕机了,Kafka 会利用 ZooKeeper 来选出一个新的主节点,这样解决了可用性的问题

默认情况下,如果所有的 ISR 节点都宕机了,分区就无法提供服务了。你也可以选择配置成让分区继续提供服务,这样只要有一个节点还活着,就可以提供服务,代价是无法保证数据一致性,会丢消息。

Kafka 的这种高度可配置的复制方式,优点是非常灵活,你可以通过配置这些复制参数,在可用性、性能和一致性这几方面做灵活的取舍,缺点就是学习成本比较高。

总结

RocketMQ 提供的主从模式性能较好,但灵活性和可用性稍差,

Kafka 提供了基于 ISR 的更加灵活可配置的复制方式,用户可以自行配置,在可用性、性能和一致性这几方面根据系统的情况来做取舍。但是,这种灵活的配置方式学习成本较高。

最后,并没有一种完美的复制方案,可以同时能够兼顾高性能、高可用和一致性。你需要根据你实际的业务需求,先做出取舍,然后再去配置消息队列的复制方式。

怎么设计一个MQ?

基本流程:
producer发送给broker存储,broker通知consumer来消费,consumer消费之后回复消费成功,broker删除/备份消息。

利用zk维护订阅关系,实现发布订阅模式、

push还是pull
push模式:主动权由生产者掌握,每生产一条消息就push给消费者
pull模式:主动权由消费者掌握,消费者根据自身情况选择主动pull消息

主流MQ选择pull模式,因为consumer可以按自己的能力去消费。

服务端承载消息堆积的能力
为了满足我们错峰/流控/最终可达等一系列需求,把消息存储下来,然后选择合适的时机给到消费者消费。
这个存储可以做成很多方式。比如存储在内存、磁盘、数据库等等。具体存在那个地方由业务场景决定

  • 对可靠性要求高选择磁盘
  • 对可靠性要求不高,数量又极大的话(如日志),消息直接暂存内存要更合适一些。

然后考虑消息不重不漏、消息积压、高可用。

kafka vs rocketMQ

kafka vs rocketMQ
注册发现:rocketmq用的是namesrv,kafka用的是zookeeper

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值