没用过消息队列?一文带你体验RabbitMQ收发消息

时间:2021-11-28 01:31:38

人生终将是场单人旅途,孤独之前是迷茫,孤独过后是成长。

楔子

先给大家说声抱歉,最近一周都没有发文,有一些比较要紧重要的事需要处理。

今天正好得空,本来说准备写SpringIOC相关的东西,但是发现想要梳理一遍还是需要很多时间,所以我打算慢慢写,先把MQ给写了,再慢慢写其他相关的,毕竟偏理论的东西一遍要比较难写,像MQ这种偏实战的大家可以clone代码去玩一玩,还是比较方便的。

同时MQ也是Java进阶不必可少的技术栈之一,所以Java开发从业者对它是必须要了解的。

现在市面上有三种消息队列比较火分别是:RabbitMQRocketMQKafka

今天要讲的消息队列中我会以RabbitMQ作为案例来入门,因为SpringBoot的amqp中默认只集成了RabbitMQ,用它来讲会方便许多,且RabbitMQ的性能和稳定性都很不错,是一款经过时间考验的开源组件。

祝有好收获。

本文代码: 码云地址GitHub地址

1. 消息队列?

消息队列(MQ)全称为Message Queue,是一种应用程序对应用程序的通信方法。

翻译一下就是:在应用之间放一个消息组件,然后应用双方通过这个消息组件进行通信。

好端端的为啥要在中间放个组件呢?

小系统其实是用不到消息队列的,一般分布式系统才会引入消息队列,因为分布式系统需要抗住高并发,需要多系统解耦,更需要对用户比较友好的响应速度,而消息队列的特性可以天然解耦,方便异步更能起到一个顶住高并发的削峰作用,完美解决上面的三个问题。


然万物抱阳负阴,系统之间突然加了个中间件,提高系统复杂度的同时也增加了很多问题:

  • 消息丢失怎么办?
  • 消息重复消费怎么办?
  • 某些任务需要消息的顺序消息,顺序消费怎么保证?
  • 消息队列组件的可用性如何保证?

这些都是使用消息队列过程中需要思考需要考虑的地方,消息队列能给你带来很大的便利,也能给你带来一些对应的麻烦。

上面说了消息队列带来的好处以及问题,而这些不在我们今天这篇的讨论范围之内,我打算之后再写这些,我们今天要做的是搭建出一个消息队列环境,让大家感受一下基础的发消息与消费消息,更高级的问题会放在以后讨论。

2. RabbitMQ一览

RabbitMQ是一个消息组件,是一个erlang开发的AMQP(Advanced Message Queue)的开源实现。

AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

RabbitMQ采用了AMQP协议,至于这协议怎么怎么样,我们关心的是RabbitMQ结构如何且怎么用。

还是那句话,学东西需要先观其大貌,我们要用RabbitMQ首先要知道它整体是怎么样,这样才有利于我们接下来的学习。

我们先来看看我刚画的架构图,因为RabbitMQ实现了AMQP协议,所以这些概念也是AMQP*有的。

没用过消息队列?一文带你体验RabbitMQ收发消息

  • Broker: 中间件本身。接收和分发消息的应用,这里指的就是RabbitMQ Server。

  • Virtual host: 虚拟主机。出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。

  • Connection: 连接。publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。

  • Channel: 渠道。如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销会比较大且效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。

  • Exchange: 路由。根据分发规则,匹配查询表中的routing key,分发消息到queue中去。

  • Queue: 消息的队列。消息最终被送到这里等待消费,一个message可以被同时拷贝到多个queue中。

  • Binding: 绑定。exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。


看完了这些概念,我再给大家梳理一遍其流程:

当我们的生产者端往Broker(RabbitMQ)中发送了一条消息,Broker会根据其消息的标识送往不同的Virtual host,然后Exchange会根据消息的路由key和交换器类型将消息分发到自己所属的Queue中去。

然后消费者端会通过Connection中的Channel获取刚刚推送的消息,拉取消息进行消费。

Tip:某个Exchange有哪些属于自己的Queue,是由Binding绑定关系决定的。

3. RabbitMQ环境

上面讲了RabbitMQ大概的结构图和一个消息的运行流程,讲完了理论,这里我们就准备实操一下吧,先进行RabbitMQ安装。

官网下载地址:http://www.rabbitmq.com/download.html

由于我还没有属于自己MAC电脑,所以这里的演示就按照Windows的来了,不过大家都是程序员,安装个东西总归是难不倒大家的吧

Windows下载地址:https://www.rabbitmq.com/install-windows.html

进去之后可以直接找到Direct Downloads,下载相关EXE程序进行安装就可以了。

