Spring Cloud(四)Docker和服务异步通信

Docker

大型项目组件较多,运行环境也较为复杂,部署往往会遇到一系列的问题:

  • 依赖关系复杂,容器出现兼容性问题
  • 开发、测试、生产环境有差异

这就需要用到Docker技术来解决上面的问题。

初识Docker

Docker如何解决依赖的兼容问题:

  • 将应用的Libs(函数库)、Deps(依赖)、配置和应用一起打包。
  • 将每个应用放到一个隔离容器去运行,避免互相干扰。

那么,不同的操作系统下,函数库可能不被当前的操作系统所支持,Docker又是如何解决的呢?

先看操作系统结构:

在这里插入图片描述
操作系统的内核与硬件交互,提供操作硬件的指令,而系统应用封装内核指令为函数,便于程序员进行调用,用户程序则基于系统函数库实现功能。

例如,Ubuntu和CentOs都是基于Linux内核,由于系统应用不同,提供的函数库有差异。

Docker巧妙地将用户程序和所需要调用的系统函数库一起打包,当运行到不同的操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行,而不需要再通过系统应用来在内核中运行了。

Docker和虚拟机

虚拟机是在操作系统中模拟硬件设备,然后运行另一个操作系统。比如在Windows平台上运行CentOs系统,这样就可以运行任意的CentOs应用了。

在这里插入图片描述
Docker与虚拟机的差异:

  • docker是一个系统进程,虚拟机是在操作系统中的操作系统。
  • docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般。

镜像和容器

镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。

容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器作隔离,对外不可见。

Docker和DockerHub

  • DockerHub:DockerHub是一个Docker镜像的托管平台,这样的平台称为Docker Registry。
  • 国内也有类似于DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等。

Docker架构

Docker是一个CS架构的程序,由两部分组成:

  • 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等。
  • 客户端(client):通过命令或RestAPI向Docker服务端发送指令,可在本地或远程向服务端发送指令。

Docker的安装

Docker分为CE和EE两大版本。

CE即社区版(免费,支持周期7个月),EE即企业版,强调安全,支持周期24个月。

Docker CE分为stable test和nightly三个更新频道。

CentOS安装Docker

安装yum工具:

yum install -y yum-utils \
           device-mapper-persistent-data \
           lvm2 --skip-broken

更新本地镜像源:

# 设置docker镜像源
yum-config-manager \
    --add-repo \
    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
    
sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo

yum makecache fast

输入命令,安装docker:

yum install -y docker-ce

由于在自己本机的虚拟机上运行docker,所以推荐关闭防火墙食用,这样就无需开放端口让windows端访问。但如果是云服务器上运行docker,千万不要关闭防火墙,而是选择去开放端口进行访问。

配置镜像加速,可以参考阿里的镜像加速文档:

https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

Docker的基本操作

镜像操作

镜像相关命令

  • 镜像名称一般分两部分组成:[repository]:[tag]
  • 在没有指定tag时,默认是latest,即代表最新版本的镜像。

在这里插入图片描述

容器相关命令

在这里插入图片描述

案例一:创建运行一个Nginx容器

步骤一:去docker hub查看Nginx的容器运行命令

docker run --name containerName -p 80:80 -d nginx

命令解读:

  • docker run:创建并运行一个容器。
  • –name:给容器起一个名字,比如叫做mn。
  • -p:将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口。
  • -d:后台运行容器。
  • nginx:镜像名称,例如nginx。

查看容器日志的命令:docker logs,添加-f参数可以持续查看日志。

案例二:进入nginx容器,修改HTML页面内容

步骤一:进入容器

docker exec -it mn bash

命令解读:

  • docker exec:进入容器内部,执行一个命令。
  • -it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互。
  • mn:要进入的容器名称。
  • bash:进入容器后执行的名称,bash是一个Linux终端交互命令。

步骤二:进入nginx的HTML所在目录 /usr/share/nginx/html

cd /usr/share/nginx/html

步骤三:修改index.html的内容

sed -i 's#Welcome to nginx#Hello World#g' index.html

注:

  • 删除容器的命令是docker rm,不能删除运行中的容器,若要进行强制删除,需要添加-f参数。

  • exec命令可以进入容器中修改文件,但是在容器内修改文件是不被推荐的。

数据卷

