当前位置:   article > 正文

RabbitMQ的使用和常见问题_raabbit 新建队列报错

raabbit 新建队列报错

1、RabbitMQ介绍

        MQ(Message Queue)消息队列,字面来看就是存放消息的队列(一种数据结构);

RabbitMQ中的一些角色:

        publisher:生产者;

        consumer:消费者;

        exchange:交换机,负责消息路由;

        queue:队列,存储消息;

        virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息  ;

1.1、MQ的优势

        使用MQ可实现微服务之间的异步通讯;

        降低服务之间的耦合;

        提升性能和吞吐量;

        服务之间没有强依赖,不用担心级联失败问题;

        流量削峰;

1.2、MQ的缺点

        架构复杂了,业务没有明显的流程线,不好管理(对程序员的技术要求高了);

        需要依赖于Broker的可靠、安全、性能;

1.3、常见的MQ

        ActiveMQ
        RabbitMQ
        RocketMQ
        Kafka

几种常见MQ的对比
对比RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMP

OpenWire,

STOMP,REST,

XMPP,

AMQP

自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

        追求可用性:Kafka、 RocketMQ 、RabbitMQ

        追求可靠性:RabbitMQ、RocketMQ

        追求吞吐能力:RocketMQ、Kafka

        追求消息低延迟:RabbitMQ、Kafka

2、快速入门

2.1、单机部署

        我们在Centos7虚拟机中使用Docker来安装;

        (1)在线拉取(联网):

docker pull rabbitmq:3.8-management

        (2)或者使用镜像包(找不到可以评论获取)

                上传到虚拟机后使用加载镜像命令即可:

docker load -i rabbmitmq-3.8.tar

        安装mq

        拉取完或者上传完后来进行安装

  1. docker run \
  2. -e RABBITMQ_DEFAULT_USER=itcast \
  3. -e RABBITMQ_DEFAULT_PASS=123321 \
  4. -v mq-plugins:/plugins \
  5. --name mq \
  6. --hostname mq \
  7. -p 15672:15672 \
  8. -p 5672:5672 \
  9. -d \
  10. rabbitmq:3.8-management
2.2、集群部署(详细部署过程后期更新)

首先,我们需要让3台MQ互相知道对方的存在。

分别在3台机器中,设置 /etc/hosts文件,添加如下内容:

192.168.150.101 mq1
192.168.150.102 mq2
192.168.150.103 mq3

2.3、简单队列实现步骤:(Demo)

基本消息队列的消息发送流程:

1. 建立connection

2. 创建channel

3. 利用channel声明队列

4. 利用channel向队列发送消息

5.关闭连接和channel

  1. package cn.itcast.mq.helloworld;
  2. import com.rabbitmq.client.Channel;
  3. import com.rabbitmq.client.Connection;
  4. import com.rabbitmq.client.ConnectionFactory;
  5. import org.junit.Test;
  6. import java.io.IOException;
  7. import java.util.concurrent.TimeoutException;
  8. public class PublisherTest {
  9. @Test
  10. public void testSendMessage() throws IOException, TimeoutException {
  11. // 1.建立连接
  12. ConnectionFactory factory = new ConnectionFactory();
  13. // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
  14. factory.setHost("192.168.200.130");
  15. factory.setPort(5672);
  16. factory.setVirtualHost("/");
  17. factory.setUsername("itcast");
  18. factory.setPassword("123321");
  19. // 1.2.建立连接
  20. Connection connection = factory.newConnection();
  21. // 2.创建通道Channel
  22. Channel channel = connection.createChannel();
  23. // 3.创建队列
  24. String queueName = "simple.queue";
  25. channel.queueDeclare(queueName, false, false, false, null);
  26. // 4.发送消息
  27. String message = "hello, rabbitmq!";
  28. channel.basicPublish("", queueName, null, message.getBytes());
  29. System.out.println("发送消息成功:【" + message + "】");
  30. // 5.关闭通道和连接
  31. channel.close();
  32. connection.close();
  33. }
  34. }

基本消息队列的消息接收流程:

1. 建立connection

2. 创建channel

3. 利用channel声明队列

4. 定义consumer的消费行为handleDelivery()