由于RabbitMQ是由erlang语言编写的,所以安装之前我们还需要安装erlang环境,你下载RabbitMQ之后直接点击安装,如果没有相关环境,安装程序会提示你,然后会让你的浏览器打开erlang的下载页面,在这个页面上根据自己的系统类型点击下载安装即可,安装完毕后再去安装RabbitMQ

这两者的安装都只需要一直NEXT下一步就可以了。

安装完成之后可以按一下Windows键看到效果如下:

没用过消息队列?一文带你体验RabbitMQ收发消息

Tip:其中Rabbit-Command后面会用到,是RabbitMQ的命令行操作台。


安装完RabbitMQ我们需要对我们的开发环境也导入RabbitMQ相关的JAR包。

为了方便起见,我们可以直接使用Spring-boot-start的方式导入,这里面也会包含所有我们需要用到的RabbitMQ相关的JAR包。

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>

直接引入spring-boot-starter-amqp即可。

4. ✍Hello World

搭建好环境之后,我们就可以上手了。

考虑到这是一个入门文章,读者很多可能没有接触过RabbitMQ,直接使用自动配置的方式可能会令大家很迷惑,因为自动配置会屏蔽很多细节,导致大家只看到了被封装后的样子,不利于大家理解。

所以在本节Hello World这里,我会直接使用最原始的连接方式就行演示,让大家看到最原始的连接的样子。

Tip:这种方式演示的代码我都在放在prototype包下面。

4.1 生产者

先来看看生产者代码,也就是我们push消息的代码:

    public static final String QUEUE_NAME = "erduo";

    // 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory(); // 连接到本地server
connectionFactory.setHost("127.0.0.1"); // 通过连接工厂创建连接
Connection connection = connectionFactory.newConnection(); // 通过连接创建通道
Channel channel = connection.createChannel(); // 创建一个名为耳朵的队列,该队列非持久(RabbitMQ重启后会消失)、非独占(非仅用于此链接)、非自动删除(服务器将不再使用的队列删除)
channel.queueDeclare(QUEUE_NAME, false, false, false, null); String msg = "hello, 我是耳朵。" + LocalDateTime.now().toString();
// 发布消息
// 四个参数为:指定路由器,指定key,指定参数,和二进制数据内容
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8)); System.out.println("生产者发送消息结束,发送内容为:" + msg);
channel.close();
connection.close();

代码我都给了注释,但是我还是要给大家讲解一遍,梳理一下。

先通过RabbitMQ中的ConnectionFactory配置一下将要连接的server-host,然后创建一个新连接,再通过此连接创建通道(Channel),通过这个通道创建队列和发送消息。

这里看上去还是很好理解的,我需要把创建队列和发送消息这里再拎出来说一下。

创建队列

    AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;

创建队列的方法里面有五个参数,第一个是参数是队列的名称,往后的三个参数代表不同的配置,最后一个参数是额外参数。

  • durable:代表是否将此队列持久化。

  • exclusive:代表是否独占,如果设置为独占队列则此队列仅对首次声明它的连接可见,并在连接断开时自动删除。

  • autoDelete:代表断开连接后是否自动删除此队列。

  • arguments:代表其他额外参数。

这些参数中durable经常会用到,它代表了我们可以对队列做持久化,以保证RabbitMQ宕机恢复后此队列也可以自行恢复。

发送消息

    void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body) throws IOException;

发送消息的方法里是四个参数,第一个是必须的指定exchange,上面的示例代码中我们传入了一个空字符串,这代表我们交由默认的匿名exchange去帮我们路由消息。

第二个参数是路由key,exchange会根据此key对消息进行路由转发,第三个参数是额外参数,讲消息持久化时会用到一下,最后一个参数就是我们要发送的数据了,需要将我们的数据转成字节数组的方式传入。

测试

讲完了这些API之后,我们可以测试一下我们的代码了,run一下之后,会在控制台打出如下:

没用过消息队列?一文带你体验RabbitMQ收发消息

这样之后我们就把消息发送到了RabbitMQ中去,此时可以打开RabbitMQ控制台(前文安装时提到过)去使用命令rabbitmqctl.bat list_queues去查看消息队列现在的情况:

没用过消息队列?一文带你体验RabbitMQ收发消息

可以看到有一条message在里面,这就代表我们的消息已经发送成功了,接下来我们可以编写一个消费者对里面的message进行消费了。

4.2 消费者

消费者代码和生产者的差不多,都需要建立连接建立通道:

    // 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory(); // 连接到本地server
connectionFactory.setHost("127.0.0.1"); // 通过连接工厂创建连接
Connection connection = connectionFactory.newConnection(); // 通过连接创建通道
Channel channel = connection.createChannel(); // 创建消费者,阻塞接收消息
com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("-------------------------------------------");
System.out.println("consumerTag : " + consumerTag);
System.out.println("exchangeName : " + envelope.getExchange());
System.out.println("routingKey : " + envelope.getRoutingKey());
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容 : " + msg);
}
}; // 启动消费者消费指定队列
channel.basicConsume(Producer.QUEUE_NAME, consumer);
// channel.close();
// connection.close();

