中间件——消息MQ

一、前言

队列是一种先进先出的数据结构。

消息队列简称MQ(Message Queue),可以简单理解为:把要传输的数据放在队列中。

为什么使用MQ:主要是:解耦、异步、削峰

  • 解耦:
    • 问题: 假设一个系统或者模块A,产生一条数据,而这条数据被多个系统B、C、D需要,如果直接在A上进行操作,则耦合度太高了。你想想,假入说B不需要了,你要修改A不发数据给B;或者有个新E说也需要,你要修改A发数据给E;如果发失败了,那还得处理,等等这些情况。
    • 解决: 使用消息队列,将系统A的数据写到消息队列中,系统BCD从消息队列中拿数据。此时:
      • 系统A只负责把数据写到队列中,不关心谁想要或不想要这个数据(消息)。
      • B不想要或者E想要,都跟系统A无关,系统A一点代码都不用改。
      • 系统B即便挂了或者请求超时,都跟系统A无关,只跟消息队列有关。
  • 异步:
    • 问题: 假设 A 系统接收一个请求,需要在自己本地写库,用时3ms;还需要在 BCD 三个系统写库,分别要 300ms、450ms、200ms;最终请求总延时是 3 + 300 + 450 + 200 = 953ms。但是A系统处理的是主要业务,BCD主要是收集信息等次要任务。
    • 解决: 使用消息队列,将系统A的数据写到消息队列中,系统BCD从消息队列中拿数据异步处理。此时:
      • 系统A执行完了以后,将数据写到消息队列中,然后就直接返回了,用时3ms。
      • 系统BCD异步处理,用户也不关心这个流程。
      • 提高了用户体验和吞吐量。
  • 削峰:
    • 问题: 假如我们每个月要搞一次大促,大促期间的并发可能会很高的,比如每秒3000个请求。假设我们现在有两台机器处理请求,并且每台机器只能每次处理1000个请求。那多出来的1000个请求,可能就把我们整个系统给搞崩了。
    • 解决: 使用消息队列,把请求放在消息队列中。此时:
      • 系统根据自己的能够处理的请求数去消息队列中拿数据,这样即便有每秒有8000个请求,那只是把请求放在消息队列中,去拿消息队列的消息由系统自己去控制,这样就不会把整个系统给搞崩。
  • 日志处理 - 解决大量日志传输。
  • 消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

使用消息队列有什么问题

  • 高可用问题(系统可用性降低)
    • 如果是单机的消息队列,万一这台机器挂了,那我们整个系统几乎就是不可用了。系统可用性会降低。
    • 所以要提高可用性,要做集群/分布式就必然希望该消息队列能够提供现成的支持,而不是自己写代码手动去实现。
  • 数据丢失问题(数据一致性问题)
    • A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
    • 我们将数据写到消息队列上,系统B和C还没来得及取消息队列的数据,就挂掉了。如果没有做任何的措施,我们的数据就丢了。
  • 系统复杂度提高
    • 消费者怎么得到消息队列的数据?
      • 生产者将数据放到消息队列中,消息队列有数据了,主动叫消费者去拿(俗称push)。
      • 消费者不断去轮训消息队列,看看有没有新的数据,如果有就消费(俗称pull)。
    • 消息重复消费了怎么办啊?
    • 我想保证消息是绝对有顺序的怎么做?
    • 一致性问题?
    • 如何保证消息可靠性传输?

明明JDK已经有不少的队列实现了,我们还需要消息队列中间件呢?

其实很简单,JDK实现的队列种类虽然有很多种,但是都是简单的内存队列,远不如中间件提供的好用。

https://www.zhihu.com/question/54152397?sort=created