5. 利用channel将消费者与队列绑定

  1. package cn.itcast.mq.helloworld;
  2. import com.rabbitmq.client.*;
  3. import java.io.IOException;
  4. import java.util.concurrent.TimeoutException;
  5. public class ConsumerTest {
  6. public static void main(String[] args) throws IOException, TimeoutException {
  7. // 1.建立连接
  8. ConnectionFactory factory = new ConnectionFactory();
  9. // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
  10. factory.setHost("192.168.200.130");
  11. factory.setPort(5672);
  12. factory.setVirtualHost("/");
  13. factory.setUsername("itcast");
  14. factory.setPassword("123321");
  15. // 1.2.建立连接
  16. Connection connection = factory.newConnection();
  17. // 2.创建通道Channel
  18. Channel channel = connection.createChannel();
  19. // 3.创建队列
  20. String queueName = "simple.queue";
  21. channel.queueDeclare(queueName, false, false, false, null);
  22. // 4.订阅消息
  23. channel.basicConsume(queueName, true, new DefaultConsumer(channel){
  24. @Override
  25. public void handleDelivery(String consumerTag, Envelope envelope,
  26. AMQP.BasicProperties properties, byte[] body) throws IOException {
  27. // 5.处理消息
  28. String message = new String(body);
  29. System.out.println("接收到消息:【" + message + "】");
  30. }
  31. });
  32. System.out.println("等待接收消息。。。。");
  33. }
  34. }

3、SpringAMQP

        SpringAMQP是基于RabbitMQ封装的一套模板,并且利用SpringBoot对其实现了自动装配,使用起来非常方便。

        SpringAmqp的官方地址:https://spring.io/projects/spring-amqp

        SpringAMQP提供了三个功能:

                自动声明队列、交换机及其绑定关系;

                封装了RabbitTemplate工具,用于发送消息;

                基于注解的监听器模式,异步接收消息;

3.1、简单队列Demo

 

3.1.1、引入依赖
  1. <!--AMQP依赖,包含RabbitMQ-->
  2. <dependency>    
  3. <groupId>org.springframework.boot</groupId>    
  4. <artifactId>spring-boot-starter-amqp</artifactId>
  5. </dependency>
3.1.2、添加配置

首先配置MQ地址,在publisher服务的application.yml中添加配置:

  1. spring:
  2. rabbitmq:
  3. host: 192.168.200.130 # 主机名
  4. port: 5672 # 端口
  5. virtual-host: / # 虚拟主机
  6. username: itcast # 用户名
  7. password: 123321 # 密码
3.1.3、发送消息/接收消息 Demo

然后在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:

  1. package cn.itcast.mq.spring;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.amqp.rabbit.core.RabbitTemplate;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import org.springframework.test.context.junit4.SpringRunner;
  7. @SpringBootTest
  8. public class SpringAmqpTest {
  9. @Autowired
  10. private RabbitTemplate rabbitTemplate;
  11. @Test //不要导错包,用比较长的import org.junit.jupiter.api.Test;
  12. public void testSimpleQueue() {
  13. // 队列名称
  14. String queueName = "simple.queue";
  15. // 消息
  16. String message = "hello, spring amqp!";
  17. // 发送消息:此处并不会自动创建队列
  18. rabbitTemplate.convertAndSend(queueName, message);
  19. }
  20. }

#如果没有创建指队列:simple.queue,可在浏览器管理端中手动创建

然后在consumer服务中新建一个类SpringRabbitListener,代码如下:

  1. package cn.itcast.mq.listener;
  2. import org.springframework.amqp.rabbit.annotation.RabbitListener;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. public class SpringRabbitListener {
  6. @RabbitListener(queues = "simple.queue")
  7. public void listenSimpleQueueMessage(String msg) {
  8. System.out.println("spring 消费者接收到消息:【" + msg + "】");
  9. }
  10. }
3.2、工作队列 Demo

 3.2.1、消息发送

  1. /**
  2. * workQueue
  3. * 向队列中不停发送消息,模拟消息堆积。
  4. */
  5. @Test
  6. public void testWorkQueue() throws InterruptedException {
  7. // 队列名称
  8. String queueName = "simple.queue";
  9. // 消息
  10. String message = "hello, message_";
  11. for (int i = 1; i <= 50; i++) {
  12. // 发送消息
  13. rabbitTemplate.convertAndSend(queueName, message + i);
  14. Thread.sleep(20);
  15. }
  16. }

