RabbitMQ从基础到进阶

MQ的优势

应用解耦 异步提速 消峰填谷

JMS 是java的一种规范 (java message service)

AMQP协议:一种高级消息队列的协议

RabbitMQ的安装与配置

第一种方式: Docker (推荐)

1 拉取镜像

docker pull rabbitmq:3.8-management

2 运行容器

docker run \
-e RABBITMQ_DEFAULT_USER=root  \
-e RABBITMQ_DEFAULT_PASS=root  \
-v mq-plugins:/plugins  \
--name mq  \
--hostname mq  \
-p 15672:15672  \
-p 5672:5672  \
-d  \
rabbitmq:3.8-management

3 访问rabbitmq所在的主机的15672端口就可以进入管理界面

第二种方式: 安装包安装(麻烦,需要的自行百度)

SpringBoot整合RabbitMQ

1 导入依赖

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

2 添加配置

spring:
  rabbitmq:
    port: 5672
    username: root
    password: root
    virtual-host: /
    host: 192.168.200.10

hello world (需要到管理界面先创建队列,demo.queue1)

不管发送消息还是接收消息,都需要先导入依赖 ,指定mq的地址

发消息:

1 注入RabbitTemplate

2 使用convertAndSend方法发送

第一个参数 队列名

第二个参数 消息的内容

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void sendMsg() {
    rabbitTemplate.convertAndSend("demo.queue1","123");
}

接收消息 ,创建一个web项目

1 新建一个监听器类,并交由spring核心容器管理

2 新建一个方法用来接收消息。使用@RabbitListener

3 运行项目

@Slf4j
@Component
public class DemoListener {

    @RabbitListener(queues = "demo.queulle1")
    public void consumer(String  msg) {
        log.info("msg ===========> {}",msg);
    }

}

工作队列案例 (多个消费者一起消费队列中的消息)

需要添加配置

spring:
  rabbitmq:
    port: 5672
    username: root
    password: root
    virtual-host: /
    host: 192.168.200.10
    listener:
      simple:
        prefetch: 1  #让每个消费者,只有消费完才可以继续取消息

消费者:


@RabbitListener(queues = "work.queue")
public void listenWorkQueue1(String  msg) throws InterruptedException {
    log.info("listener1 ===========> {} {}",msg, LocalTime.now());
    Thread.sleep(20);
}


@RabbitListener(queues = "work.queue")
public void listenWorkQueue2(String  msg) throws InterruptedException {
    log.error("listener2 ===========> {} {}",msg,LocalTime.now());
    Thread.sleep(200);
}


生产者:

@Test
public void workQueue() throws InterruptedException {
    for (int i = 1; i <= 50; i++) {
        rabbitTemplate.convertAndSend("work.queue","workQueue " + i);
        Thread.sleep(20);
    }
}

发布订阅案例

简单得案例,跟工作队列模型都是一条消息只能有一个消费者消费,消费了就没了。

发布订阅模型是可以同时发送给多个消费者。实现方式是加入了路由器。

常见的exchange有:

fanout (广播) 会发送给所有跟这个路由器绑定的队列

direct (路由) 发送给绑定为key的队列。

topic (主题) 比路由模式多了通配符

 fanout 代码实现

发送消息:

@Test
public void demoFanout() throws InterruptedException {
    rabbitTemplate.convertAndSend("demo.fanout","","hello every one");
}

接收消息:

@RabbitListener(bindings = @QueueBinding(
        value = @Queue("fanout.queue1"),
        exchange = @Exchange(name = "demo.fanout",type = ExchangeTypes.FANOUT)
))
public void listenDemoQueue1(String  msg) {
    log.info("queue1 ===========> {}",msg);
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue("fanout.queue2"),
        exchange = @Exchange(name = "demo.fanout",type = ExchangeTypes.FANOUT)
))
public void listenDemoQueue2(String msg) {
    log.info("queue2 ===========> {}",msg);
}