二、消息中间件的选择

  • 1、ActiveMQ
    • 老牌的消息中间件,但是没经过大规模吞吐量场景的验证,社区也不是很活跃
    • 传统企业用ActiveMQ做异步调用和系统解耦。
    • springboot集成支持。
  • 2、RabbitMQ
    • 可以支撑高并发、高吞吐、性能很高,同时有非常完善便捷的后台管理界面可以使用。
    • 另外,他还支持集群化、高可用部署架构、消息高可靠支持,功能较为完善。
    • 开源社区很活跃,较高频率的迭代版本,来修复发现的bug以及进行各种优化。
    • 缺点是它自身是基于erlang语言开发的,所以导致较为难以分析里面的源码,也较难进行深层次的源码定制和改造。
    • 目前国内各大中小型互联网公司使用较多。
  • 3、RocketMQ
    • 阿里开源的,经过阿里的生产环境的超高并发、高吞吐的考验,性能卓越,同时还支持分布式事务等特殊场景。
    • 基于Java语言开发的,适合深入阅读源码,有需要可以站在源码层面解决线上生产问题,包括源码的二次开发和改造。
    • 目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高
  • 4、Kafka
    • Kafka提供的消息中间件的功能相对上述几款MQ中间件要少很多。
    • 优势在于专为超高吞吐量的实时日志采集、实时数据同步、实时数据计算等场景来设计。
    • 因此Kafka在大数据领域中配合实时计算技术(比如Spark Streaming、Storm、Flink)和日志采集等场景使用的较多。但是在传统的MQ中间件使用场景中较少采用。
    • 业内标准,社区活跃度很高。