在上个案例中,我们不难发现容器与数据存在耦合:

  • 不便于修改:当我们要修改Nginx的html内容时,需要进入容器内部进行修改,很不方便。
  • 数据不可复用:在容器内的修改对外不可见,所有修改对新创建的容器是不可复用的。
  • 升级维护困难:数据在容器内,如果要升级容器必然要删除旧容器,所有数据都跟着删除了。

数据卷(Volume)是一个虚拟目录,指向宿主机系统中的某个目录。
在这里插入图片描述
容器内的数据路径通过挂载到虚拟的数据卷上,数据卷再映射到宿主机中的某个目录上。我们可以对真实目录中的文件进行修改,无需再去进入容器中修改数据;同时,只要容器挂载到数据卷上,就可以共享里面的内容;当版本升级要删除低版本后,我们也只需要让升级版本后的容器挂载该数据卷,之前的数据就不会丢失。通过将容器挂载到数据卷上,就完美地解决了容器和数据卷耦合的问题。

操作数据卷

数据卷操作的基本语法:

docker volume [COMMAND]

docker volume命令是数据卷操作,根据命令后跟随的command来确定下一步操作:

  • create 创建一个volume
  • inspect 显示一个或多个volume的信息
  • ls 列出所有的volume
  • prune 删除未使用的volume
  • rm 删除一个或多个指定的volume

挂载数据卷

在创建容器时,可以通过-v参数来挂载一个宿主机目录到某个容器目录:

docker run --name mn -v html:/root/html -p 80:80 nginx

案例:创建一个nginx容器,修改容器内的html目录内的index.html内容

步骤一:创建容器并挂载数据卷到容器内的HTML目录

docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx

步骤二:进入html数据卷所在位置,并修改HTML内容

# 查看html数据卷所在位置
docker volume inspect html
# 进入目录
cd /var/lib/docker/volume/html/_data
# 修改文件
vim index.html

注:如果容器创建时挂载的数据卷不存在,那么这个数据卷会被自动创建出来

在创建容器时,还可以通过-v参数挂载一个宿主机文件到容器文件

docker run --name mysql -e MYSQL_ROOT_PASSWORD=123 \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25

在这里插入图片描述
数据卷挂载与目录直接挂载:

  • 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找。
  • 目录挂载耦合度高,需要我们自己进行管理,不过目录容易寻找查找。

DockerFile自定义镜像

镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。

在这里插入图片描述

Dockerfile

Dockerfile是一个文本文件,其中包含一个个的指令,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层Layer。

指令说明示例
FROM指定基础镜像FROM centos:6
ENV设置环境变量,可在后面指令使用ENV key value
COPY拷贝本地文件到镜像的指定目录COPY ./mysql-5.7.rpm /tmp
RUN执行Linux的shell命令,一般是安装过程的命令RUN yum install gcc
EXPOSE指定容器运行时监听的端口,是给镜像使用者看的EXPOSE 8080
ENTRYPOINT镜像中应用的启动命令,容器运行时调用ENTRYPOINT java -jar xx.jar

案例:基于java:8-alpine镜像,将一个Java项目构建为镜像。
编写Dockerfile文件:

  1. 基于java:8-alpine作为基础镜像
  2. 将app.jar拷贝到镜像中
  3. 暴露端口
  4. 编写入口ENTRYPOINT

使用docker build命令构建镜像
使用docker run创建容器并运行

# 指定基础镜像
FROM java:8-alpine
COPY ./docker-demo.jar /tmp/app.jar
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
docker build -t javaweb:2.0 .
docker run --name web -p 8090:8090 -d javaweb:2.0

DockerCompose

DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,无需手动一个个创建和运行容器。

Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。

案例:将之前的cloud-demo微服务集群利用DockerCompose部署

docker-compose.yml文件:

version: "3.2"

services:
  nacos:
    image: nacos/nacos-server
    environment:
      MODE: standalone
    ports:
      - "8848:8848"
  mysql:
    image: mysql:5.7.25
    environment:
      MYSQL_ROOT_PASSWORD: 123
    volumes:
      - "$PWD/mysql/data:/var/lib/mysql"
      - "$PWD/mysql/conf:/etc/mysql/conf.d/"
  userservice:
    build: ./user-service
  orderservice:
    build: ./order-service
  gateway:
    build: ./gateway
    ports:
      - "10010:10010"

