RabbitMQ

RabbitMQ

初始MQ

同步通讯和异步通讯

img

同步调用的问题

微服务间基于Feign的调用就属于同步方式,存在一些问题。

img

  • 耦合度高
  • 性能下降
  • 资源浪费
  • 级联失败

异步调用方案

异步调用常见实现就是事件驱动模式

img

优势:

  • 服务解耦
  • 性能提升,吞吐量提高

img

  • 没有强依赖关系
  • 流量削峰

缺点:

  • 依赖于Broker的可靠性、安全性、吞吐能力
  • 架构复杂了,业务没有明显的流程线,不好追踪管理

什么是MQ

MQ,中文是消息队列,字面来看即使存放消息队列。也就是事件驱动架构中的Broker。

img

RabbitMQ快速入门

RabbitMQ概述

RabbitMQ是基于Erlang语言开发的开源消息中间件

官网:Messaging that just works — RabbitMQ

img

RabbitMQ部署指南

1.单机部署

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

1.1.下载镜像

方式一:在线拉取

docker pull rabbitmq:3-management

方式二:从本地加载

在课前资料已经提供了镜像包:

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

docker load -i mq.tar

1.2.安装MQ

执行下面的命令来运行MQ容器:

docker run \
 -e RABBITMQ_DEFAULT_USER=xcxc \
 -e RABBITMQ_DEFAULT_PASS=xcxc666 \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:latest
1.3.浏览器访问15672端口

img

如果无法访问,则需要开启管理插件

# 进入mq的内部
docker exec -it mq /bin/bash
# 开启管理插件
rabbitmq-plugins enable rabbitmq_management

2.集群部署

接下来,我们看看如何安装RabbitMQ的集群。

2.1.集群分类

在RabbitMQ的官方文档中,讲述了两种集群的配置方式:

  • 普通模式:普通模式集群不进行数据同步,每个MQ都有自己的队列、数据信息(其它元数据信息如交换机等会同步)。例如我们有2个MQ:mq1,和mq2,如果你的消息在mq1,而你连接到了mq2,那么mq2会去mq1拉取消息,然后返回给你。如果mq1宕机,消息就会丢失。
  • 镜像模式:与普通模式不同,队列会在各个mq的镜像节点之间同步,因此你连接到任何一个镜像节点,均可获取到消息。而且如果一个节点宕机,并不会导致数据丢失。不过,这种方式增加了数据同步的带宽消耗。

我们先来看普通模式集群。

2.2.设置网络

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

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

192.168.150.101 mq1
192.168.150.102 mq2
192.168.150.103 mq3

并在每台机器上测试,是否可以ping通对方:

常见消息模型

  • 基本消息队列

img

  • 工作消息队列

img

发布订阅,又根据交换机类型不同分为三种:

  • 广播

img

  • 路由

img

  • 主题

img

HelloWorld案例

官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:

  • publisher:消息发布者,将消息发送到队列queue
  • queue:消息队列,负责接受并缓存消息
  • consumer:订阅队列,处理队列中的消息

img

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

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 利用channel向队列发送消息

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

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 定义consumer的消费行为handleDelivery()
  5. 利用channel将消费者与队列绑定

SpringAMQP

什么是SpringAMQP

img

官网:https://spring.io/projects/spring-amqp

案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能

发送消息

引入依赖

      <!--AMQP依赖,包含RabbitMQ-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>

在publisher服务编写application.yml,添加mq连接信息

spring:
  rabbitmq:
    host: 192.168.72.133
    port: 5672
    username: xcxc
    password: xcxc666
    virtual-host: /

编写测试类

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;

/**
 * @author xc
 * @date 2023/5/8 7:00
 */
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSend(){
        String queueName = "simple.queue";
        String message = "hello xc";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

接受消息

  • 引依赖
  • 配yml
  • 编写配置类
package cn.itcast.mq.listener;

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

/**
 * @author xc
 * @date 2023/5/8 7:08
 */
@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者接受到simple.queue的消息:【"+msg+"】");
    }
}

Work Queue工作队列

img

案例:模拟WorkQueue,实现一个队列绑定多个消费者

  1. 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
	@Test
    public void testSend1() throws InterruptedException {
        String queueName = "simple.queue";
        String message = "hello xc-";
        for (int i = 0; i < 50; i++) {
            Thread.sleep(20);
            rabbitTemplate.convertAndSend(queueName,message+i);
        }
    }
  1. 在consumer服务中定义两个消息监听者,都监听simple.queue
package cn.itcast.mq.listener;

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

/**
 * @author xc
 * @date 2023/5/8 7:08
 */
@Component
@Slf4j
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue1(String msg) throws InterruptedException {
        log.info("消费者1接受到simple.queue的消息:【"+msg+"】");
        Thread.sleep(20);
    }

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue2(String msg) throws InterruptedException {
        log.error("消费者2接受到simple.queue的消息:【"+msg+"】");
        Thread.sleep(200);
    }
}
  1. 消费者1每秒处理50条消息,消费者2每秒处理10条消息

