消息队列的常见面试题,看完就懂咯!

178 阅读5分钟

在消息队列当中的多条消息如果都是对同一个数据来进行操作,那么这些操作就具有前后的关系,必须需要按照前后的这种顺序来执行,不然就会出错。

出现顺序错乱场景

有一个queue,这时候有多个consumer去进行消费,这样就会造成了顺序出现错误,因为每一个consumer执行的时间是不一样的,这样就无法保证了先读到的消息一定是它先完成的操作,这样就导致了没有按照顺序来执行消息,造成了数据顺序的错误。

图片

这种情况是由于不一样的消费者再处理的数据的速度是不一样的,比如这里的消费者2就先处理完了,然后就先往数据库来进行操作了。起初想的是这三个操作有序执行的,结果是顺序不一样,造成了数据的混乱。

多线程导致的顺序错乱

图片

这种情况是消费者里使用了多线程来进行消息消费,由于是不一样的消费者,所以在处理数据的速度也是不一样的,线程2先处理好的,所以线程2先对数据库进行了操作,造成了数据混乱。

什么是顺序消息

消息的生产和消费顺序一致

全局顺序:  topic下面全部消息都要有序(少用),性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的 场景,并行度成为消息系统的瓶颈, 吞吐量不够。

使用场景:  在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO 的方式进行发布和消费

局部顺序:  只要保证一组消息被顺序消费即可,性能要求高。

使用场景:  电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费

下面是用RocketMQ举例(用kafka或rabbitmq类似)

一个topic下面有多个queue

图片

顺序发布:  对于指定的一个 Topic,客户端将按照一定的先后顺序发送消息。

举例:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,

根据MessageQueueSelector里面自定义策略,根据同个业务id放置到同个queue里面,如订单号取模运算再放到selector中,同一个模的值都会投递到同一条queue

   public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            //如果是订单号是字符串,则进行hash,得到一个hash值
          Long id = (Long) arg;
          long index = id % mqs.size();
          return mqs.get((int)index);
   }

顺序消费:  对于指定的一个 Topic,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。

举例:消费端要在保证消费同个topic里的同个队列,不应该用MessageListenerConcurrently,应该使用MessageListenerOrderly,自带单线程消费消息,不能再Consumer端再使用多线程去消费,消费端分配到的queue数量是固定的,集群消费会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。

图片

注意:

  • 顺序消息暂不支持广播模式
  • 顺序消息不支持异步发送方式,否则将无法严格保证顺序
  • 不能在Consumer端再使用多线程去消费

消息队列怎么避免重复消费

幂等性:一个请求,不管重复来多少次,结果是不会改变的。

RabbitMQ、RocketMQ、Kafka等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性

与消息队列相关的三种传递标准:

  1. At most once:至多一次。消息传递时,最多会被送达一次,没有什么可靠性,允许消息丢失。
  2. At least once:至少一次。消息传递时,至少会被送打一次,保证消息可靠性,但存在多次消费的可能。
  3. Exactly once:恰好一次。消息传递时,只会被送达一次,不允许丢失也不允许重复。

方式一: Redis的setNX() , 做消息id去重 java版本目前不支持设置过期时间

//Redis中操作,判断是否已经操作过 TODO
boolean flag =  jedis.setNX(key);
if(flag){
        //消费
}else{
        //忽略,重复消费
}

方式二: redis的 Incr 原子操作:key自增,大于0 返回值大于0则说明消费过,(key可以是消息的md5取值, 或者如果消息id设计合理直接用id做key)

int num =  jedis.incr(key);
if(num == 1){
    //消费
}else{
    //忽略,重复消费
}

方式三:数据库去重表

设计一个去重表,某个字段使用Message的key做唯一索引,因为存在唯一索引,所以重复消费会失败

CREATE TABLE message_record ( id int(11) unsigned NOT NULL AUTO_INCREMENT, 
                             key varchar(128DEFAULT NULL, 
                             create_time datetime DEFAULT NULLPRIMARY KEY (id), 
                             UNIQUE KEY key (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

消息发生大量堆积应该怎么处理

需求分析:  消息堆积了10小时,有几千万条消息待处理,现在怎么办?修复consumer, 然后慢慢消费?也需要几小时才可以消费完成,新的消息怎么办?

核心思想:  紧急临时扩容,更快的速度去消费数据

(1) 修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停

(2) 临时topic队列扩容,并提高消费者能力,但是如果增加Consumer数量,但是堆积的topic里面的messag