进入到要部署的目录利用命令进行部署:

docker-compose up -d

Docker镜像仓库

镜像仓库有公共的和私有的两种形式:

  • 公共仓库:例如Docker官方的Docker-Hub,国内也有一些云服务商提供类似于Docker-Hub的公开服务,例如网易云镜像服务、阿里云镜像服务等。
  • 除了使用公开仓库外,用户还可以在本地搭建私有Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。

Docker官方提供的Docker Registry

是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。

搭建命令如下:

docker run -d \
    --restart=always \
    --name registry	\
    -p 5000:5000 \
    -v registry-data:/var/lib/registry \
    registry

在私有镜像仓库推送或拉取镜像

推送镜像到私有镜像服务必须先tag,步骤如下:

  1. 重新tag本地镜像,名称前缀为私有仓库的地址
docker tag nginx:latest 192.168.36.136:8080/nginx:1.0
  1. 推送镜像
docker push 192.168.36.136:8080/nginx:1.0
  1. 拉取镜像
docker pull 192.168.36.136:8080/nginx:1.0

服务异步通信

初识MQ

同步通讯

什么是同步呢?我们可以浅析为同时。打电话就是一个同步通讯的例子,建立通讯后,我们就要保持通话,我所说的话,对方是第一时间听见的,是实时性的。而且与此同时,如果有人要给我打电话,是无法建立通讯的。

在这里插入图片描述
同步调用的问题:
在这里插入图片描述
比如说应用中有一个支付服务,需要去调用订单服务、仓储服务、短信服务等等…在同步通讯的情况下,支付服务在调用期间什么都不能做,只能等待调用的服务执行完毕通知支付服务,这样一来,如果调用其他服务过多,就会导致很高的延迟,在高并发情况下,这样肯定是不行的。

  1. 耦合度高:每次加入新的需求,都要修改原来的代码。
  2. 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
  3. 资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源。
  4. 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障。

但同样的,一切事物都有其正反面,同步通讯也有他的优点,比如说时效性强,可以立即得到结果。

异步通讯

那什么是异步呢?我们使用聊天软件进行聊天就是异步,当我发送消息后,对方不是第一时间能看到且作出回应的,时效性不好。但与此同时,我可以向多个人发送消息,而自身并不受影响。
在这里插入图片描述

异步调用方案

事件驱动模式

在这里插入图片描述
采用异步通讯的事件驱动模式时,不再是由支付服务调用其他服务了。而是当用户支付完成时,支付服务向Broker事件代理者发布支付成功事件,Broker向其他服务通知用户支付成功,让它们执行自己的业务,与此同时,支付服务直接返回给用户,无需等待其他服务执行完毕。

事件驱动优势如下

  1. 服务解耦:当要增加服务时,无需改动支付服务中的代码,只需关注新增服务的业务代码,并订阅Broker即可。
  2. 性能提升、吞吐量提高:无需调用其他服务,也就是说无需等待其他服务是否执行完毕。采用同步时我们需要500ms,但采用事件驱动只需要支付服务自身的耗时+发布事件的耗时。
  3. 服务没有强依赖,不担心级联失败问题:如果下游的服务出现了问题,是不会影响到支付服务的,因为异步调用无需关注其他的服务,只关注业务自身,解决了资源浪费的问题。
  4. 流量削峰:随着用户的增多,业务量也会随之增加。比如下游服务每秒只能处理一个请求,但在某一时刻突然有5000个请求过来,Broker会起到一个缓冲的作用,将这些请求拦截下来,根据下游业务自身的能力进行分配。如此一来,一个高的并发就被砍平了变成低并发,就完成了流量消峰。

在这里插入图片描述
异步通讯的缺点

  1. 依赖于Broker的可靠性、安全性和吞吐能力,若Broker扛不住高并发自身挂了,整个服务也就崩了。
  2. 架构更加复杂,业务没有明显的流程线,不好进行追踪管理。

如何选择同步通讯和异步通讯

  • 如果需要其他服务返回结果,建议使用同步通讯。
  • 如果对高并发、吞吐量的要求较高,建议使用异步通讯。

MQ

MQ(Message Queue),中文即消息队列,字面上是存放消息的队列。也就是事件驱动架构中的Broker。
在这里插入图片描述