消息预取限制

logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.72.133
    port: 5672
    username: xcxc
    password: xcxc666
    virtual-host: /
    listener:
      simple:
        prefetch: 1 # 每次只取一条消息,处理完进行下一条消息

发布(Publish)、订阅(Subscribe)

发布订阅与之前的区别就是允许将同一个消息发送给多个消费者。实现方式是加入了exchange

img

常见的exchange类型包括:

  • Fanout:广播
  • Direct:路由
  • Topic:话题

注意 :exchange负责消息路由,而不是存储,路由失败则消息丢失

发布订阅-Fanout Exchange

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue

img

案例:利用SpringAMQP演示FanoutExchange的使用

  1. 在consumer服务中,利用代码声明队列,交换机,并将两者绑定
package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author xc
 * @date 2023/5/8 7:48
 */
@Configuration
public class FanoutConfig {
    /**
     * 声明交换机
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("xc.fanout");
    }
    /**
     * 声明队列
     */
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }
    /**
     * 绑定交换机与队列
     */
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}
  1. 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
package cn.itcast.mq.listener;

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

/**
 * @author xc
 * @date 2023/5/8 7:08
 */
@Component
@Slf4j
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue1")
    public void listenFanoutQueue1(String msg) throws InterruptedException {
        log.info("消费者1接受到simple.queue1的消息:【"+msg+"】");
    }

    @RabbitListener(queues = "simple.queue2")
    public void listenFanoutQueue2(String msg) throws InterruptedException {
        log.error("消费者2接受到simple.queue2的消息:【"+msg+"】");
    }
}
  1. 在publisher中编写测试方法,向itcast.fanout发送消息
    @Test
    public void testSendExchange() throws InterruptedException {
        String exchangeName = "xc.fanout";
        String message = "hello xc-";
        for (int i = 0; i < 50; i++) {
            Thread.sleep(20);
            rabbitTemplate.convertAndSend(exchangeName,"",message+i);
        }
    }

发布订阅-DirectExchange

Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的Routingkey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

img

绑定交换机和队列

package cn.itcast.mq.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
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;

/**
 * @author xc
 * @date 2023/5/8 7:08
 */
@Component
@Slf4j
public class SpringRabbitListener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "xc.direct",type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void listenSimpleQueue1(String msg) {
        log.info("消费者接受到direct.queue1的消息:【"+msg+"】");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "xc.direct",type = ExchangeTypes.DIRECT),
            key = {"red","yellow"}
    ))
    public void listenSimpleQueue2(String msg) {
        log.info("消费者接受到direct.queue2的消息:【"+msg+"】");
    }
}

发布测试

    @Test
    public void testSendDirectExchange() throws InterruptedException {
        String exchangeName = "xc.direct";
        String message = "hello xc-";
        for (int i = 0; i < 50; i++) {
            rabbitTemplate.convertAndSend(exchangeName,"red",message+i);
        }
    }

发布订阅-TopicExchange

TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割

Queue与Exchange指定BindingKey时可以使用通配符:

#:代表0个或多个单词

*:代指一个单词

img

案例:利用SpringAMQP演示TopicExchange的使用

绑定交换机和队列

package cn.itcast.mq.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
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;

/**
 * @author xc
 * @date 2023/5/8 7:08
 */
@Component
@Slf4j
public class SpringRabbitListener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "xc.topic",type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenTopicQueue1(String msg) {
        log.info("消费者接受到direct.queue1的消息:【"+msg+"】");
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "xc.topic",type = ExchangeTypes.TOPIC),
            key = "*.news"
    ))
    public void listenTopicQueue2(String msg) {
        log.info("消费者接受到direct.queue2的消息:【"+msg+"】");
    }
}

发布测试

    @Test
    public void testSendTopicExchange1() throws InterruptedException {
        String exchangeName = "xc.topic";
        String message = "hello red";
            rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
    }
    @Test
    public void testSendTopicExchange2() throws InterruptedException {
        String exchangeName = "xc.topic";
        String message = "hello red";
            rabbitTemplate.convertAndSend(exchangeName,"am.news",message);
    }
    @Test
    public void testSendTopicExchange3() throws InterruptedException {
        String exchangeName = "xc.topic";
        String message = "hello red";
            rabbitTemplate.convertAndSend(exchangeName,"china.weahter",message);
    }

消息转换器

发送消息

引入依赖

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

声明bean

package cn.itcast.mq;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

因为消息转换是在底层做的,所以消息直接发就行

接受消息

引入依赖

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

声明bean

package cn.itcast.mq;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

因为消息转换是在底层做的,所以消息直接收就行

定义收消费者

    @RabbitListener(queues = "object.queue")
    public void listenObjQueue(Map<String,Object> msg){
        System.out.println(msg);
    }
;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

因为消息转换是在底层做的,所以消息直接收就行

定义收消费者

    @RabbitListener(queues = "object.queue")
    public void listenObjQueue(Map<String,Object> msg){
        System.out.println(msg);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值