3.2.2、消息接收

1、注释掉之前接收消息的监听器
2、注意两个消费者的消费速度不一致,模拟消息分配方式(是否平分呢?)

  1. //@RabbitListener(queues = "simple.queue")
  2. //public void listenSimpleQueueMessage(String msg) {
  3. // System.out.println(msg);
  4. //}
  5. @RabbitListener(queues = "simple.queue")
  6. public void listenWorkQueue1(String msg) throws InterruptedException {
  7. System.out.println(LocalTime.now() + "消费者1:" + msg);
  8. Thread.sleep(20);
  9. }
  10. @RabbitListener(queues = "simple.queue")
  11. public void listenWorkQueue2(String msg) throws InterruptedException {
  12. System.err.println(LocalTime.now() + "消费者2:" + msg);
  13. Thread.sleep(200);
  14. }

看控制台

 

 这是什么情况呢,这得让1能者多劳啊,这就得修改配置文件了:

  1. spring:
  2. rabbitmq:
  3. host: 192.168.200.130 # 主机名
  4. port: 5672 # 端口
  5. virtual-host: / # 虚拟主机
  6. username: itcast # 用户名
  7. password: 123321 # 密码
  8. listener:
  9. simple:
  10. prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
3.3、发布/订阅

 

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给exchage(交换机)

  • Consumer:消费者,与以前一样,订阅队列,没有变化

  • Queue:消息队列也与以前一样,接收消息、缓存消息。

  • Exchange:交换机(消息路由)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或将消息丢弃。到底如何操作,取决于Exchange的类型。

    Exchange有以下3种类型:

    • Fanout:广播,将消息交给所有绑定到交换机的队列

    • Direct:定向,把消息交给符合指定routing key 的队列

    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

3.3.1、来整一个Topic的  Demo

        Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符

举例:
item.#:能够匹配item.spu.insert 或者 item.spu
item.*:只能匹配item.spu

  • Queue1:绑定的是china.# ,因此凡是以 china.开头的routing key 都会被匹配到。包括china.news和china.weather

  • Queue2:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配。包括china.news和japan.news

3.3.2、赶紧上案例(好嘞)

 在consumer服务的SpringRabbitListener中添加方法:(消息接收)

  1. package cn.itcast.mq.listener;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.amqp.rabbit.annotation.Exchange;
  4. import org.springframework.amqp.rabbit.annotation.Queue;
  5. import org.springframework.amqp.rabbit.annotation.QueueBinding;
  6. import org.springframework.amqp.rabbit.annotation.RabbitListener;
  7. import org.springframework.stereotype.Component;
  8. @Slf4j
  9. @Component
  10. public class SpringRabbitListener {
  11. @RabbitListener(bindings = @QueueBinding(
  12. value = @Queue(name = "topic.queue1"),
  13. exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
  14. key = "china.#"))
  15. public void listenTopicQueue1(String msg){
  16. System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
  17. }
  18. @RabbitListener(bindings = @QueueBinding(
  19. value = @Queue(name = "topic.queue2"),
  20. exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
  21. key = "#.news"))
  22. public void listenTopicQueue2(String msg){
  23. System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
  24. }
  25. }

在publisher服务的SpringAmqpTest类中添加测试方法:(消息发送)

  1. /**
  2. * topicExchange
  3. */
  4. @Test
  5. public void testSendTopicExchange() {
  6. // 交换机名称
  7. String exchangeName = "itcast.topic";
  8. // 消息
  9. String message = "喜报!孙悟空大战哥斯拉,胜!";
  10. // 发送消息
  11. rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
  12. }

4、MQ的一些常见问题

4.1、MQ-如何保证消息不丢失呢
4.1.1、具体问题

消息从发送,到消费者接收,会经历多个过程:

 

其中的每一步都可能导致消息丢失,常见的丢失原因包括:

        发送时丢失:

                生产者发送的消息未送达exchange;

                消息到达exchange后未到达queue;

        MQ宕机,queue将消息丢失;

        consumer接收到消息后未消费就宕机;

