面试官居然问我用没用过消息队列?我反手说没有用过,然后就回家等offer了...

面试官:用过消息队列吗?谈谈你对消息队列的理解.

我:没有用过....

面试官:哦~~,你回去等消息吧,,,

我:别介啊,我刚刚没准备好,再给我一次机会啊。

在前期的时候,公司处于萌芽状态,业务体积量很小,直接用单机就可以解决了。但是公司因为我的加入,变得越来越庞大,我们程序就采用微服务的设计思想,分布式的部署方式,把很多的服务进行了拆分,同时随着业务体量的增加以及业务场景越来越复杂,单机的技术栈和中间件已经不够用了,比如在一些秒杀活动和抢票活动等高频业务中,服务扛不住大量的QPS,所以最后我们公司决定引入消息队列来优化该类问题。

面试官:不错~~那你说说消息队列的应用场景?

我:该用的时候就用呗,我看到别人用了我就用。。。

面试官:哦~~那你回去。。。

我:别啊,我就皮一下嘛。

使用消息队列的场景主要有3个,分别是异步、解耦、削峰

异步:

公司刚开始的时候,还没有那么多的业务需求,但随着公司的不断发展,业务需求也随之不断增加,比如一个简单的下单系统,起初业务非常简单,用户下单了付完钱了就完事儿了,但刚开始也说了,公司大了,业务需求多了,老板就开始压榨我们这些打工人了...

在阳光明媚的一天,我们老板看见我们在五黑打王者荣耀,我们本来还想着打完这一局就拉他一起玩,没想到他看我们打完了,就笑嘻嘻的走了过来,叫我给这个下单系统再加一个优惠券系统,我也觉得问题不大,一天就搞定了。

第二天,开小差又被老板发现,又让我再加一个积分系统,我觉得也还行,一天又搞定了。

第三天,还是原来的配方,又加个短信系统通知用户下单成功,我也忍了....

第四天,老板笑呵呵的走了过来,老子反手起身说:“你大爷的,我不干了”,说完才发现老板手里拿着一塌子毛爷爷...然后老板就含泪拿着我的奖金把我开了。

试想一下,我这才加了几个业务就不耐烦了,而且在实际的下单系统中(一些主流电商业务),涉及的系统数量以十为单位。但是服务器处理每个系统都需要一定的时间,可能一个系统只需要100ms,但是如果涉及几十个系统,累计的时间可能达到数秒或者数十秒,这非常影响用户的体验感,用户想我结个账还要花费我几十秒时间,心里不免来气,所以该电商也很难有回头客,不过要是都像贫刀刀那样便宜,每次都让人忍不住刀一刀,还是会回头的...

公司为了不打价格战,那就只能从系统优化方面下手了。

我们发现上面的一些流程其实可以同时做,用户支付成功后,优惠券系统与积分系统可以同时操作的,对于短信这些,又不会影响用户体验,晚个几秒发送问题也不大的。这时候异步就派上用场了,这样子用户很快就能完成下单的整体流程,100ms就可以了,给了用户很好的体验,毕竟顾客是上帝,让顾客经常来光顾,这不香吗?

解耦:

比如系统A为支付系统,一开始用户支付完调用日志记录系统B记录就完了,后来内容越来越多,支付完成要调用加积分系统C、短信通知系统D、优惠券系统E等等…

可以看到, A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条支付成功的数据,很多系统接口都需要 A 系统调用把支付成功的数据发送过去。A 系统程序员要时刻考虑这些问题:

  • 其它系统如果有一个挂了该怎么办?
  • 经常加业务,每次都需要重新部署系统,会不会太麻烦了?

那如果引入 MQ,A 系统产生一条数据,发送到 MQ 里面去,每个子系统加上对消息队列中支付成功消息的订阅,持续监听就可以了,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。

在这里插入图片描述

 这样下来,A系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况,我只负责把支付成功的信息放到MQ里就行了,至于能否正常加积分、能否正常短信通知,管我鸟事!(甩锅就完事了)~~可见,通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。

面试官:那你的流程走完了,你不用管别人是否成功么?比如你下单了积分没加,优惠券没扣怎么办?

问题是个好问题,但是没必要考虑,业务系统本身就是自己的开发人员维护的,你积分扣失败关我下单的什么事情?你管好自己下单系统的就好了。这里其实涉及到MQ在分布式事务中数据一致性的问题。

数据一致性