建立完通道之后,我们需要创建一个消费者对象,然后用这个消费者对象去消费指定队列中的消息。

这个示例中我们就是新建了一个consumer,然后用它去消费队列-erduo中的消息。

最后两句代码我给注释掉了,因为一旦把连接也关闭了,那我们的消费者就不能保持消费状态了,所以要开着连接,监听此队列。

ok,运行这段程序,然后我们的消费者会去队列-erduo拿到里面的消息,效果如下:

没用过消息队列?一文带你体验RabbitMQ收发消息

  • consumerTag:是这个消息的标识。

  • exchangeName:是这个消息所发送exchange的名字,我们先前传入的是空字符串,所以这里也是空字符串。

  • exchangeName:是这个消息所发送路由key。

这样我们的程序就处在一个监听的状态下,你再次调用生产者发送消息消费者就会实时的在控制上打印消息内容。

5. 消息接收确认(ACK)

上面我们演示了生产者和消费者,我们生产者发送一条消息,消费者消费一条信息,这个时候我们的RabbitMQ应该有多少消息?

理论上来说发送一条,消费一条,现在里面应该是0才对,但是现在的情况并不是:

没用过消息队列?一文带你体验RabbitMQ收发消息

消息队列里面还是有1条信息,我们重启一下消费者,又打印了一遍我们消费过的那条消息,通过消息上面的时间我们可以看出来还是当时我们发送的那条信息,也就是说我们消费者消费过了之后这条信息并没有被删除。

没用过消息队列?一文带你体验RabbitMQ收发消息

这种状况出现的原因是因为RabbitMQ消息接收确认机制,也就是说一条信息被消费者接收到了之后,需要进行一次确认操作,这条消息才会被删除。

RabbitMQ中默认消费确认是手动的,也可以将其设置为自动删除,自动删除模式消费者接收到消息之后就会自动删除这条消息,如果消息处理过程中发生了异常,这条消息就等于没被处理完但是也被删除掉了,所以这里我们会一直使用手动确认模式。

消息接受确认(ACK)的代码很简单,只要在原来消费者的代码里加上一句就可以了:

    com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("-------------------------------------------");
System.out.println("consumerTag : " + consumerTag);
System.out.println("exchangeName : " + envelope.getExchange());
System.out.println("routingKey : " + envelope.getRoutingKey());
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("消息内容 : " + msg); // 消息确认
channel.basicAck(envelope.getDeliveryTag(), false);
System.out.println("消息已确认");
}
};

我们将代码改成如此之后,可以再run一次消费者,可以看看效果:

没用过消息队列?一文带你体验RabbitMQ收发消息

再来看看RabbitMQ中的队列情况:

没用过消息队列?一文带你体验RabbitMQ收发消息

从图中我们可以看出消息消费后已经成功被删除了,其实大胆猜一猜,自动删除应该是在我们的代码还没执行之前就帮我们返回了确认,所以这就导致了消息丢失的可能性。

我们采用手动确认的方式之后,可以先将逻辑处理完毕之后(可能出现异常的地方可以try-catch起来),把手动确认的代码放到最后一行,这样如果出现异常情况导致这条消息没有被确认,那么这条消息会在之后被重新消费一遍。

后记

今天的内容就到这里,下一篇将会我们将会撇弃传统的手动建立连接的方式进行发消息收消息,而转用Spring帮我们定义好的注解和Spring提供的RabbitTemplate,更方便的收发消息。

消息队列呢,其实用法都是一样的,只是各个开源消息队列的侧重点稍有不同,我们应该根据我们自己的项目需求来决定我们应该选取什么样的消息队列来为我们的项目服务,这个项目选型的工作一般都是开发组长帮你们做了,一般是轮不到我们来做的,但是面试的时候可能会考察相关知识,所以这几种消息队列我们都应该有所涉猎。

好了,以上就是本期的全部内容,感谢你能看到这里,欢迎对本文点赞收藏与评论,你们的每个点赞都是我创作的最大动力。

我是耳朵,一个一直想做知识输出的伪文艺程序员,我们下期见。

本文代码:码云地址GitHub地址

