RabbitMQ发送和接收消息实战(代码可运行)

986 阅读4分钟

1、消息队列是什么?

我们可以把消息队列看作是存储消息的容器,当需要使用消息的时候,直接从容器中取出消息进行消费就可以了。如下图所示:

                          

2、消息队列产生的背景

在如今的互联网大环境下,随着用户流量的快速增长,由于传统应用在系统接口和服务处理模块层面仍然沿用“高耦合”和“同步”的处理方式,导致接口由于线程阻塞而延长了整体响应时间,也就是接口响应的“高延迟”,导致用户体验差。

消息队列的使用,可以对多个模块之间进行解耦,并实现异步通信,降低系统整合的响应时间。消息队列也可以对高并发请求的接口进行 削峰/限流。

举个例子:

电商平台的秒杀活动,传统的处理流程:在秒杀活动开始的那一刻,将会产生巨大的用户抢购请求,这些请求几乎在同一时刻到达后端系统接口,这样业务系统处理压力太大,从而导致大量失败甚至崩溃,也就是用户请求压力=业务系统压力。

引入消息队列后,当用户进行抢购时,前端产生的高并发请求,并不会像无头苍蝇一样达到后端系统接口,而是像每天上班时的地铁限流一样,将这些请求按照先来后到的规则加入到MQ队列,这样就实现了秒杀活动接口的削峰/限流。

3、常见的几种消息队列以及各自的应用场景

常见几种消息队列,有ActiveMQ、RabbitMQ、RacketMQ、Kafka,它们区别如下图所示:

从图中可以看到,RabbitMQ功能完备,且社区活跃,我们可以依靠RabbitMQ社区的力量解决开发过程中遇到的bug,适用于中小型软件公司。鉴于RabbitMQ的普遍性,下面重点介绍RabbitMQ的核心概念,并与SpringBoot整合,实现RabbitMQ发送、接收消息实战。

4、RabbitMQ的核心概念,类比于寄快递和取快递

RabbitMQ的核心概念:生产者、消息模型、消费者。

消息模型,是由交换机、路由、队列组成。

为了便于理解RabbitMQ的概念,我们结合实际生活中寄快递和取快递的场景。

小芹住在武汉,需要寄一个快递包裹,发送给深圳的小汪。具体寄快递、取快递情况如下:

小芹将包裹给快递员,然后快递员通过 某种运输方式将包裹从武汉运到深圳,小汪从深圳快递点取走快递。

我们类比下,得到如下的对应关系:

小芹:生产者

快递包裹:消息

快递员:交换机

某种运输方式:路由

武汉到深圳:队列名称

小汪:消费者

所以寄快递、取快递的生活场景,我们可以看成如下的消息传输过程:

生产者将消息给交换机,然后交换机通过路由,将消息发送武汉到深圳的队列,消费者取出该条消息进行处理。生产者-消息-消息模型-消费者的示意图,如下所示:

5、RabbitMQ与SpringBoot整合,发送、接收消息实战

5.1、SpringBoot项目的pom文件中引入RabbitMQ的起步依赖

<!-- RabbitMQ的起步依赖,和spring-boot整合成一个Jar包了 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId><version>1.5.6.RELEASE</version></dependency>

5.2、配置文件中配置RabbitMQ的host、端口等信息

我们在application.properties配置文件中,增加host主机地址、port端口号、用户名、密码、

virtual-host,如下所示:# RabbitMQ配置spring.rabbitmq.virtual-host=/# 配置host、端口号、用户名、密码spring.rabbitmq.host=127.0.0.1spring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.password=guest

接着配置队列、交换机、路由的信息:

# 设置交换机、路由、队列,使用directExchange消息模型# 自定义变量,表示本地开发环境mq.env=local# 设置direct消息模型中队列、交换机、路由:字符串信息mq.basic.info.queue.name=${mq.env}.middleware.mq.basic.info.queue.demo1mq.basic.info.exchange.name=${mq.env}.middleware.mq.basic.info.exchange.demo1mq.basic.info.routing.key.name=${mq.env}.middleware.mq.basic.info.routing.key.demo1

5.3、自定义注入Bean相关组件

RabbitMQ在实际项目的应用过程中,如果配置和使用不当,则会出现各种令人头疼的问题,面试官经常考察的问题:如何防止消息丢失?如何保证消费不被重复消费?

笔者就出现了消息丢失的问题,以一大把头发为代价才解决了此问题。

为此,笔者参考学习了RabbitMQ的相关教程和视频,为了保证消息的高可用和确认消费,RabbitMQ官方给我们提供了三条准则。

也就是如果我们想要保证消息的高可用和确认消费,需要遵守这3条准则。

(1)生产者的发送确认机制;

(2)创建队列、交换机消息时设置持久化模式;

(3)消费者的确认消费Ack机制。

(1)生产者的发送确认机制

位于RabbitmqConfig#rabbitTemplate()

