Java打怪之路----谷粒商场RabbitMQ

Java 内存级别
分布式 需要消息中间件

一、RabbitMQ介绍

1、简介

RabbitMQ是一种消息队列。
Java中也提供Queue的相关操作,但是Java中的API是基于内存级别,我们的微服务使用它的API保存数据,最多只能在它的机器中使用。、

在分布式系统中,我们将消息全都保存在消息中间件中,消息中间件是在服务器中部署,所有的微服务都可以通过这个消息中间件来获取消息。

2、应用场景

  1. 异步处理:通过将将成功消息写入消息中间件,无需等待邮箱发送邮件
    在这里插入图片描述
    图一是注册写入数据库、发送注册邮件、发送注册短信三者同步执行,这样会带来的弊端就是用户从注册到得知注册成功需要叫长的时间;
    图二进行了将注册邮箱和注册短信做了异步处理,但是还是需要等待返回邮件和短信;
    图三将写入数据库的消息写入消息队列,发送注册邮件和发送注册邮箱订阅消息队列,注册邮件、注册短信,这两个任务,让它在后台慢慢发就行,成功或是失败,我们无需知道,只要它做了这个事就行,而且我们经常会有收不到短信、收不到邮件的情况
  2. 应用解耦
    在这里插入图片描述

假设订单系统有3个参数,库存系统有5个参数,直接调用就可以,如果这个库存系统不升级,API 也不变,一直是这几个参数还好,

假设我们库存系统经常会升级, 减库存的接口经常发生变化,这样我们以前的这种调用方式,一旦库存系统升级了,则订单系统必须修改它的源代码,重新部署,这样就感觉会非常麻烦,所以我们可以引入消息队列,

订单系统只要下好订单,我们给消息队列里面写上一个消息,说我们哪个用户下了哪个订单购买了哪个商品,把这个消息保存到队列里面,我们不关心库存系统的接口是什么样,不管它要几个参数,我们只需要把我们的消息写进去,接下来库存系统要实时订阅我们队列里面的内容,只要有内容,库存系统就会收到我们订单系统写的消息,然后它自己分析消息,然后对库存进行修改

  1. 流量控制
    在这里插入图片描述

针对一些秒杀业务来说,瞬间流量会非常大,比如:瞬间百万个请求都要进来秒杀一个商品,这个商品要真正去执行业务,就算我们前端服务器可以接受百万请求,我们要执行业务代码,因为我们秒杀完之后,要下订单,整个流程会非常慢, 后台会一直阻塞,可能就会导致资源耗尽,最终导致宕机

此时,我们可以这样做,我们让大并发的请求全部进来,进来以后,先将它们存储到消息队列里面,存到消息队列以后,我们就可以不用管这个请求该怎么做了,直接给它响应:秒杀成功了或者其他

然后,消息队列中,后台真正的业务处理要下订单、减库存等等这些业务处理,我们不着急立即调用,只要存到消息队列里面,这些业务去订阅消息队列里面进来的这些秒杀请求,接下来,挨个处理:下订单…,即使后台每秒只能处理1个,那100W请求,也就花费100W秒,但永远都不会导致机器的资源耗尽,导致宕机所以我们可以达到前端的流量控制。

二、RabbitMQ概述

大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力

1、Rabbit中的一些概念

  1. Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
  2. Publish:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
  3. Exchage:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别
  4. Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
  5. Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表Exchange 和Queue的绑定可以是多对多的关系。
  6. Connection:网络连接,比如一个TCP连接。
  7. Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
  8. 消息代理:当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
  9. 目的地:消息队列主要有两种形式的目的地
    1. 队列(queue):点对点消息通信(point-to-point):消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获
      取消息内容,消息读取后被移出队列
    2. 主题(topic):发布(publish)/订阅(subscribe)消息通信 :发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个
      主题,那么就会在消息到达时同时收到消息

2、JMS与AMQP

JMS:基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
AMQP:高级消息队列协议,也是一个消息代理的规范,兼容JMS。RabbitMQ是AMQP的实现
在这里插入图片描述

3、RabbitMQ工作流程

1、Publisher生产出消息,消息会送入消息代理中。消息包含消息头和消息体,最主要的是包含一个路由键,路由键的作用是让交换机知道发送到哪个消息队列中。;
2、消息代理中的交换机根据绑定规则,将消息路由到队列中。绑定规则中传入的参数包括交换机名,队列名和路由键;
3、consumer监听该队列,根据获取消息队列。
4、注意: 无论是消息发送还是消息接受,都会与消息代理建立连接(connection),消息在连接内的不同信道进行传输。
在这里插入图片描述

三、Exchange交换机

RabbitMQ与JMS中不同点在于消息路由的方式不一样,RabbitMQ中设置了Exchange交换机和binding绑定规则。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。

四种Exchange

  1. direct:消息中的路由键如果与绑定中的bindingKey一致,就将消息发送给对应的队列中。direct路由规则是发送消息的路由键与绑定的路由键完全匹配。如果一个队列绑定到交换机的路由键是dog,那么只会转发路由键为dog的消息到该队列,不会转发其他路由键的消息。是单播模式。
    在这里插入图片描述

  2. fanout:发到fanout类型的交换机的消息,都会全部转发到所有绑定的队列中。fanout交换机不处理路由键,只是简单将消息转发到绑定的队列中。是广播模式。
    在这里插入图片描述

  3. topic:将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
    在这里插入图片描述