direct 代码实现

发送消息:

@Test
public void demoDirect() throws InterruptedException {
    //rabbitTemplate.convertAndSend("demo.direct","red","hello ,how are you");
    //rabbitTemplate.convertAndSend("demo.direct","blue","hello ,how are you");
    rabbitTemplate.convertAndSend("demo.direct","all","hello ,how are you");
}

接收消息:

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1"),
        exchange = @Exchange(name = "demo.direct",type = ExchangeTypes.DIRECT),
        key = {"blue","all"}
))
public void listenDemoQueue1(String  msg) {
    log.info("bule,all ===========> {}",msg);
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue("direct.queue2"),
        exchange = @Exchange(name = "demo.direct",type = ExchangeTypes.DIRECT),
        key = {"red","all"}
))
public void listenDemoQueue2(String msg) {
    log.info("red,all ===========> {}",msg);
}

Topic (跟direct 相比,多了通配符)

消费者:

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue1"),
        exchange = @Exchange(name = "demo.topic",type = ExchangeTypes.TOPIC),
        key = {"china.#"}
))
public void listenTopicQueue1(String  msg) {
    log.info("china ===========> {}",msg);
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue("topic.queue2"),
        exchange = @Exchange(name = "demo.topic",type = ExchangeTypes.TOPIC),
        key = {"#.news.#"}
))
public void listenDemoQueue2(String msg) {
    log.info("news ===========> {}",msg);
}

生产者:

@Test
public void demoTopic() throws InterruptedException {
    //rabbitTemplate.convertAndSend("demo.topic","china.news.test","china news");
    //rabbitTemplate.convertAndSend("demo.topic","news.test","china news");
    rabbitTemplate.convertAndSend("demo.topic","china.test","china news");
}

消息转换器

底层的对象序列化是使用jdk的默认的。

我们不用jdk的默认,用json格式

实现步骤:

1 导入依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

2 定义bean

@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}

消费者跟生产序列器需要使用一样的

MQ的常见问题与解决方式

1 - 消息可靠性 确保发送的消息至少被消费一次

2 - 延迟消息问题 如何实现消息的延迟投递

3 - 消息堆积问题 如何解决百万消息的堆积,无法及时消费的问题

4 - 高可用问题 搭建集群

消息可靠性问题

可能丢失:

1 解决方法:发送者确认机制

发送者 -> exchange

exchange -> queue

2 解决方法: 消息持久化

消息到达queue,然后宕机了,mq是基于内存存储,启动的时候消息丢失了。

3 解决方案 : 消费者确认机制

消费者拿到消息未及时消费 宕机了

解决生产者到队列之间可能存在丢失的问题 -- 生产者确认机制

RabbitMQ提供了一种publisher confirm机制来避免消息发送到MQ的过程中丢失。

publisher-comfirm 发送者确认

当消息投递到交换机,返回ack

当消息未投递到交换机,返回nack

publisher-return 发送者回执

当消息到达交换机但未到达队列,就会在返回ack的同时返回路由到队列失败的原因。

SpringAMQP实现生产者确认机制的实现:

1 添加配置 需要先开启异步方式、开启回执功能、开启路由失败的策略

spring:
  rabbitmq:
    port: 5672
    username: root
    password: root
    virtual-host: /
    host: 192.168.200.10
    publisher-confirm-type: correlated  #异步方式
    publisher-returns: true  #开启publish-return功能
    template: 
      mandatory: true #开启路由失败时的策略,false是直接丢弃信息

2 配置ReturnCallback 每个RabbitTemplate只能配置一个

我们要实现当spring核心容器创建完之后对RabbitTemplate设置ReturnCallback。

@Slf4j
@Configuration
public class RabbitMQConfig implements ApplicationContextAware {

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnCallback((message, errCode, errInfo, exchange, key) -> log.error("消息发送失败,{} 原因: {}  exchange: {} key: {}  消息内容: {} ",errCode,errInfo,exchange,key,message.toString()));
    }
}