RabbitMQ

RabbitMQ的结构和概念
在这里插入图片描述

  • channel:操作MQ的工具。
  • exchange:路由消息到队列中。
  • queue:缓存消息。
  • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组。
常见消息模型

在这里插入图片描述

HelloWorld

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

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

Spring AMQP

AMQP:Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。

Spring AMQP:基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。

Hello World

生产者

步骤一:引入amqp的starter依赖

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

步骤二:配置application.yml文件中RabbitMQ的地址

spring:
  rabbitmq:
    host: 192.168.36.136
    port: 5672
    virtual-host: /
    username: itcast
    password: 123321

步骤三:编写测试类,利用RabbitTemplate的convertAndSend方法

package cn.itcast.mq.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
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;

/**
 * @version 1.0
 * @Description
 * @Author 月上叁竿
 * @Date 2022-05-22 13:35
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testSimpleQueue(){
        String queueName = "simple.queue";
        String message = "hello, spring amqp!";
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

消费者

步骤一:引入amqp的starter依赖

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

步骤二:配置application.yml的RabbitMQ地址

spring:
  rabbitmq:
    host: 192.168.36.136
    port: 5672
    virtual-host: /
    username: itcast
    password: 123321

步骤三:定义类,添加@Component注解,类中声明方法并添加@RabbitListener注解,方法参数接收消息

package cn.itcast.mq;

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

/**
 * @version 1.0
 * @Description
 * @Author 月上叁竿
 * @Date 2022-05-22 13:57
 **/
@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg){
        System.out.println("Spring消费者接收到消息 : [" + msg + "]");
    }
}

注:消息一旦消费就会从队列中删除,RabbitMQ没有消息回溯功能。

Work Queue

Work queue(工作队列),可以提高消息的处理速度,避免队列消息堆积。
在这里插入图片描述

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

  1. 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
  2. 在consumer服务中定义两个消息监听者,都监听simple.queue队列
  3. 消费者1每秒处理50条消息,消费者2每秒处理10条消息

步骤1:生产者循环发送消息到simple.queue

在publisher服务中添加一个测试方法,循环发送50条消息到simple.queue队列

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

步骤2:编写两个消费者,都监听simple.queue

在consumer服务中添加一个消费者,也监听simple.queue:

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

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

运行代码后,我们发现所运行的结果与我们所预想的不一致,消费者1接收了25条奇数消息,而消费者2接收了25条偶数消息。这是因为RabbitMQ的消费预取机制,当消息到达队列后,channel会对消息进行预取。

我们通过修改application.yml,设置preFetch的值即可控制预取消息的上限:

spring:
  rabbitmq:
    host: 192.168.36.136
    port: 5672
    virtual-host: /
    username: itcast
    password: 123321
    listener:
      simple:
        prefetch: 1

注:多个消费者绑定到一个队列,同一条消息只能被一个消费者处理。

发布、订阅

发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange,常见exchange类型包括:

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

在这里插入图片描述
需要注意的是exchange负责消息路由,而不是存储,路由失败则消息丢失。

Fanout Exchange

Fanout Exchange会将接收到的消息广播到每一个跟其绑定的queue。

在这里插入图片描述

案例:利用Spring AMQP演示Fanout Exchange的使用

  1. 在consumer服务中,利用代码声明队列、交换机,并将两者绑定。
  2. 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
  3. 在publisher中编写测试方法,向itcast.fanout发送消息

在这里插入图片描述
步骤1:在consumer服务中声明Exchange、Queue、Binding

创建一个类,添加@Configuration注解,并声明FanoutExchange、Queue和绑定关系对象Bindling:

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;

/**
 * @version 1.0
 * @Description
 * @Author 月上叁竿
 * @Date 2022-05-22 15:17
 **/
@Configuration
public class FanoutConfig {
    // itcast.fanout
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }

    // fanout.queue1
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

    // fanout.queue2
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    // 绑定队列1到交换机
    @Bean
    public Binding fanoutBinding(Queue fanoutQueue1,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    // 绑定队列2到交换机
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}

步骤2:在consumer服务声明两个消费者

在consumer服务的SpringRabbitListener类中,添加两个方法,分别监听fanout.queue1和fanout.queue2:

    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg) {
        System.out.println("消费者接收到fanout.queue1消息 : [" + msg + "]");
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg) {
        System.out.println("消费者接收到fanout.queue2消息 : [" + msg + "]");
    }

