(一)什么是消息中间件?


消息中间件(MQ)的定义

其实并没有标准定义。消息中间件是随着系统的发展诞生出来的,消息中间件属于分布式系统中一个子系统,诞生于分布式系统.也就是传统的单体架构是不会有消息中间件的.
关注于数据的发送和接收,消息中间件关注的是消息,消息就是对应的数据,利用高效可靠的异步消息传递机制对分布式系统中的其余各个子系统进行集成。

高效:对于消息的处理处理速度快。
可靠:一般消息中间件都会有消息持久化机制和其他的机制确保消息不丢失。
异步:指发送完一个请求,不需要等待返回,随时可以再发送下一个请求,既不需要等待。

一句话总结,我们消息中间件不生产消息,只是消息的搬运工。

1.基础概念 - 图1
一个MQ,交易系统往MQ里面存数据,商品中心也往MQ里面存数据,存完数据以后,最后物流中心这个系统来接收信息,通知平台和数据分析也会从MQ里面获取交易系统和商品系统存放的数据.

(二)为什么要用消息中间件?


假设一个电商交易的场景,用户下单之后调用库存系统减库存,然后需要调用物流系统进行发货,如果交易、库存、物流是属于一个系统的,那么就是接口调用。但是随着系统的发展,各个模块越来越庞大、业务逻辑越来越复杂,必然是要做服务化和业务拆分的。这个时候就需要考虑这些系统之间如何交互,一般的处理方式就是 RPC(Remote Procedure Call)(具体实现 dubbo,SpringCloud)。系统继续发展,可能一笔交易后续需要调用几十个接口来执行业务,比如还有风控系统、短信服务等等。这个时候就需要消息中间件登场来解决问题了。

所以消息中间件主要解决分布式系统之间消息的传递,同时为分布式系统中其他子系统提供了松耦合的架构,同时还有以下好处:

低耦合

低耦合,不管是程序还是模块之间,使用消息中间件进行间接通信。

异步通信能力

异步通信能力,使得子系统之间得以充分执行自己的逻辑而无需等待。

缓冲能力

缓冲能力,消息中间件像是一个巨大的蓄水池,将高峰期大量的请求存储下来慢慢交给后台进行处理,对于秒杀业务来说尤为重要。

伸缩性

并发量大了,可以增加MQ集群,比如增加两台MQ服务器,来解决大量的并发.
假如业务量下降的话我也可以给这些机器拆掉.

伸缩性,是指通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。就像弹簧一样挂东西一样,用户多,伸一点,用户少,浅一点,啊,不对,缩一点。是伸缩,不是深浅。衡量架构是否高伸缩性的主要标准就是是否可用多台服务器构建集群,是否容易向集群中添加新的服务器。加入新的服务器后是否可以提供和原来服务器无差别的服务。集群中可容纳的总的服务器数量是否有限制。

扩展性

扩展性,主要标准就是在网站增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或者很少改动既有业务功能就可以上线新产品。比如用户购买电影票的应用,现在我们要增加一个功能,用户买了铁血战士的票后,随机抽取用户送异形的限量周边。怎么做到不改动用户购票功能的基础上增加这个功能。熟悉设计模式的同学,应该很眼熟,这是设计模式中的开闭原则(对扩展开放,对修改关闭)在架构层面的一个原则。

(三)和RPC有何区别?


RPC(远程过程调用)和消息中间件的场景的差异很大程度上在于就是“依赖性”和“同步性”。
RPC是同步性的,而消息中间件是异步性的.

依赖性

RPC是依赖性的,A服务器调用必定要调用B服务器的某个方法,这样A服务器就会和B服务器有依赖性了.
1.基础概念 - 图2
对于消息中间件来说可能就没有依赖了,交易系统我只管发消息到MQ,这样交易系统和物流中心.通知平台.数据分析 三个是没有依赖关系的,使用消息中间件其实就是一个松耦合的架构.
1.基础概念 - 图3
比如短信通知服务并不是事交易环节必须的,并不影响下单流程,不是强依赖,所以交易系统不应该依赖短信服务。如果是 RPC 调用,短信通知服务挂了,整个业务就挂了,这个就是依赖性导致的,而消息中间件则没有这个依赖性。
消息中间件出现以后对于交易场景可能是调用库存中心等强依赖系统执行业务,之后发布一条消息(这条消息存储于消息中间件中)。像是短信通知服务、数据统计服务等等都是依赖于消息中间件去消费这条消息来完成自己的业务逻辑。

同步性