3 发送消息时配置ComfirmCallback

@Slf4j
@SpringBootTest(classes = PublishApplication.class)
@RunWith(SpringRunner.class)
public class AdvanceDemoRabbitMQTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMsgAndComfirmCallback() {
        CorrelationData correlationData = new CorrelationData(IdUtil.simpleUUID());
        String id = correlationData.getId();
        correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable throwable) {
                log.error("id: {} 消息发送异常,msg : {}",throwable.getMessage());
            }

            @Override
            public void onSuccess(CorrelationData.Confirm confirm) {
                if(confirm.isAck()) {
                    log.info("id: {}  消息发送成功!",id);
                }else {
                    log.error("id: {} 消息发送失败 msg: {}",id,confirm.getReason());
                }
            }
        });
        rabbitTemplate.convertAndSend("demo.exchange","","hello!",correlationData);
    }

}

消息持久化

消息发送到mq之后,如果mq宕机,那么消息会丢失。所以需要让mq的消息持久化

使用SpringAMQP默认交换机跟队列还有消息都是持久化的。

交换机跟队列的配置

//durable = "true"  持久化,autoDelete = "false" 不自动删除
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "demo.queue",durable = "true",autoDelete = "false"),
        exchange = @Exchange(name = "demo.exchange",type = ExchangeTypes.FANOUT,durable = "true",autoDelete = "false")
))

消息的发送默认也是持久化

消费者确认机制

RabbitMQ支持消费者确认机制,当消费者成功处理消息之后会向RabbitMQ发送ack回执,MQ收到ack回执才会删除该条消息。

SpringAMQP允许配置三种模式:

none : 关闭ack,MQ默认消息发送出去就是被处理了,会直接删除这条消息。

manual : 手动ack,需要在消息消费之后手动调用api发送ack给MQ。

auto : 自动ack , 在消费消息的代码中如果没有出现异常,代码结束之后就会自动向MQ发送ack,如果出现异常即出现nack。(推荐使用)

配置方法:

spring:
  rabbitmq:
    port: 5672
    username: root
    password: root
    virtual-host: /
    host: 192.168.200.10
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto  #自动ack

失败重试机制

如果代码本身有问题,消费者确认机制会导致代码陷入死循环,给MQ带来很多压力。

解决方式:可以利用Spring的retry机制,在本地进行重试。

使用方式:

1 配置 开启retry、配置每次失败的等待时间、最多重试次数等

spring:
  rabbitmq:
    port: 5672
    username: root
    password: root
    virtual-host: /
    host: 192.168.200.10
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: manual  #手动ack
        retry:
          enabled: true  #开启retry
          initial-interval: 1000 #初始的失败等待时间
          multiplier: 1 #计算下次失败的等待时间
          max-attempts: 3 #最多重试次数
          stateless: true #true 状态  false 有状态 ,如果业务中包含事务,那这边改成false

使用RabbitMQ实现延迟任务

死信交换机

成为死信的三种情况:

1 被消费者返回nack或则reject。

2 消息超时未消费。

3 队列满了,最早进入队列的消息也会变成死信。

给队列绑定死信交换机:

TTL

time-to-live : 超时时间

ttl 队列跟消息都可以设置,设置在队列上从消息到达队列开始进行计时,当计时结束消息还未被消费就会变成死信,设置在消息上从消息发送出的那一个开始倒计时,如果时间到还未被消,也会变成死信。

 基于死信交换机跟TTL实现延迟:

 1 - 需要先给队列指定死信交换机,路由key,设置ttl超时时间。

//==========================配置队列超时时间==============================

@Bean
public DirectExchange ttlExchange() {
    return new DirectExchange("ttl.direct");
}

@Bean
public Queue ttlQueue() {
    return QueueBuilder.durable("ttl.queue")
            .deadLetterExchange("dl.direct")
            .deadLetterRoutingKey("dl")
            .ttl(10_000)
            .build();
}