/*** 构建RabbitMQ发送消息的操作组件实例* 生产者的发送确认机制*/@Bean(name = "rabbitMQTemplate")public RabbitTemplate rabbitTemplate() {// 生产者确认消息是否发送过去了connectionFactory.setPublisherConfirms(true);// 生产者发送消息后,返回反馈消息connectionFactory.setPublisherReturns(true);// 构建rabbitTemlate操作模板RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);rabbitTemplate.setMandatory(true);// 生产者发送消息后,如果发送成功,则打印“消息发送成功”的日志信息rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {public void confirm(CorrelationData correlationData, boolean ack, String cause) {log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);}});// 生产者发送消息后,若发送失败,则输出“消息发送失败”的日志信息rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);}});return rabbitTemplate;}

(2)创建队列、交换机、消息,设置持久化模式

RabbitmqConfig#basicQueue() 、 DirectExchange basicExchange()

BasicPublisher#sendMsg()

1)创建队列,设置持久化

// 1.1、创建队列@Bean(name = "basicQueue")public Queue basicQueue() {return new Queue(env.getProperty("mq.basic.info.queue.name"), true);}

2)创建交换机设置持久化

3)创建消息,设置持久化模式

// 2创建队列、交换机、消息 设置持久化模式// 设置消息的持久化模式Message message = MessageBuilder.withBody(messageStr.getBytes("utf-8")).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();

(3)消费者的确认消费Ack机制

消费者的确认消费机制有三种:None、Auto、Manual

None:不进行确认消息,也就是消费者发送任何反馈信息给MQ服务端;

Auto:消费者自动确认消费。消费者处理该消息后,需要发送一个自动的ack反馈信息给MQ服务端,之后该消息从MQ的队列中移除掉。其底层的实现逻辑是由RabbitMQ内置的相关组件实现自动发送确认反馈信息。

Manual:人为手动确认消费机制。消费者处理该消息后,需要手动地“以代码的形式”发送给一个ack地反馈信息给MQ服务端。

RabbitmqConfig#listenerContainer()

/*** 3消费者的确认消费Ack机制* 单一消费者-ack确认消费模式AUTO* @return*/@Bean(name = "singleListenerContainer")public SimpleRabbitListenerContainerFactory listenerContainerAuto(){SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConnectionFactory(connectionFactory);factory.setMessageConverter(new Jackson2JsonMessageConverter());factory.setConcurrentConsumers(1);factory.setMaxConcurrentConsumers(1);factory.setPrefetchCount(1);//设置确认消费模式为自动确认消费-AUTOfactory.setAcknowledgeMode(AcknowledgeMode.AUTO);return factory;}

5.4、RabbitMQ发送、接收实战

(1)定义生产者

@Component@Slf4jpublic class BasicPublisher {@Autowiredprivate RabbitTemplate rabbitTemplate;@AutowiredEnvironment env;// 发送字符串类型的消息public void sendMsg(String messageStr) {if (!Strings.isNullOrEmpty(messageStr)) {try {rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());rabbitTemplate.setExchange(env.getProperty("mq.basic.info.exchange.name"));rabbitTemplate.setRoutingKey(env.getProperty("mq.basic.info.routing.key.name"));// 2创建队列、交换机、消息 设置持久化模式// 设置消息的持久化模式Message message = MessageBuilder.withBody(messageStr.getBytes("utf-8")).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();rabbitTemplate.convertAndSend(message);log.info("基本消息模型-生产者-发送消息:{}", messageStr);} catch (UnsupportedEncodingException e) {log.error("基本消息模型-生产者-发送消息发生异常:{}", messageStr, e.fillInStackTrace());}}}}

(2)创建消费者

@Component@Slf4jpublic class BasicConsumer {/*** 监听并消费队列中的消息*/@RabbitListener(queues = "${mq.basic.info.queue.name}", containerFactory = "singleListenerContainer")public void consumerMsg(@Payload byte[] msg) {try {String messageStr = new String(msg, "utf-8");log.info("基本消息模型-消费者-监听并消费到的消息:{}", messageStr);} catch (UnsupportedEncodingException e) {log.error("基本消息模型-消费者-发生异常:", e.fillInStackTrace());}}}

(3)编写单元测试,将一串字符发送到消息队列

@Slf4j@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class RabbitMQTest {@Autowiredprivate BasicPublisher basicPublisher;// 测试 基本消息模型,消息内容为 字符串@Testpublic void testBasicMessageModel() {String msgStr = "~~~这是一串字符串消息~~~~";basicPublisher.sendMsg(msgStr);}}

(4)运行单元测试

对编写的单元测试,测试结果如下图:

从图中可以看到,生产者发送了一条消息:"~~~这是一串字符串消息~~~~",放到了消息队列中;消费者监听并处理了该条消息,然后消费者打印了该消息内容。

  1. 这样,我们就完成了RabbitMQ的发送、接收消息实战,而且配置了RabbitMQ保证消息的高可用和确认消费,需要遵守这3条准则:
  • 生产者的发送确认机制;

  • 创建队列、交换机消息时设置持久化模式;

  • 消费者的确认消费Ack机制。

RabbitMQ发送和接收消息实战,该项目链接笔者放在码云仓库:

gitee.com/qinstudy/sp…

至此,笔者完成了RabbitMQ发送和接收消息实战,后续将持续输出RabbitMQ的其他教程,如RabbitMQ的死信队列等知识点。