RPC 方式是典型的同步方式,让远程调用像本地调用。消息中间件方式属于异步方式。
1.基础概念 - 图41.基础概念 - 图5
相同点:都是分布式下面的通信方式。

(四)使用场景

异步处理

场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种: 1.串行的方式;2.并行方式。

串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。
1.基础概念 - 图6
并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。
假设三个业务节点每个使用 50 毫秒钟,不考虑网络等其他开销,则串行方式的时间是 150 毫秒,并行的时间可能是 100 毫秒。
1.基础概念 - 图7

小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?
引入消息队列,将不是必须的业务逻辑,异步处理。

1.基础概念 - 图8
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是 50 毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是 50 毫秒。因此架构改变后,系统的吞吐量提高到每秒 20 QPS。比串行提高了 3 倍,比并行提高了两倍。

应用解耦


场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。
传统模式的缺点:
1) 假如库存系统无法访问,则订单减库存将失败,从而导致订单失败;
2) 订单系统与库存系统耦合;

1.基础概念 - 图9
如何解决以上问题呢?引入应用消息队列后的方案

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。
1.基础概念 - 图10

假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。

流量削峰


流量削峰也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列:可以控制活动的人数;可以缓解短时间内高流量压垮应用。
1.基础概念 - 图11
正常情况下数据库肯定会挂掉,然后会返回给用户错误数据,
使用消息中间件之后数据不会丢失.
1.基础概念 - 图12

用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面;秒杀业务根据消息队列中的请求信息,再做后续处理。

日志处理


日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。架构简化如下:
1.基础概念 - 图13
日志采集客户端,负责日志数据采集,定时写入 Kafka 队列:Kafka 消息队列,负责日志数据的接收,存储和转发;日志处理应用:订阅并消费 kafka队列中的日志数据;

消息通讯

消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。
点对点通讯:客户端 A 和客户端 B 使用同一队列,进行消息通讯。
聊天室通讯:客户端 A,客户端 B,客户端 N 订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

(五)常见的消息中间件比较


1.基础概念 - 图14
消息持久化就是消息可以写入到磁盘里面,但是消息持久化是有代价的,开启消息持久化带来的是一个性能的损耗,持久化做的越好,你的性能就越差.

如果一般的业务系统要引入 MQ,怎么选型:
用户访问量在 ActiveMQ 的可承受范围内,而且确实主要是基于解耦和异步来用的,可以考虑 ActiveMQ,也比较贴近 Java 工程师的使用习惯,但是 ActiveMQ 现在停止维护了,同时 ActiveMQ 并发不高,所以业务量一定的情况下可以考虑使用。

RabbitMQ 作为一个纯正血统的消息中间件,有着高级消息协议 AMQP 的完美结合,在消息中间件中地位无可取代,但是 erlang 语言阻止了我们去深入研究和掌控,对公司而言,底层技术无法控制,但是确实是开源的,有比较稳定的支持,活跃度也高。

对自己公司技术实力有绝对自信的,可以用 RocketMQ,但是 RocketMQ 诞生比较晚,并且更新迭代很快,这个意味着在使用过程中有可能会遇到很多坑,所以如果你们公司 Java 技术不是很强,不推荐使用 。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用ActiveMQ、RabbitMQ是不错的选择;大型公司,基础架构研发实力较强,用RocketMQ是很好的选择

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,几乎是全世界这个领域的事实性规范。

从性能上来看,使用文件系统的消息中间件(kafka、rokcetMq)性能是最好的,所以基于文件系统存储的消息中间件是发展趋势。(从存储方式和效率来看 文件系统>KV 存储>关系型数据库)