@Bean
public Binding ttlBinding() {
    return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}

//======================================================================

安装DelayExchange插件

链接:https://pan.baidu.com/s/1wVl3lFKdQJ64UpbTUd7BUg?pwd=3okb 
提取码:3okb

上传插件: 

docker volume inspect mq-plugins

查看数据卷位置

将插件上传到这个数据卷的目录

安装插件:

docker exec -it mq bash
rabbitmq-plugins enable rabbitmq_delayed_message_exchange    

使用插件:

在管理界面创建一个交换机。

发送消息的时候指定x-delay

SpringAMQP使用延迟队列插件:

消费者:主要多了设置交换机属性 delayed = "true"

@Slf4j
@Component
public class DelayListener {
    
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue",durable = "true"),
            exchange = @Exchange(name = "delay.direct",delayed = "true"),
            key = "delay"
    ))
    public void listenDelayQueue(String msg) {
        log.info(msg);
    }
    
}

发消息: 主要多了设置Message的头 setHeader("x-delay", "5000")

@Test
public void testSendDelay() {
    Message msg = MessageBuilder.withBody("helloDelay".getBytes()).setDeliveryMode(MessageDeliveryMode.PERSISTENT).setHeader("x-delay", "5000").build();
    CorrelationData correlationData = new CorrelationData(IdUtil.simpleUUID());
    correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
        @Override
        public void onFailure(Throwable throwable) {

        }

        @Override
        public void onSuccess(CorrelationData.Confirm confirm) {
            if(confirm.isAck()) {
                log.info("{} 发送成功",correlationData.getId());
            }
        }
    });
    rabbitTemplate.convertAndSend("delay.direct","delay",msg,correlationData);
}

消息堆积问题

消息堆积问题: 当消费者的消费速度小于生生产者的生产速度,就有可能出现消息堆积问题。

1 - 增加消费者。

2 - 提高消费者的性能(使用多线程)。

3 - 增加队列的容量,提高堆积的上限。

惰性队列

特征:

1 - 接收到消息直接写入磁盘,而非内存。

2 - 消费者需要消费消息才会从磁盘中读取并加载内存

3 - 支持百万条的消息存储。

优点:

1 - 存储的消息容量大。

2 - 没有间接性的page-out,性能比较稳定

缺点:

1 - 基于磁盘存储,消息的读写时间较长.

2 - 性能受限于磁盘IO。

惰性队列的开启方式

在运行中的队列改成惰性队列

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}'  --apply-to queues

使用SpringAMQP声明多线队列的俩种方式:

@Bean的方式

@Bean
public Queue lazyQueue() {
    return QueueBuilder.durable("queue").lazy().build();
}

注解方式:

@RabbitListener(bindings = @QueueBinding(
        value = @Queue("lazy.queue"),
        exchange = @Exchange(name = "lazy.direct",type = ExchangeTypes.DIRECT),
        key = "lazy",
        arguments = @Argument(name = "x-queue-mode",value = "lazy")
))
public void listenLazyQueue(String msg) {
    log.info(msg);
}

高可用问题 -- MQ集群

普通集群

分布式集群,将队列分布到各个节点,从而提升整个集群的功能。

在普通集群中交换机的资源是共享的,还有队列的元信息(相当于应用了队列的地址)

实现方式: 以目录/usr/local/mq为例

1 - 获取cookie(集群中MQ传递消息就是通过cookie)

docker exec -it mq cat /var/lib/rabbitmq/.erlang.cookie  #返回一个cookie

2 - 准备集群配置

2 - 1 rabbitmq.conf

loopback_users.guest = false
listeners.tcp.default = 5672
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config
cluster_formation.classic_config.nodes.1 = rabbit@mq1
cluster_formation.classic_config.nodes.2 = rabbit@mq2
cluster_formation.classic_config.nodes.3 = rabbit@mq3

