RabbitMQ的使用和常见问题

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

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

docker run \
 -e RABBITMQ_DEFAULT_USER=itcast \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 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

package cn.itcast.mq.helloworld;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.200.130");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123321");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");

        // 5.关闭通道和连接
        channel.close();
        connection.close();

    }
}

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

1. 建立connection

2. 创建channel

3. 利用channel声明队列

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

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

package cn.itcast.mq.helloworld;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.200.130");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123321");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}

3、SpringAMQP

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

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

        SpringAMQP提供了三个功能:

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

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

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

3.1、简单队列Demo

 

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

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

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

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

package cn.itcast.mq.spring;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test //不要导错包,用比较长的import org.junit.jupiter.api.Test;
    public void testSimpleQueue() {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, spring amqp!";
        // 发送消息:此处并不会自动创建队列
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

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

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

package cn.itcast.mq.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg) {
        System.out.println("spring 消费者接收到消息:【" + msg + "】");
    }
}
3.2、工作队列 Demo

 3.2.1、消息发送

/**
     * workQueue
     * 向队列中不停发送消息,模拟消息堆积。
     */
@Test
public void testWorkQueue() throws InterruptedException {
    // 队列名称
    String queueName = "simple.queue";
    // 消息
    String message = "hello, message_";
    for (int i = 1; i <= 50; i++) {
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message + i);
        Thread.sleep(20);
    }
}

3.2.2、消息接收

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

//@RabbitListener(queues = "simple.queue")
//public void listenSimpleQueueMessage(String msg) {
//    System.out.println(msg);
//}

@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
    System.out.println(LocalTime.now() + "消费者1:" + msg);
    Thread.sleep(20);
}

@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
    System.err.println(LocalTime.now() + "消费者2:" + msg);
    Thread.sleep(200);
}

看控制台

 

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

spring:
  rabbitmq:
    host: 192.168.200.130 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码
    listener:
      simple:
        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中添加方法:(消息接收)

package cn.itcast.mq.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SpringRabbitListener {

    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue1"),
        exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
        key = "china.#"))
    public void listenTopicQueue1(String msg){
        System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
    }


    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue2"),
        exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
        key = "#.news"))
    public void listenTopicQueue2(String msg){
        System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
    }
}

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

/**
     * topicExchange
     */
@Test
public void testSendTopicExchange() {
    // 交换机名称
    String exchangeName = "itcast.topic";
    // 消息
    String message = "喜报!孙悟空大战哥斯拉,胜!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}

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中可以通过代码指定交换机持久化

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

2、队列持久化

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

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

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

3、消息持久化

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

  • 1:非持久化:MessageDeliveryMode.NON_PERSISTENT

  • 2:持久化:MessageDeliveryMode.PERSISTENT

 

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

修改配置文件

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 根据异常自动ack

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

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

实现也是修改配置文件

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

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

举个例子:

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

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

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

 

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

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

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

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

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

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

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

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

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

        可以使用RabbitMQ惰性队列,

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

                惰性队列的好处主要是

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

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

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

4.5、数据丢失了可咋办呀

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

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值