步骤3:在publisher服务发送消息到FanoutExchange

在publisher服务的SpringAmqpTest类中添加测试方法:

    @Test
    public void testFanoutExchange(){
        // 交换机名称
        String exchangeName = "itcast.fanout";
        // 消息
        String message = "Hello everyone";

        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }

交换机的作用:

  1. 接收publisher发送的消息
  2. 将消息按照规则路由到与之绑定的队列
  3. 不能缓存消息,路由失败则消息丢失
  4. FanoutExchange会将消息路由到每个绑定的队列
Direct Exchange

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

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

在这里插入图片描述

案例:利用Spring AMQP演示DirectExchange的使用

  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey
  2. 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
  3. 在publisher中编写测试方法,向itcast.direct发送消息

步骤1:在consumer服务中编写两个消费者方法,分别监听direct.queue1和direct.queue2,并利用@RabbitListener声明Exchange、Queue、RoutingKey

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void listenDirectQueue1(String msg){
        System.out.println("消费者接收到direct.queue1消息 : [" + msg + "]");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red","yellow"}
    ))
    public void listenDirectQueue2(String msg){
        System.out.println("消费者接收到direct.queue2消息 : [" + msg + "]");
    }

步骤2:在publisher服务中发送消息到DirectExchange

    @Test
    public void testDirectExchange(){
        // 交换机名称
        String exchangeName = "itcast.direct";
        // 消息
        String message = "I'm direct exchange!";

        rabbitTemplate.convertAndSend(exchangeName, "yellow", message);
    }

Direct Exchange和Fanout Exchange的差异:

  • Fanout Exchange将消息路由给每一个与之绑定的队列。
  • Direct Exchange根据RoutingKey判断路由给哪个队列。
  • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似。
Topic Exchange

Topic Exchange与Direct Exchange类似,区别在于routingKey必须是多个单词的列表,并且以.分割。Queue与Exchange指定BindingKey时可以使用通配符:

  • #:代指0个或多个单词
  • *:代指一个单词

在这里插入图片描述

案例:利用Spring AMQP演示TopicExchange的使用

  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey
  2. 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
  3. 在publisher中编写测试方法,向itcast.topic发送消息

步骤1:在consumer服务中编写两个消费者方法,分别监听topic.queue1和topic.queue2,并利用@RabbitListener声明Exchange、Queue、RoutingKey

    @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("消费者接收到direct.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("消费者接收到direct.queue2消息 : [" + msg + "]");
    }

步骤2:在publisher服务中发送消息到TopicExchange

    @Test
    public void testTopicExchange(){
        // 交换机名称
        String exchangeName = "itcast.topic";
        // 消息
        String message = "路飞战胜凯多!和之国开国了!";

        rabbitTemplate.convertAndSend(exchangeName, "ONE PIECE.news", message);
    }
消息转换器

在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送

在consumer服务中声明object.queue:

@Bean
public Queue objectMessageQueue(){
	return new Queue("object.queue");
}

在publisher中发送消息以测试:

@Test
public void testSendMap() {
	Map<String, Object> msg = new HashMap<>();
	msg.put("name", "柳岩");
	msg.put("age", 36);
}

在Web管理端查看object.queue中的message:
在这里插入图片描述
发现Map对象被转换成了Java序列化对象。

这是因为Spring对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,是基于JDK的ObjectOutputStream来完成序列化的。

JDK自带的序列化效率不高并且部分版本存在安全漏洞,所以我们进行消息转换,如果要修改只需要定义一个MessageConverter类型的Bean即可,推荐使用JSON方式进行序列化:

步骤1:引入jackson依赖

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

步骤2:在publisher服务中声明MessageConverter

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

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

consumer中上面的步骤照旧,紧接着定义一个消费者,用于监听object.queue队列并消费消息:

    @RabbitListener(queues = "object.queue")
    public void listenObjectQueue(Map<String, Object> msg){
        System.out.println("消费者接收到object.queue的消息:[" + msg + "]");
    }

此时Web管理界面查看object.queue的消息:

在这里插入图片描述
需要注意的是,发送方和接收方必须使用相同的MessageConverter

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值