(六)使用消息中间件缺点

  1. 复杂性
    2. 数据丢失
    3. 数据重复
    4. 一致性问题

    (七)消息的幂等性(如何保证消息不被重复消费)


    MQ不保证一个消息可能被重复发多次,如果接到重复的消息,可能程序就会和预期的结果不一样,

    保证消息被消费一次和消费多次结果是一样的,比如说防止重复扣款问题. 在生产者发送消息的时候生成唯一的id,在消息的报文体里面还需要指定业务id,比如说交易流水号等等,通过这两个id可以在消费者里面,比如说建一张表,来区分重复的id,来避免消息的重复消费.


    具体流程是,生产者生产一条数据到MQ里面,然后消费者从MQ拉取这条消息之后,就往数据库(或者Redis)里面写入一条消息.

    假设此时有两条重复的数据,当消费者拿到第一条数据的时候,消费者先判断是否在数据库里面已经有这个数据了,如果没有就往数据库里面写一条记录,当消费者拿到第二条数据的时候,先判断数据库里面是否有这个数据了(也可以用数据库的唯一索引来保证数据不会重复多条),此时发现有,那么这条数据就别处理了,,放着别管了.


    总之思路就是每次消费完一个MQ的消息就往一个地方(可以是MySQL可以是Redis)插入一条记录,下次再消费MQ的时候,先判断之前有没有消费过.

    (八)消息丢失问题


    生产者以为自己发出去了,可能消费者就根本没有接收到,在这过程中可能就得了,也有可能MQ已经接收到了消息,但是消费者没有消费到.

    一旦丢失,数据就少一条.就会导致很严重的问题,

    1.存在消息丢失的情况三个环节

  2. 生产者往MQ写消息(写消息的过程中,消息都没到MQ,在网络传输过程中丢失,或者消息到了MQ,但是MQ内部出错了,没有保存下来)
    2. MQ内部把消息保存起来(MQ接收到消息之后先暂存到自己内部内存里,结果消费者还没来得及消费,MQ自己出错挂掉了,就导致暂存到内存里面的数据就给搞丢失了)
    3. 消费者给消息从MQ里面拉过来(消费者消费了消息,但是还没来得及处理消费者自己就挂掉了,但是MQ以为消费者已经处理完了.)

    上面三个环节都可能导致数据丢失问题.

    2.第一个环节解决方案

    confirm机制,先给channel设置confirm的模式,然后发送一个消息,发送完消息之后就不用管了.MQ如果接收到了这条消息的话就会回调生产者本地的接口,通知说这条消息我已经收到了,如果MQ在接收消息报错了,就会回调接口告诉程序员接收失败了,你可以再次重发
    confirm机制是异步机制,发送消息不会阻塞,可以直接再发送下一条消息.

    3.第二个环节的解决方案

    让MQ给消息持久化到磁盘上面去,给queue设置持久化,这样MQ在接收到消息的时候就能保证消息持久化到磁盘上,但是不持久化Queue里面的数据,还需要再设置deliveryMode设置为2,就是将消息持久化到磁盘上去,这样才能保证第二环节消息不被完整丢失.这样即使挂掉了,MQ重启也可以通过本地磁盘的数据来恢复到Queue里面.

    当然还有一点点的风险,就是内存里面的数据还没写入到磁盘里面就挂了,此时内存里面的数据就丢失了,不过这个风险几乎是很小很小了.

    4.第三个环节的解决方案

    如果消费者给消息搞丢了,只能说你消费者开了autoAck机制,autoAck是消费到了数据之后消费者会自动通知MQ说我已经消费完这条消息了,但是这样是有一个问题的,如果我消费的一条消息,然后还没处理完,此时消费者就自动autoAck了(告诉MQ这个消息我已经处理完了),此时不巧,消费者宕机了,此时这条消息就丢失了.

    解决办法就是给autoAck关掉,然后在代码逻辑里面每次处理完业务之后再手动发送ack,如果还没处理完就宕机了,此时MQ是没收到ACK消息的,然后MQ就会给这个消息重新分配给其它的消费者去处理.

    (九)保证消息消费的顺序性



    比如在生产环境使用自己研制的MySQL binlog同步系统,如果记录顺序错了,那就错了.比如说本来是增加-修改-删除,结果你换了个顺序执行成删除-修改-增加,那么问题就大了.

    一个生产者给一个消息A写到队列里面去,然后只有一个消费者A会消费到,然后生产者又给一个消息B写到队列里面去,然后消息B可能被另外一个消费者B消费了,可能会出现后来的消费者B处理比较快,在前面的消费者A处理完之前先处理完了,结果就会导致消息B插入到消息A前面了.

    RabbitMQ如何保证消息的顺序性,
    把你需要保证顺序的消息一定发到一个Queue里面,然后只有消费者监听这一个Queue,然后这多个消息就依次被一个消费者消费到,消费者会严格按照顺序去处理,这样就能保证顺序性.

    (十)如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?


    出现这个问题基本就是消费者出问题了,不消费队列里面的消息了,然后就会积压消息.

    如果消费者故障问题处理完了,现在能正常消费消息了,思路就是新建个交换器,然后改原来的消费者代码,让原来的消费者代码把消息转发到新的交换器上,然后再给公司申请服务器去多搭建几个消费者去消费这个新的交换器里面的消息.

    正常情况下生产环境的不允许设计过期时间的,会导致MQ很坑.

    消息积压处理办法:临时紧急扩容:

    先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。
    新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
    然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
    接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
    等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
    MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

    mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。