四、RabbitMQ消息确认机制

引入消息确认机制是为了让消息可靠抵达

在分布式系统中,比如现在有很多微服务,微服务连接上消息队列服务器,其它微服务可能还要监听这些消息,

但是可能会因为服务器抖动、宕机,MQ 的宕机、资源耗尽,以及无论是发消息的生产者、还是收消息的消费者,它们的卡顿、宕机等各种问题,都会导致消息的丢失,比如发送者发消息的时候,给弄丢了 ,看起来消息是发出去了,MQ网络抖动没接到, 或者MQ接到了,但是它消费消息的时候,因为网络抖动又没拿到,等等各种问题

所以在分布式系统里面,一些关键环节,我们需要保证消息一定不能不丢失,比如:订单消息发出去之后,该算库存的、该算积分的、该算优惠的等等 ,这些消息千万不能丢,因为这都是经济上的问题
所以,想要保证不丢失,也就是可靠抵达,无论是发消息,可靠的抵达MQ,还是收消息,MQ的消息可靠抵达到我们的消费端,我们一定要保证消息可靠抵达,包括如果出现错误,我们也应该知道哪些消息丢失了,

以前我们要做这种事情,可以使用事务消息,比如我们在发消息的时候,我们发消息的客户端首先会跟 MQ 建立一个连接,会在通道里面发消息,可以将通道设置成事务模式,这样发消息,只有整个消息发送过去,MQ消费成功给我们有完全的响应以后,我们才算消息成功,

但是使用事务消息,会使性能下降的很严重,官方文档说,性能会下降250倍…

为了保证在高并发期间能很快速的,确认哪些消息成功、哪些消息失败,我们引入了消息确认机制,

4.1消息确认时机

从下图可以看出,消息需要经过三个流程到达消费端

发送端确认:
生产者送入交换机需要确认消息发送成功
交换机送入队列需要消息确认发送成功

接收端确认
消费者拿到消息需要确认接受成功

在这里插入图片描述

4.2发送端确认

①调用时机

为了保证发送端可靠传递,在发送端两个阶段引入确认回调。确认回调的含义在于,当发送消息成功,就会调用回调方法来确认消息发送成功。成功与否都会触发

p---->b confirmCallback,就是P端给B端 发送消息的过程,Broker 一旦收到了消息,就会回调我们的方法 confirmCallback,这是第一个回调时机,这个时机就可以知道哪些消息到达服务器了

e---->q returnCallback,服务器收到消息以后,要使用 Exchange 交换机,最终投递给 Queue,但是投递给队列这个过程可能也会失败,比如我们指定的路由键有问题,或者我们队列正在使用的过程中,被其它的一些客户端删除等操作,可能都会投递失败,投递失败就会调用 returnCallback

①代码实现
配置文件中开启发送端确认

spring:
  rabbitmq:
    publisher-confirms: true  # 开启发送端确认
    publisher-returns: true # 开启发送端消息抵达队列的确认  
    template:
      mandatory: true # 只要消息抵达了队列,以异步发送优先回调这个returnconfirm

confirmCallback

@Configuration
public class MyRabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //自定义初始化initRabbitTemplate
    @PostConstruct //MyRabbitConfig对象构造器完成之后调用
    public void initRabbitTemplate(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 生产者发送给broker后的回调
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param b 消息是否成功收到
             * @param s 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                //指定你要做什么
            }
        });
        });
    }
}

returnCallback

@Configuration
public class MyRabbitConfig {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    //自定义初始化initRabbitTemplate
    @PostConstruct //MyRabbitConfig对象构造器完成之后调用
    public void initRabbitTemplate(){
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * broker发送给队列(Queue)后的失败回调
             * @param message 哪个投递失败的消息信息
             * @param i 回复码
             * @param s 回复的文本内容
             * @param s1 当时这个消息发送给哪个交换机
             * @param s2 但是这个消息用的哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int i, String s, String s1, String s2) {
                //指定你要做什么
            }
        });
    }
}

  • 消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有broker 接收到才会调用 confirmCallback。

  • 被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里。所以需要用到接下来的 returnCallback 。

4.3接收端确认

消费端默认的签收方式是自动确认,即只要发送端成功发送,消费端就自动签收,并且服务端会删除相应的消息

然而, 当消费端接收到很多消息,自动回复给服务器ack,但只有一个被成功处理,此时服务端会把所有消息删除,导致其余消息丢失。

解决办法: 将接收端的确认模式设置为手动确认,来一个消息确认一个消息

配置文件中设置为手动确认

  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual #手动确认模式

只要没有手动确认消息被接受,消息一直为unack状态,服务端不会删除此消息,在客户端宕机之后,未被确认的消息会进入ready状态

具体如何实现: 用channel调用basicAck方法
在这里插入图片描述

在这里插入图片描述

拒收消息: 用channel调用basicNAck方法

nack() 用于否定确认;可以指定broker 是否丢弃此消息,可以批量

reject() 用于否定确认;同上,但不能批量

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值