4.1.2、解决方案

        主要从三个层面考虑

        第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据

返回结果有两种方式:

        publisher-confirm,发送者确认
                消息成功投递到交换机,返回ack
                消息未投递到交换机,返回nack
        publisher-return,发送者回执
                消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。

        第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化

1、交换机持久化

默认的是非持久化的、SpringAMQP中可以通过代码指定交换机持久化

  1. @Bean
  2. public DirectExchange simpleExchange(){
  3. // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
  4. return new DirectExchange("simple.direct", true, false);
  5. }

2、队列持久化

默认的是非持久化的、SpringAMQP中可以通过代码指定交换机持久化

        事实上,默认情况下,由SpringAMQP声明的队列都是持久化的

  1. @Bean
  2. public Queue simpleQueue(){
  3. // 使用QueueBuilder构建队列,durable就是持久化的
  4. return QueueBuilder.durable("simple.queue").build();
  5. }

3、消息持久化

利用SpringAMQP发送消息时,可以设置消息的属性(MessageProperties),指定delivery-mode:

  • 1:非持久化:MessageDeliveryMode.NON_PERSISTENT

  • 2:持久化:MessageDeliveryMode.PERSISTENT

 

        第三个是开启消费者确认机制为auto,由spring确认消息处理成功后完成ack;

修改配置文件

  1. spring:
  2. rabbitmq:
  3. listener:
  4. simple:
  5. acknowledge-mode: auto # 根据异常自动ack

        第四消费失败重试机制,我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理

实现也是修改配置文件

  1. spring:
  2. rabbitmq:
  3. listener:
  4. simple:
  5. retry:
  6. enabled: true # 开启消费者失败重试
  7. initial-interval: 1000 # 初识的失败等待时长为1
  8. multiplier: 2 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
  9. max-attempts: 3 # 最大重试次数
  10. stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
4.2、MQ-如何保证消息不重复消费呢

这种问题其实这个就是典型的幂等的问题,比如,redis分布式锁、数据库的锁都是可以解决

举个例子:

        我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息。这样就重复消费了

        因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了

4.3、MQ-死信交换机-怎么实现延迟队列呢

 

延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。

如果消息超时未消费就会变成死信,在RabbitMQ中如果消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。

我记得RabbitMQ还有一种方式可以实现延迟队列,在RabbitMQ中安装一个死信插件,这样更方便一些,我们只需要在声明交互机的时候,指定这个就是死信交换机,然后在发送消息的时候直接指定超时时间就行了,相对于死信交换机+TTL要省略了一些步骤

4.4、有大量消息堆积怎么办呢

        解决消息堆积有三种思路:

第一:提高消费者的消费能力 ,可以使用多线程消费任务

第二:增加更多消费者,提高消费速度 

​           使用工作队列模式, 设置多个消费者消费消费同一个队列中的消息

第三:扩大队列容积,提高堆积上限 

        可以使用RabbitMQ惰性队列,

  1. @RabbitListener(queuesToDeclare = @Queue(
  2. name = "lazy.queue",
  3. durable = "true",
  4. //开启惰性队列
  5. arguments = @Argument(name = "x-queue-mode", value = "lazy")
  6. ))
  7. public void listLazyQueue(String msg) {
  8. log.info("接收到 lazy.queue 的消息:{}", msg);
  9. }

                惰性队列的好处主要是

                ①接收到消息后直接存入磁盘而非内存

                ②消费者要消费消息时才会从磁盘中读取并加载到内存

                ③支持数百万条的消息存储

4.5、数据丢失了可咋办呀

一般工作里,都会搭建集群的

        我们当时项目在生产环境下,使用的集群,当时搭建是镜像模式集群,使用了3台机器。

        镜像队列结构是一主多从,所有操作都是主节点完成,然后同步给镜像节点,如果主节点宕机后,镜像节点会替代成新的主节点,不过在主从同步完成前,主节点就已经宕机,可能出现数据丢失

        那出现丢数据怎么解决呢:我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致。并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/598208
推荐阅读
相关标签
  

闽ICP备14008679号