就像咱们上面说的,你支付成功的服务自己保证自己的逻辑成功处理了,你成功发了消息,但是短信系统,积分系统等等这么多系统,他们成功还是失败你就不管了?当然不行啊,咱们是一个team,要共进退。

  怎么办?那就把所有的服务都放到一个事务里,所有都成功成功才能算这一次下单是成功的,要成功一起成功,要失败一起失败。

削峰:

如果业务平时流量很低,但是你要做秒杀活动00 :00的时候流量疯狂怼进来,你的服务器,RedisMySQL各自的承受能力都不一样,你直接全部流量照单全收肯定有问题啊,直接就打挂了。那怎么办?我们把请求放到队列里面,然后至于每秒消费多少请求,就看自己的服务器处理能力,你能处理5000QPS你就消费这么多,可能会比正常的慢一点,但是不至于打挂服务器,等流量高峰下去了,你的服务也就没压力了。你看阿里双十一12:00的时候这么多流量瞬间涌进去,他有时候是不是会慢一点,但是人家没挂啊,或者降级给你个友好的提示页面,等高峰过去了又是一条好汉了。

为了这个图特意打高一台服务的流量

比如我们系统有代售抢票业务,平时每天QPS也就50左右,A 系统风平浪静。结果每次一到春运抢票,每秒并发请求数量突然会暴增到10000以上。但是系统是直接基于 MySQL 的,大量的请求直接打到 MySQL,比如一般MySQL能抗2000条请求,现在每秒10000 条 SQL,可能就直接把 MySQL 给打死了,导致系统崩溃。但是高峰期一过就又没人了,QPS回到50,对整个系统几乎没有任何的压力。

如果这里使用 MQ,每秒 1w 个请求写入 MQ,A 系统每秒钟最多处理 2000 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok了,这样下来,哪怕是高峰期的时候,A 系统也不会挂掉。当然了,用户的响应时间肯定会受影响,毕竟秒杀嘛,只要把前多少条请求处理好,其余的抢票失败就行了。

  另外,MQ 每秒钟 1w 个请求进来,只处理 2k 个请求出去,结果会导致在中午高峰期,可能有几十万甚至几百万的请求积压在 MQ 中。

  这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给消费掉。

消息队列的优缺点

  • 系统可用性降低

  系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用?

  • 系统复杂度提高

  硬生生加个 MQ 进来,你怎么保证消息一定被消费?如何避免消息重复投递或重复消费?数据丢失怎么办?怎么保证消息传递的顺序性?

  • 一致性问题

  A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

消息中间机

目前市面上比较主流的消息队列中间件主要有,Kafka、ActiveMQ、RabbitMQ、RocketMQ 等。

  ActiveMQ和RabbitMQ这两由于吞吐量的原因,只有业务体量一般的公司在用,RabbitMQ由于是erlang语言开发的,我们都不了解,因此扩展和维护成本都很高,查个问题都头疼。

  Kafka和RocketMQ一直在各自擅长的领域发光发亮,两者的吞吐量、可靠性、时效性等都很可观。

在这里插入图片描述

 大家其实一下子就能看到差距了,就拿吞吐量来说,早期比较活跃的ActiveMQ 和RabbitMQ基本上不是后两者的对手了,在现在这样大数据的年代吞吐量是真的很重要

Kafka在大数据领域,公司的日志采集,实时计算等场景,都离不开他的身影,他基本上算得上是世界范围级别的消息队列标杆了。

消息的发送与接收

如何确保消息正确地发送至 RabbitMQ?如何确保消息接收方消费了消息?

发送方确认模式

  将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。

  一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。

  如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条Nack(not acknowledged,未确认)消息。

  发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

接收方确认机制

  消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。

  这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。保证数据的最终一致性;

MQ的可靠传输(消息丢失问题)

以我们常用的RabbitMQ为例,消息不可靠的情况可能是消息丢失,劫持等原因;

  丢失又分为:生产者丢失消息、消息队列丢失消息、消费者丢失消息

  生产者丢失消息:从生产者弄丢数据这个角度来看,RabbitMQ提供confirm模式来确保生产者不丢消息;

  confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后;RabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;

  如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

  消息队列丢数据:消息持久化。

  处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

  持久化配置和confirm机制配合使用,在消息持久化磁盘后,再给生产者发送一个Ack信号。

  这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

重复消费

消息重复消费是使用消息队列之后,必须考虑的一个问题,也是比较严重和常见的问题,在开发过程中但凡用到了消息队列,第一时间考虑的就是重复消费的问题。

