本次分享的消息队列面试题
1)如何保证消息队列的高可用?
2)如何保证消息不被重复消费(如何保证消息消费时的幂等性)?

一、如何保证消息队列的高可用性

在上一讲(1中,知道虽然给我们的项目提供了解耦、异步、削峰的作用,但是它也带来了缺点,比如如何保证消息队列的高可用?万一消息队列宕机了,那么整个系统将无法运行。


1、RabbitMQ的高可用性
首先,RabbitMQ是基于主从来做高可用的,一般RabbitMQ有三种模式,单机模式、普通集群模式、镜像集群模式

单机模式

单机模式就是最简单的,适用于我们平时练习、学习MQ,生产环境下不采取这种模式。

普通集群模式

在多台机器上启动多个rabbitmq实例,每个机器启动一个。但是创建的queue,只会放在一个rabbtimq实例上,每个实例都同步queue的元数据。当用户需要消费的时候,如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。
这种方式确实很麻烦,也不怎么好,没做到所谓的分布式,就是个普通集群。因为这导致要么消费者每次随机连接一个实例然后去拉取数据,要么固定连接那个queue所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。而且如果放queue的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让rabbitmq落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个queue拉取数据。这种方式没有高可用性可言了,这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个queue的读写操作。

镜像集群模式


这种模式,才能算是rabbitmq的高可用模式,跟普通集群模式不一样的是,我们创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次写消息到queue的时候,都会自动把消息同步到多个实例的queue里。这样的话,好处在于,任何一个机器宕机了,其他机器还可以使用。坏处在于,第一,性能开销太大,消息同步所有机器,导致网络带宽压力和消耗很重。第二,没有扩展性,如果某个queue负载很重,即使加了机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展queue。

如何开启这个镜像集群模式?简单说一下,rabbitmq有很好的管理控制台,可以在后台新增一个策略,这个策略是镜像集群模式的策略,可以要求数据同步到所有的节点,也可以要求就同步到指定数量的节点,然后再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去。

2、kafka的高可用性


kafka由多个broker组成,每个broker是一个节点;创建一个topic,这个topic可以划分为多个partition,每个partition可以存在于不同的broker上,每个partition存放一部分数据。kafka是天然的分布式消息队列,一个topic的数据,是分散放在多个机器上的,每个机器就放一部分数据。
kafka 0.8以前,是没有HA机制的,任何一个broker宕机了,这个broker上的partition就废了,没法写也没法读,没有什么高可用性可言。
kafka 0.8以后,提供了HA机制,就是replica副本机制。每个partition的数据都会同步到其他机器上,形成自己的多个副本。然后所有副本会选举一个leader出来,生产和消费都跟这个leader打交道,然后其他副本就是follower。写的时候,leader会负责把数据同步到所有follower上去,读的时候就直接读leader上数据即可。
只能读写leader的原因在于,如果可以随意读写每个follower,那么就要关心数据一致性的问题,这样就会导致系统复杂度太高,很容易出问题。
kafka会均匀的将一个partition的所有副本分布在不同的机器上,从而可以提高容错性。


这样子kafka就有所谓的高可用性了,因为如果某个broker宕机了,这个broker上面的partition在其他机器上都有副本的,如果这上面有某个partition的leader,那么此时会重新选举一个新的leader出来,然后继续读写那个新的leader即可。
当在写数据的时候,生产者就写给leader,然后leader将数据落地写本地磁盘,接着其他follower自己主动从leader来pull数据。一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。(这只是其中一种模式,还可以适当调整这个行为)消费的时候,只会从leader去读,但是只有一个消息已经被所有follower都同步成功返回ack的时候,这个消息才会被消费者读到。

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

    首先先说一说,有哪些重复消费的情况发生
    
    那么重复消费发生了,如何保证幂等性?
    假设你有个系统,消费一条往数据库里插入一条,要是一个消息重复两次,就会发生向数据库中插入了两条记录的情况发生,这数据不就发生错了。但是如果消费到第二次的时候,自己判断一下是否已经消费过了,如果消费了就直接扔了,一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。
    幂等性,通俗点说,就一个数据,或者一个请求,重复来了多次请求,但是必须确保对应的数据是不会改变,不能出错。
    怎么保证消息队列消费的幂等性,其实还是得结合业务来思考,这里给几个思路:
    1)比如消费者要去数据库写数据,在写数据前,先根据主键查一下,如果这数据已经存在了,就别插再插入一条数据,可以进行update操作
    2)比如是写redis,因为每次都是写进set集合中,而set集合又有天然幂等性
    3)比如不是上面两个场景,那做的稍微复杂一点,需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后消费者消费时,先根据这个id去redis里查一下之前是否消费过了,如果没有消费过,就处理,然后将这个id写进redis。如果消费过了,那就别处理,保证别重复处理相同的消息即可。
    4)比如基于数据库的唯一键来保证重复数据不会重复插入多条