2 - 2 创建一个文件,记录获取到的cookie,并设置权限为只读。

# 创建cookie文件
touch .erlang.cookie
# 写入cookie
echo "获取到的cooike" > .erlang.cookie
# 修改cookie文件的权限
chmod 600 .erlang.cookie

3 - 创建集群每个mq的目录,并将配置文件以及cookie文件拷贝进去。

mkdir mq1 mq2 mq3

cp rabbitmq.conf mq1
cp rabbitmq.conf mq2
cp rabbitmq.conf mq3
cp .erlang.cookie mq1
cp .erlang.cookie mq2
cp .erlang.cookie mq3

4 - 启动集群

创建网络:

docker network create mq-net

运行容器:

docker run -d --net mq-net \
-v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq1 \
--hostname mq1 \
-p 8071:5672 \
-p 8081:15672 \
rabbitmq:3.8-management



docker run -d --net mq-net \
-v ${PWD}/mq2/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq2 \
--hostname mq2 \
-p 8072:5672 \
-p 8082:15672 \
rabbitmq:3.8-management



docker run -d --net mq-net \
-v ${PWD}/mq3/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq3 \
--hostname mq3 \
-p 8073:5672 \
-p 8083:15672 \
rabbitmq:3.8-management

镜像集群

官方文档地址:

Classic Queue Mirroring — RabbitMQ

主从集群普通集群的基础上,添加了主从备份功能(也就是队列也共享),提高集群的数据可用性。

但是主从同步存在丢失数据的可能。

镜像集群是在普通集群的基础上去加配置。

镜像配置的三种模式

exactly 精确模式(推荐) ha-params的值是等于镜像节点加主节点

rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'

"^two\." 这个策略的意思是以two开头的队列都会创建一个镜像节点。

 all (无需指定全部都是主节点的镜像节点)

rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'

nodes (需要指定镜像节点)

rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'

仲裁队列(推荐使用)

底层采用Raft协议确保主从数据的一致性。用来替换镜像队列。

主从同步效果跟镜像队列一致。(配置简单)

但是主从同步是基于Raft协议,强一致性。

实现方式:

 使用SpringAMQP创建一个仲裁队列

@Bean
public Queue quorumQueue() {
    return QueueBuilder.durable("quorum.queue").quorum().build();
}

使用java连接MQ集群

spring:
  rabbitmq:
    port: 5672
    username: root
    password: root
    virtual-host: /
    addresses: 192.168.200.10:8071,192.168.200.10:8072,192.168.200.10:8073
    publisher-confirm-type: correlated #异步方式等待MQ返回,并执行confirmCallback
    publisher-returns: true #开启publisher-returns
    template:
      mandatory: true  #开启失败策略,当消息发送失败就会接收回执,并执行处理代码

集群扩容

1)启动一个新的MQ容器:

docker run -d --net mq-net \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq4 \
--hostname mq5 \
-p 8074:15672 \
-p 8084:15672 \
rabbitmq:3.8-management

2)进入容器控制台:

docker exec -it mq4 bash

3)停止mq进程

rabbitmqctl stop_app

4)重置RabbitMQ中的数据:

rabbitmqctl reset

5)加入mq1:

rabbitmqctl join_cluster rabbit@mq1

6)再次启动mq进程

rabbitmqctl start_app

增加仲裁队列副本

我们先查看下quorum.queue这个队列目前的副本情况,进入mq1容器:

docker exec -it mq1 bash

执行命令:

rabbitmq-queues quorum_status "quorum.queue"

结果:

 现在,我们让mq4也加入进来:

rabbitmq-queues add_member "quorum.queue" "rabbit@mq4"

结果:

 再次查看:

rabbitmq-queues quorum_status "quorum.queue"

查看控制台,发现quorum.queue的镜像数量也从原来的 +2 变成了 +3:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

isuweijie

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值