就比如有这样的一个场景,用户下单成功后我需要去一个活动页面给他加GMV(销售总额),最后根据他的GMV去给他发奖励,这是电商活动很常见的玩法。

类似累计下单金额到哪个梯度给你返回什么梯度的奖励这样。

我只能告诉你这样的活动页面10000%是用异步去加的,不然你想,你一个用户下一单就给他加一下,那就意味着对那张表就要操作一下,你考虑下双十一当天多少次对这个表的操作?这数据库或者缓存都顶不住吧。

而且大家应该也有这样的体会,你下单了马上去看一些活动页面,有时候马上就有了,有时候却延迟有很久,为啥?这个速度取决于消息队列的消费速度,消费慢堵塞了就迟点看到呗。

你下个单支付成功你就发个消息出去,我们上面那个活动的开发人员就监听你的支付成功消息,我监听到你这个订单成功支付的消息,那我就去我活动GMV表里给你加上去,听到这里大家可能觉得顺理成章。

但是我告诉大家一般消息队列的使用,我们都是有重试机制的,就是说我下游的业务发生异常了,我会抛出异常并且要求你重新发一次。

我这个活动这里发生错误,你要求重发肯定没问题。但是大家仔细想一下问题在哪里?

是的,不止你一个人监听这个消息啊,还有别的服务也在监听,他们也会失败啊,他一失败他也要求重发,但是你这里其实是成功的,重发了,你的钱不就加了两次了?
面试官:那怎么解决呢?

我:

为了保证消息不被重复消费,首先要保证每个消息是唯一的,所以可以给每一个消息携带一个全局唯一的id,流程如下:

1、消费者监听到消息后获取id,先去查询这个id是否存中

2、如果不存在,则正常消费消息,并把消息的id存入 数据库或者redis中(下面的编码示例使用redis)

3、如果存在则丢弃此消息

测试

消息生产者服务

    /**
     * @Description:  发送消息 模拟消息重复消费
     *      消息重复消费情景: 消息生产者已把消息发送到mq,消息消费者在消息消费的过程中突然因为网络    
                            原因或者其他原因导致消息消费中断,消费者消费成功后,在给MQ确认的时候 
                            出现了网络波动,MQ没有接收到确认,为了保证消息被消费,MQ就会继续给消 
                            费者投递之前的消息。这时候消费者就接收到了两条一样的消息
     */
    @GetMapping("/rabbitmq/sendMsgNoRepeat")
    public String sendMsgNoRepeat() {
        String message = "server message sendMsgNoRepeat";
        for (int i = 0; i <10000 ; i++) {
            Message msg = MessageBuilder.withBody((message+"--"+i).getBytes()).setMessageId(UUID.randomUUID()+"").build();
            amqpTemplate.convertAndSend("queueName4",msg);
        }
        return message;
    }

消息消费者服务

将id存入string中(单消费者场景):

    @RabbitListener(queues = "queueName4")//发送的队列名称     @RabbitListener注解到类和方法都可以
    @RabbitHandler
    public void receiveMessage(Message message) throws UnsupportedEncodingException {
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(),"utf-8");
 
        String messageRedisValue = redisUtil.get("queueName4","");
        if (messageRedisValue.equals(messageId)) {//检查该消息是否已经消费过
            return;
        }
        System.out.println("消息:"+msg+", id:"+messageId);
 
        redisUtil.set("queueName4",messageId);//以队列为key,id为value
    }

将id存入list中(多消费者场景):

    @RabbitListener(queues = "queueName4")//发送的队列名称     @RabbitListener注解到类和方法都可以
    @RabbitHandler
    public void receiveMessage1(Message message) throws UnsupportedEncodingException {
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(),"utf-8");
 
        List<String> messageRedisValue = redisUtil.lrange("queueName4");
        if (messageRedisValue.contains(messageId)) {//检查该消息是否已经消费过
            return;
        }
        System.out.println("消息:"+msg+", id:"+messageId);
 
        redisUtil.lpush("queueName4",messageId);//存入list
    }

首先,启动消息生成服务,发送一万条消息

启动消息消费服务,然后中断服务,消费了1934条消息

查看未被消费的消息条数为8067条,多了一条(10000-1934=8066 )

再次启动消费者服务,消费者舍弃了已被消费的第1934(编号为1933)条消息

由此就避免了消息重复消费问题。(如果不判断该消息是否已经消费过了,则还是会从编号为1933(第1934)的消息开始消费,这就会导致重复消费) 。

消息顺序

(持续更新中...)

分布式事务

(持续更新中)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值