三、MQ 常见问题与RabbitMQ

  • 1、MQ 的常见问题:
    • 消息的顺序问题
      • 消息有序指的是可以按照消息的发送顺序来消费。
      • 假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如何保证 M1 先于 M2 被消费?
      • 保证生产者 - MQServer - 消费者是一对一对一的关系
        • 会造成吞吐量下降
        • 只要消费端出现问题,就会导致整个处理流程阻塞
        • 队列无序并不意味着消息无序 所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系统,是一种更合理的方式。
    • 消息的重复问题(幂等性)
      • kafka实际上有个offset的概念,就是每个消息写进去,都有一个offset,代表他的序号,然后consumer消费了数据之后,每隔一段时间,会把自己消费过的消息的offset提交一下,代表我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的offset来继续消费吧。
      • 如果遇到意外情况了,导致系统重启,这会导致consumer有些消息处理了,但是没来得及提交offset。重启之后,少数消息会再次消费一次。假设你有个系统,消费一条往数据库里插入一条,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?
      • 怎么保证消息队列消费的幂等性?
        • 比如插库,你要是消费到第二次的时候,自己判断一下已经消费过了,直接不要或者update即可。
        • 比如你是写redis,那没问题了,反正每次都是set,天然幂等性。
        • 让生产者发送每条数据的时候,里面加一个全局唯一的id(或者有数据库的唯一键),类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去查一下,之前消费过吗?如果没有消费过,你就处理。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
        • 幂等性,通俗点,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
  • 2、RabbitMQ
    RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件
  • 3、rabbitmq 的使用场景
    • 服务间异步通信
    • 顺序消费
    • 定时任务
    • 请求削峰
  • 4、RabbitMQ基本概念
    • Broker: 简单来说就是消息队列服务器实体
    • Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
    • Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
    • Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
    • Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
    • VHost: vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。
      • 其内部均含有独立的 queue、exchange 和 binding 等。
      • 最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。
      • 从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。
    • Producer: 消息生产者,就是投递消息的程序
    • Consumer: 消息消费者,就是接受消息的程序
    • Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
    • 由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。
  • 5、RabbitMQ的工作模式
    • simple模式(最简单的收发模式)
      • 消息产生消息,将消息放入队列。
      • 消息的消费者(consumer) 监听消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除。
      • 隐患:
        • 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失。
        • 以设置成手动的ack,但如果设置成手动ack,处理完后要及时发送ack消息给队列,否则会造成内存溢出。
    • work工作模式(资源的竞争)
      • 消息产生者将消息放入队列,消费者可以有多个。
      • 消费者1、消费者2同时监听同一个队列,竞争当前的消息队列内容,谁先拿到谁负责消费消息。
      • 隐患:
        • 高并发情况下,默认会产生某一个消息被多个消费者共同使用。
        • 设置一个开关(syncronize) 保证一条消息只能被一个消费者使用。
    • publish/subscribe发布订阅(共享资源)
      • 每个消费者监听自己的队列。
      • 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。
    • routing路由模式
      • 生产者将消息发送到direct交换器。
      • 在绑定队列和交换器的时候有一个路由key,生产者发送的消息会指定一个路由key,那么消息只会发送到相应key相同的队列,接着监听该队列的消费者消费信息。
      • 缺陷:路由表必须明确,不能匹配,比如模糊匹配,可以通过topic解决。没有匹配的消息将被丢弃。
    • topic 主题模式(路由模式的一种)
      • 消息产生者产生消息,把消息交给交换机。
      • 交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费。
      • 星号井号代表通配符,星号代表一个单词,井号代表多个单词。
  • 6、如何保证RabbitMQ消息的顺序性
    • 拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;
    • 或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
  • 7、消息如何分发
    若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。通过路由可实现多消费的功能
  • 8、消息怎么路由
    • 消息提供方->路由->一至多个队列
    • 消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
    • 通过队列路由键,可以把队列绑定到交换器上。
    • 消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则);常用的交换器主要分为一下三种:
      • fanout:如果交换器收到消息,将会广播到所有绑定的队列上
      • direct:如果路由键完全匹配,消息就被投递到相应的队列
      • topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符
  • 9、消息基于什么传输
    由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。
  • 10、如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?
    • 发送方确认模式
      • 将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。
      • 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。
      • 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。
      • 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
    • 接收方确认机制
      • 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
      • 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性。
        • 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
        • 如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。
  • 11、如何保证RabbitMQ消息的可靠传输
    • 消息不可靠的情况可能是消息丢失,劫持等原因;
    • 丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;
    • 生产者丢失消息:提供transaction和confirm模式来确保生产者不丢消息。
      • transaction机制:发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降。
      • confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后;rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
    • 消息队列丢数据:消息持久化。
      • 处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。
      • 这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
      • 如何持久化:
        • 将queue的持久化标识durable设置为true,则代表是一个持久的队列
        • 发送消息的时候将deliveryMode=2
    • 消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!
      • 消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息。
      • 解决方案:处理消息成功后,手动回复确认消息。
  • 12、为什么不应该对所有的 message 都使用持久化机制
    • 必然导致性能的下降,因为写磁盘比写 RAM 慢的多,message 的吞吐量可能有 10 倍的差距。
    • message 的持久化机制用在 RabbitMQ 的内置 cluster 方案时会出现“坑爹”问题。矛盾点在于,若 message 设置了 persistent 属性,但 queue 未设置 durable 属性,那么当该 queue 的 owner node 出现异常后,在未重建该 queue 前,发往该 queue 的 message 将被 blackholed ;若 message 设置了 persistent 属性,同时 queue 也设置了 durable 属性,那么当 queue 的 owner node 异常且无法重启的情况下,则该 queue 无法在其他 node 上重建,只能等待其 owner node 重启后,才能恢复该 queue 的使用,而在这段时间内发送给该 queue 的 message 将被 blackholed 。
    • 所以,是否要对 message 进行持久化,需要综合考虑性能需要,以及可能遇到的问题。若想达到 100,000 条/秒以上的消息吞吐量(单 RabbitMQ 服务器),则要么使用其他的方式来确保 message 的可靠 delivery ,要么使用非常快速的存储系统以支持全持久化(例如使用 SSD)。另外一种处理原则是:仅对关键消息作持久化处理(根据业务重要程度),且应该保证关键消息的量不会导致性能瓶颈。
  • 13、如何保证高可用的?RabbitMQ集群
    RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的。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 的完整数据。
  • 14、如何解决消息队列的延时以及过期失效问题
    假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。
    如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。
    批量重导就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。
  • 15、有几百万消息持续积压几小时怎么解决
    临时紧急扩容:
    先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。
    新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
    然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
    接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
    等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
  • 16、消息队列满了以后该怎么处理
    扩容或者丢掉再补充
  • 17、实际应用MQ思路
    • 支持可伸缩性:分布式的
    • 数据落地磁盘:顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的
    • 可用性:多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
    • 支持数据 0 丢失:代表生产者把消息发送到服务端,服务端的ISR列表里所有的 replica都写入成功以后,才会返回成功响应给生产者。并且还需要保证ISR列表里面是1个副本以上。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值