没用过消息队列?一文带你体验RabbitMQ收发消息的更多相关文章

  1. 初试kafka消息队列中间件二(采用java代码收发消息)

    初试kafka消息队列中间件二(采用java代码收发消息) 上一篇 初试kafka消息队列中间件一 今天的案例主要是将采用命令行收发信息改成使用java代码实现,根据上一篇的接着写: 先启动Zooke ...

  2. 刚体验完RabbitMQ?一文带你SpringBoot&plus;RabbitMQ方式收发消息

    人生终将是场单人旅途,孤独之前是迷茫,孤独过后是成长. 楔子 这篇是消息队列RabbitMQ的第二弹. 上一篇的结尾我也预告了本篇的内容:利用RabbitTemplate和注解进行收发消息,还有一个我 ...

  3. 消息队列高手课&comma;带你从源码角度全面解析MQ的设计与实现

    消息队列中间件的使用并不复杂,但如果你对消息队列不熟悉,很难构建出健壮.稳定并且高性能的企业级系统,你会面临很多实际问题: 如何选择最适合系统的消息队列产品? 如何保证消息不重复.不丢失? 如果你掌握 ...

  4. 消息队列系列(二):&period;Rabbitmq的使用及Web监控工具使用

    一.文档资料        1.官方网站:http://www.rabbitmq.com/        2.安装教程:http://www.rabbitmq.com/install-windows. ...

  5. &lpar;转&rpar;RabbitMQ消息队列(六):使用主题进行消息分发

    在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity的log.但是,这也是它之所以叫做简单日志系统的原因, ...

  6. RabbitMQ消息队列(六):使用主题进行消息分发

    在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity的log.但是,这也是它之所以叫做简单日志系统的原因, ...

  7. RabbitMQ消息队列(六):使用主题进行消息分发&lbrack;转&rsqb;

    在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity(严重级别)的log.但是,这也是它之所以叫做简单日志 ...

  8. 通信与实际用例应用(消息队列和进程撰写的ATM机与消息队列的五子棋对站)

    int semget(key_t key, int nsems, int semflg); 功能:创建信号量或获取信号量 nsems:信号量的数量 semflg: IPC_CREAT|IPC_EXEC ...

  9. RabbitMQ消息队列(九)-通过Headers模式分发消息(&period;Net Core版)

    Headers类型的exchange使用的比较少,以至于官方文档貌似都没提到,它是忽略routingKey的一种路由方式.是使用Headers来匹配的.Headers是一个键值对,可以定义成Hasht ...

随机推荐

  1. 命令行查看Windows激活信息&lpar;win7、win8、win10&period;&period;&period;&rpar;

    使用:Win+ R 组合键,打开运行命令框,复制命令,粘贴后回车. slmgr.vbs -xpr 查询Windows是否永久激活slmgr.vbs -dlv 查询到Windows的激活信息,包括:激活 ...

  2. Eclipse常用快捷键汇总

    经常使用eclipse进行开发,不掌握快捷键步行啊,在此整理了一些快捷键,大家要灵活运用啊... (注:红色标出来的是经常使用到的快捷键,磨刀不误砍柴工啊...) Ctrl+1 快速修复(最经典的快捷 ...

  3. FFT质数打表程序

    #include<bits/stdc++.h> using namespace std; typedef long long ll; void sol(ll x){ int y=0; fo ...

  4. linux 标准输入输出

    文件描述符是一个简单的正整数,用以标明每一个被进程所打开的文件和socket.最前面的三个文件描述符(0,1,2)分别与标准输入(stdin),标准输出(stdout)和标准错误(stderr)对应 ...

  5. HTMLTestRunner生成空白resault&period;html

    发现一奇葩问题,用idle或pyscripter执行脚本,生成的是空白html,通过cmd,进入脚本目录执行python xx.py,却能生成测试报告. HTMLTestRunner 例子 #codi ...

  6. redis(Remote Dictionary Server)

    redis的简介和使用   简介 redis(Remote Dictionary Server)是一种Nosql技术,它是一个开源的高级kv存储和数据结构存储系统,它经常被拿来和Memcached相比 ...

  7. 微信小程序封装http访问网络库实例代码

    之前都是使用LeanCloud为存储,现在用传统API调用时做如下封装 var HOST = 'http://localhost/lendoo/public/index.php/'; // 网站请求接 ...

  8. python 批量修改数字类的文件名

    今天碰到一个小问题,下载音频的时候,文件名的名字变成了数字,排序呢,是按照数字的大小往下排的. 想自己给它们重新起名字,但是又不打乱音频的顺序.好吧,那就自己写写代码吧. 思路就是遍历音频文件的数字文 ...

  9. ARM汇编指令格式

    一.ARM介绍 1) arm汇编中,程序下一步执行哪一条指令,是根据程序计数器(PC)的值来决定. 类比于8086的CS:IP的功能. 2) 8086属于CISC(复杂指令集),每条指令长度不等,ar ...

  10. Http Header 之 Requests Header 和 Responses Header

    在开发中,经常会遇到对网络请求添加相应的头信息,下面我们梳理一下Http Header相关的内容. 一.Requests Header Header 解释 示例 Accept 指定客户端能够接收的内容 ...