Java微服务基础(上)

263 阅读1小时+

那么到了这一章,我们就去学习微服务了

微服务技术栈导学

首先,微服务技术并不等同于Spring Cloud,微服务技术其实是分布式架构的一种,所谓的分布式结构,就是要对服务做拆分,而拆分的过程中有各种各样的问题需要解决,而SpringCloud只是解决了服务拆分时的服务质量问题,而其他问题则没有提供对应的解决方案,因此一个完整的微服务技术,不仅仅包含Spring Cloud

那么接下来我们对微服务技术栈做一个整体的介绍

image-20220819003547950

首先,微服务要做的第一件事情就是拆分,因为以往的项目随着业务越来越复杂,耦合情况越来越严重,因此大型的互联网项目都必须做拆分。

而微服务在拆分时会根据功能模块,将一个单体的项目拆分许多独立的项目,每个项目完成一部分业务功能,未来就对这些项目进行独立开发和部署,这些项目就共同构成了服务集群

往往我们的一个请求是需要调用服务集群里的多个项目共同协作才能完成的,为了便于程序员去记录和维护这些项目,因此在微服务中一定有注册中心这个组件,用于记录每个项目的ip、端口以及其能完成的功能,程序员需要调用另外的服务时,也只需要去注册中心就可以轻松寻找到对应信息完成调用。同时,随着服务越来越多,每个项目又都有自己的配置文件,这样一个个配置当然太麻烦,因此还会提供配置中心这一组件,可以在该组件中统一管理服务集群中所有项目的配置

当微服务运行起来时,还需要服务网关,服务网关的作用有二,一是拦截没有资格的请求,二是接受到请求之后由它来调用正确的服务集群里的具体项目,同时还具有负载均衡的作用。

请求到服务集群之后查询对应的数据库(虽然我们图里只画了一个数据库,但是当然的,我们的项目肯定是不止一个数据库的),为了解决高并发带来的效率降低问题,我们会在数据库前加入分布式缓存,先去请求缓存中的数据,如果缓存中没有数据,我们再去请求数据库,同时将数据库数据加入到缓存中。同时有一些复杂的搜索请求,即使是缓存也做不来,因此我们还会加入分布式搜索在数据库前用于完成用户的复杂搜索请求

最后在微服务中还需要一个异步通信的消息队列,该消息队列会通知对应要执行对应请求的项目去执行自己的功能,这样各个项目就可以独立运作,不必说等到前面的运作完后面的再运作,可以提高我们的微服务效率和并发,在一些秒杀的高并发场景下很是管用

当然,我们这么大的项目,如果出了问题肯定也是需要维护的,为了便于维护,微服务还会有分布式日志服务系统监控链路追踪这两个组件,前者会统一记录服务集群中每个项目的日志,而后者则会实时监控每一个项目的运行情况,包括其负载情况或者是运行情况,一旦某个项目发生问题,就直接定位到对应的项目,便于我们的维护

image-20220819010043964

而向这么大的项目,肯定就不能跟我们以前一样手动去部署打包了,所以在微服务里,我们往往需要自动化的部署我们会通过Jenkins对我们的项目进行自动化编译,而通过docker进行打包形成一些镜像,再通过kubernetes或者是RANCHER等技术实现自动化部署,这一整个部署打包的流程就称之为持续集成

结合前面的微服务技术,再加上持续集成,这才是完整的微服务

认识微服务

服务架构演变

先来看看普通的单体架构存在的问题

image-20220819084331787

我们所有的大型互联网项目,都必须要做拆分,将我们的项目进行拆分能带来许多好处

image-20220819084513920

然而同时拆分我们的项目也会带来许多问题,为了解决这些问题因此也出现了许多对应的技术,但当属微服务技术是最火热最好的一种

image-20220819084702460

微服务是一种经过良好架构设计的分布式架构方案,其具有单一职责,面向服务,自治以及隔离性强的特点

image-20220819085014444

微服务技术对比

微服务这种方案需要技术框架来落地,全球各地的互联网公司都有自己的微服务落地技术,在国内,最知名的就是SpringCloud和阿里巴巴的Dubbo

我们先来看看三种不同的微服务技术的对比

image-20220819090014851

Dubbo是早期的技术,对现在而言当然是不够完善的,现在国内SpringCloudAlibaba是越来越火,我们接下来的课程都按照这个技术进行展开学习

而当我们实际到了企业中,我们要接手的项目采用的微服务技术最可能是这四种技术中的一种

image-20220819090424864

SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架,其中集成了各种微服务组件,并基于SpringBoot实现了组件的自动装配,提供了良好的开箱即用的体验,从而将微服务推广开来

image-20220819090629757

每一个SpringCloud版本都有对应的SpringBoot兼容版本,其对应的版本情况如下,左边是SpringCloud版本,右边是SpringBoot版本

image-20220819090728436

案例Demo

先来看看微服务拆分时的注意事项

image-20220819091207726

案例介绍

image-20220819091332492

接着我们就导入我们的数据,创建对应的工程还有对应的数据库,这里发生了一些小插曲值得一提

首先是我们的之前的瑞吉外卖的数据库被人整蛊了,导致我们的数据库直接无了,之所以会发生这种情况是因为我们直接把数据库的连接和密码放到我们的工程中了,然后又将工程分享出去,这件事情提醒我们开源也有风险,要做好个人信息的安全工作,开源的项目里要除去可能发生安全问题的数据信息

其次是我们启动Demo时报出了java: java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor (in unnamed module @0x5d0这个错误,我们参考这个网址解决了问题blog.csdn.net/weixin_5417…,简单来说就是我们的lombok版本不符合,将lom替换成如下版本即可解决问题

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

接着我们进入网页进行对应的测试,能够确认到我们的项目是可以运行的

最后我们可以做一个总结

image-20220819102648055

服务远程调用

那么假设我们需要一个根据订单id查询订单信息,同时还要将所属的用户信息一起返回,如果我们要完成这个功能,我们要怎么做呢?

一个简单的想法当然是直接改造我们的订单代码,令其查询用户的数据库,得到结果之后返回,但是这样就不符合我们微服务里拆分的定义了,我们微服务里一个模块只能查询自己的数据库的,那我们要怎么办呢?

image-20220819103122829

我们可以在我们的订单模块中,同样对用户模块发起一个查询请求,获取到用户模块查询的结果再封装到我们的订单模块的信息中返回即可

image-20220819103226630

要完成这个需求,首先我们要注册RestTemplate,其实就返回这个对象并让Spring管理,我们直接在启动类中写入这行代码即可

/**
 * 创建RestTemplate并注入Spring容器
 * @return
 */
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

然后我们在Service层中写入我们的代码如下,我们首先注入我们的RestTemplate对象,然后我们自己构造我们的动态请求地址,再利用该对象发送地址,调用getForObject()方法,需要传入url地址和指定返回的类型的字节码文件,其会自动将查询的数据转换成指定的对象,然后我们将该对象封装到我们的返回结果中即可

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起http请求,查询用户
        // 2.1 url路径
        String url = "http://localhost:8081/user/"+order.getUserId();
        // 2.2 发送http请求,实现远程调用
        User user = restTemplate.getForObject(url,User.class);
        //3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

那么最后我们来做一个总结

image-20220819105245192

提供者与消费者

image-20220819105350587

image-20220819105440526

Eureka

Eureka介绍

image-20220819131549543

image-20220819131653116

image-20220819131716865

我本想说点啥,结果图里面把我要说的都说完了,那我讲个几把,自己看吧

搭建Eureka服务

先来看看我们的步骤

image-20220819131937407

我们这里需要创建一个全新的服务模块,其步骤如下

image-20220819132042267

那么接着我们就来做,但是中间发生了一些意外,我只能说是真几把折磨,学习技术真的是,打代码半小时,调试费半天,恶心恶心

首先遇到的问题是我们的每次试图创建新的maven项目时报出Error adding module to project: null错误,去搜索引擎上找到对应的解决方案blog.csdn.net/weixin_4599…,但是屁用没有,照样报出这个错误

然后我们选择创建Springboot项目的方式来避开这个错误,同时将内部的pom文件替换成课程中的pom文件即可

然后我们正式来搭建我们的Eureka服务,首先我们引入对应的项目配置

<!--eureka服务端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

最后提一下,我们这里的Eureka服务是继承了最开始我们的cloud-demo的依赖的,所以我们在eureka-server中不需要引入其他的Springboot组件了,因为其父类依赖已经有了,没必要多此一举

然后我们在resource文件夹中创建yml文件,写入其配置信息如下

server:
  port: 10086 # 服务端口
spring:
  application:
    name: eurekaserver
eureka:
  client:
    service-url: # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

之前我们我们启动的时候是报了这个异常的

Description:

Failed to bind properties under 'eureka.client.service-url' to java.util.Map<java.lang.String, java.lang.String>:

Reason: No converter found capable of converting from type [java.lang.String] to type [java.util.Map<java.lang.String, java.lang.String>]

Action:

  Update your application's configuration

其实这个错误报的意思就是咱们的配置文件,也就是yml文件内部的配置出错了,一看果然出错了,我们忘了填写defaultZone标签了,正确填写即可使用

然后我们来解释下我们这里的配置信息,首先我们的第一个信息当然是设置端口,然后我们后面设置的是我们的eureka的服务名称,接着我们设置的是我们eureka自己的地址信息,这时候可能有同学觉得,自己设置自己是在干嘛?其实这是因为我们的eureka本身也是一个服务来的,也是要被其他服务调用的,所以我们这里要做其自身的注册,以后我们eureka服务可能是很多个,到时候我们这里填写的就是eureka集群的地址,但是我们这里只有一个,所以我们就填写当前的eureka地址

我们在该模块下创建cn.itcast.eureka.EurekaApplication,写入其代码如下

package cn.itcast.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

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

从下面的Services标签点进去就进入该服务的地址,可以看到如下画面,这个就是eureka的管理页面

image-20220819151122213

上面显示的是系统状态,就是我们的系统的一些时间信息之类的

最下面则是一些通用信息,比如内存大小一类的

而中间是最重要的部分,我们来一起看看

image-20220819151408916

Instances currently registered with Eureka意为注册到Eureka上的实例,也就是说,我们的服务有几个注册到了Eureka上,这里就会显示几个,我们只注册了一个,也就是eureka服务自身,因此这里只显示一个

Application显示我们之前写入的名字,而Status则是显示服务对应的ip地址和端口号,以后我们开发就可以从中轻松找到我们所需要的服务和端口号

服务注册

我们之前只是将我们的Eureka注册了自己的服务,接下来我们要将其他服务注册到Eureka中去,先来看看步骤

image-20220823014552305

首先我们在对应的服务中引入Eureka的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

接着我们在配置文件中给其设置eureka客户端的地址以及我们要注册的服务的名字,那么最终我们的配置文件就可以写成下面的形式

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://175.178.114.158:3306/cloud_user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: itheima
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: userservice # user服务的服务名称
mybatis:
  type-aliases-package: cn.itcast.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.itcast: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
eureka:
  client:
    service-url: # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

之后我们再重启服务就可以在Eureka服务的对应网页中查看到我们的服务注册信息了,如果想要注册其他的服务,也是依葫芦画瓢即可

image-20220823015014991

如果我们希望我们的相同的服务不只是开启一个实例,可以使用idea提供的开启多个相同服务的功能,使用方式很简单,首先点击我们想要进行复制的服务,选择Copy Configuration...

接着给复制的服务配置另外一个名字,并且设置新的端口号即可,设置新端口号的命令为-Dserver.port=端口号,-D是参数,server.port则是类似于是yml文件中的设置端口号的命令,我们这里一定要设置新的端口,否则端口号会冲突

image-20220823015248280

服务发现

那么接着我们就要使用Eureka服务来完成来用户请求客户端时的动态请求对应服务并处理请求的功能,先来看看步骤

image-20220823015653044

首先我们将我们OrderService代码的url路径修改,用服务名来代替ip+端口的代码

String url = "http://userservice/user/"+order.getUserId();

然后我们在order-service的项目的启动类的RestTemplate中加入LoadBanlanced注解

/**
 * 创建RestTemplate并注入Spring容器
 * @return
 */
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

然后我们再次测试请求访问我们的对应服务,其就会自动使用对应的服务进行处理了,并且是采用轮询的负载均衡策略,没错,就是这么简单

image-20220823020041204

Ribbon

负载均衡原理

我们上面实现了Eureka服务,并且还调用了我们的负载均衡的策略,但是我们没有具体讲负载均衡策略是如何实现的,那么现在我们就来讲讲

首先Eureka服务的负载均衡策略是通过Ribbon来实现的,其大致的作用是拦截请求,然后从eureka-server中拉取到对应的服务列表,然后执行对应的负载均衡策略,拿到具体的服务地址,然后向服务器发送对应的请求

image-20220823021429249

我们查看其源码,我们可以知道,请求首先会被RibbonLoadBanlancerClient类拦截,然后其通过DynamicServerListLoadBanlancer类来拉取eureka-server中具体的请求列表,接着通过IRule类来决定负载均衡的策略,接着通过该策略具体选择某一个服务,最后修改url,向具体的服务器中发送请求

image-20220823022402425

负载均衡策略

接着我们来讲Ribbon中的具体指定不同的负载均衡策略的方式,首先Ribbon的负载均衡规则由IRule接口定义,每一个子接口都是一种规则

image-20220823023250566

下图是具体的对不同的实现类的解释

image-20220823023556832

我们指定负载均衡的策略有两种方式

image-20220823024430172

第一种是通过代码方式,直接在对应的启动类中定义一个对应的规则的实现类并交由Spring容器管理即可

该方式会令服务内的所有请求都采用同一种负载均衡的方式进行处理,可以简单理解为是全局的负载均衡设置

@Bean
public IRule randomRule(){
    return new RandomRule();
}

第二种格式是通过配置文件方式,在对应的服务中添加具体的配置即可,该配置可以具体设置对应哪个服务的请求对应哪种负载均衡方式,在对应服务中首先设置对应的服务类名,然后指定实现负载均衡的类,最后指定具体类中的负载均衡类的包的地址即可

饥饿加载

Ribbon默认采用懒加载,就是第一次访问时才会去创建LoadBalanceClient,因此第一次的请求时间会很长,如果我们想要解决这个问题,我们可以使用饥饿加载,让我们对应的服务类在项目启动时就创建

image-20220823042627961

我们可以在配置类中实现饥饿加载,将enable设置为true之后,我们还需要具体指定实现饥饿加载的服务名称,如果只需要指定一个,那么可以直接将服务名称指定在clients标签后,而如果需要指定多个实例,则要使用- 服务名称的方式来实现多指定,每一个服务实例都独占一行

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: # 指定饥饿加载的服务名称
      - userservice

image-20220823043036276

Nacos(注册中心)

介绍和安装

接着我们来实现Nacos,其实阿里巴巴的产品,是SpringCloud中的一个组件,相比于Eureka而言,其功能更加丰富,在国内比较受欢迎,因此我们接着就来学习Nacos

image-20220823043713058

要使用Nacos,我们当然要安装,在课程资料里已经提供了对应的安装包,我们直接将其解压到一个没有中文路径的目录即可

Nacos默认的端口号是8848

然后进入其中的bin目录,打开cmd窗口,输入startup.cmd -m standalone即可启动nacos,该启动方式是单机启动,我们其实还有集群启动方式,这个我们以后再讲

然后访问该网址即可进入到nacos的首页http://192.168.110.203:8848/nacos/index.html,默认状态下,其账号密码都是nacos,输入即可进入到对应的nacos的管理页面

快速入门

接着我们学习如何将我们的服务注册到我们的Nacos中

image-20220823045347992

首先我们要在最主要的项目中添加alibaba的管理依赖

<!--nacos的管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

然后我们要删去原来的eureka依赖,接着我们再在对应的具体服务中添加nacos的客户端依赖

<!-- nacos 客户端依赖包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

然后再配置文件中注释掉eureka的配置信息,添加上Nacos的配置信息

image-20220823045403694

由于nacos是由spring管理的,因此我们这里对于nacos的设置直接在spring的配置下进行设置

spring:
  datasource:
    url: jdbc:mysql://175.178.114.158:3306/cloud_user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: itheima
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: userservice # user服务的服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址

最后我们对每一个服务都这么做,然后我们进入对应的nacos网址中就能查看到我们进行注册的服务了

image-20220823050651300

服务分级存储模型

在Nacos中存在服务分级存储模型,换言之,我们会对许多不同机房的相同服务进行划分,将一个服务划分为一个集群,每一个集群下都有对应的服务模块

image-20220823122133740

用户访问时,尽可能先访问其本地的集群内的服务,因为跨集群调用延迟较高,只有当本地集群不可访问时,我们再去调用其他集群

image-20220823122338942

如果我们想要在Nacos中注册服务时同时注册集群,那么就需要进入到对应的服务模块中配置集群名称,然后开启对应我们想要进入到这一集群中的服务,如果想要创建另一个集群,就只需要修改对应的集群名称,然后再开启服务即可

image-20220823122419823

NacosRule负载均衡

然后我们还需要设置订单模块的所属集群,我们这里同样将其设置为杭州

image-20220823123947166

接着我们要实现的事情是,我们的请求应该尽可能先请求本地集群,此时我们可以进入订单服务的配置中,然后给其具体配置如下的内容,代表我们指定的负载均衡规则是优先寻找与自己同集群的服务

image-20220823124307126

如果我们的服务没有同集群的服务,那么其会进行跨集群访问,如果我们进行了跨集群访问,那么其会在对应的接受请求的控制台中,这里是orderservice发出警告信息,警告这里发生了一次跨集群访问

同时,其内部是采用随机负载均衡的方式来挑选同一个集群内的服务的

服务实例的权重设置

当然,实际上,有些时候我们的希望某些服务器承担更多的请求,而某些服务器则处理更少的请求,那么此时我们就可以在Nacos控制台中具体设置对应的权重值来实现这个需求,默认的权重值为1,权重值越小,被访问的概率就越低,如果权重值被设置为0,那么就不可能会被访问

image-20220823130526741

权重值的另外一个作用是可以用于服务的版本升级,如果我们想要给我们的某一个服务进行版本升级,我们可以不断下调其权重值直到为0,之后再进行重启升级,升级完毕之后再不断提高其权重值,测试其是否存在问题,若都没有问题则将权重调整为正常状态,这样就可以做到用户无感知的情况下实现对服务的升级,这种升级方式我们称之为灰度升级

环境隔离

在Nacos的服务存储和数据存储中,存在一个namespace的属性,其用于作最外层的隔离,namespace中又还有具体的分组Group,我们可以将耦合度比较高的服务模块放到一个组中,最终组中存放的东西才是我们的具体服务

image-20220823132339076

如果我们要设置具体的环境隔离,首先我们要进入nacos网址中,选择命名空间,然后创建我们的命名空间即可,我们只需要指定对应的命名空间名字以及其描述,其唯一表示id会采用uuid的方式自动生成

image-20220823132510657

然后我们在对应的配置文件中加入命名空间的id即可,然后如果我们此时再访问order-service,其会因为namespace不同而找不到对应的userservice导致报错,因此如果我们想要服务正确访问对应的另外服务,需要将他们放到同一个命名空间中

image-20220823132901281

nacos注册中心原理

nacos注册中心下同样分为服务消费者与服务提供者,但不同的是,服务消费者会从注册中心拉取对应的服务并保存到缓存中,每个一段时间会定时拉取一次

而注册中心会将服务提供者分为临时实例与非临时实例,临时实例每个一段时间会向注册中心发送心跳监测,如果心跳监测没有得到正确的回应信息,那么就是后面临时实例挂了,nacos会将该服务从注册中心中移除。而如果是非临时实例,则不会采用心跳监测,而会由nacos注册中心主动向服务提供者发送询问,如果询问没有得到正确的回应,则说明对于的非临时实例服务挂了,那么注册中心会立刻向服务消费者推送服务变更的消息,告知服务消费者该服务不可用,同时非临时实例挂了也不会从nacos注册中心中移除,其会标红表示不可用并等待服务恢复,除非我们手动删除

image-20220823140844741

如果我们想要让我们的服务变为非临时实例事务,只需要设置对应的属性ephemeral为false即可,默认为true,即是临时实例

image-20220823140343836

统一配置管理

我们的Nacos下管理多个微服务,我们需要来实现Nacos的统一配置管理,这样便于我们来管理其下具体的服务,并且这个统一配置管理我们想要的效果是应用就能生效的,而不用重启服务,也就是热更新

由于在我们的Nacos中已经提供了统一配置管理服务的组件,因此我们可以直接使用该服务来实现统一配置管理

image-20220823151601058

接着我们来讲解如何在Nacos中添加具体额配置系信息,首先进入配置列表,点击右侧的+号

image-20220823152448061

我们在弹出的表单中需要填写对应的配置信息,首先是配置文件的id,其应该是一个唯一标识,常用的表示方式是[服务名称]-[生产环境].[后缀名],后缀名目前支持yaml和properties

分组我们采用默认分组,具体的配置内容我们不需要全部从我们的原本的配置信息中全部复制粘贴过来,我们只需要复制一部分我们要修改的内容即可

这里我们往其中填入日期格式的新格式的配置信息

image-20220823152650442

微服务配置拉取

前面我们创建了对应的Nacos的配置文件,接着我们来学习如何将该配置文件具体应用到我们的微服务中

首先我们先来看看统一配置管理的步骤,项目启动时要先获得nacos的配置文件的地址从而来读取nacos的配置文件,为了让其能够提前获得nacos的配置文件,我们会体用一个bootstrap.yml的配置文件来获得nacos地址,接着先读取naocs的配置文件,然后读取本地的配置文件,将两个配置文件进行合并,之后应用到服务中

image-20220823153505126

我们要实现微服务配置的拉取,首先要在具体的服务中引入Nacos配置管理客户端的依赖,然后在resource目录中添加一个bootstrap.yml文件,在其中添加对应的配置信息即可

image-20220823153753534

那么我们可以引入其客户端的管理依赖如下

<!--nacos的配置管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

接着我们往其中写入具体的配置信息

spring:
  application:
    name: userservice
  profiles:
    active: dev # 环境
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
      config:
        file-extension: yaml # 文件后缀名

我们往其中写入的内容就是服务名称,nacos的地址、生产环境以及nacos的地址,接着我们从application.yml中文件中除去重复配置的内容

为了能够查看到我们上面配置的信息的内容,我们可以在控制层中创建一个新的方法用于测试

@Value("${pattern.dateformat}")
private String dateformat;

@GetMapping("now")
public String now(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}

我们这里获取到配置中的信息的数据并封装到一个数据中,接着调用该数据返回给页面,最终我们测试也可以正确获得该信息,此时就说明我们的配置过程是没有问题的

image-20220823160620330

配置热更新

接着我们来实现配置的热更新,我们有两种方式可以实现配置的热更新,第一种方式是使用RefreshScope注解,在对应的有Value注解的类上注入该注解即可

image-20220823160908708

第二种方式是使用@ConfigurationProperties注解实现自动刷新

image-20220823161315904

我们首先需要创建一个全新的类,写入其代码如下

@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}

然后我们在对应的类中通过autowire注解实现属性的自动注入,然后调用该属性中的get方法得到想要的数据

@Autowired
private PatternProperties properties;

@GetMapping("now")
public String now(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}

同样可以实现配置的自动更新,我们在实际的开发中,更加推荐使用第二种方式来实现配置的热更新

image-20220823163237408

多环境配置共享

有些时候,有一些完全相同配置我们不必对每一个服务都进行相同的配置,我们只需要将其设置到一个配置文件中即可

微服务启动时会加载多个配置文件,而[spring.application.name].yaml该文件是一定会加载的,因此多环境共享配置可以写入到该文件中

image-20220823163449577

我们可以在配置列表中选择+号,创建userservice.yaml的名称的配置列表,写入指定的共同具有的配置信息

然后我们要进行测试,我们希望能看出效果,因此我们开启两个服务,分别对应两个不同的生产环境,一个是dev,另一个是test,为了让我们的第二个服务的生成环境就是test,我们可以选择对应的服务右键选择Edit Configuration...

image-20220823165849559

找到Active profiles,然后设置其生产环境为我们想要的环境即可,这里我们设置为测试环境

image-20220823165920757

然后我们同时获取两个服务中的两个配置信息值,可以发现日期信息只有一个服务有,另一个服务没有,这是因为配置文件中设定了只有生产环境的服务才会被注入该配置信息,而全局配置信息则是全部都要应用,因此两个都能承接到信息

最后的一个问题就是多服务共享配置的优先级,我们可以直接记忆,优先级从小到大排序就是本地配置<服务名称.yaml<服务名-profile.yaml

image-20220823164907699

nacos集群搭建

生产环境下,nacos一定要部署为集群状态,那么最后我们就来学习nacos的集群搭建

image-20220823170446986

首先我们会有一个nacos的客户端,还拥有三个nacos的节点,给这三个结点做负载均衡的是Nginx

nacos的安装包我们课程资料是已经提供了,是1.4.1版本的

我们首先初始化我们的数据库,创建nacos数据库,然后我们导入下面的SQL创建对应的表

CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) DEFAULT NULL,
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) DEFAULT NULL,
  `c_use` varchar(64) DEFAULT NULL,
  `effect` varchar(64) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  `c_schema` text,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_aggr   */
/******************************************/
CREATE TABLE `config_info_aggr` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  `content` longtext NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_beta   */
/******************************************/
CREATE TABLE `config_info_beta` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_tag   */
/******************************************/
CREATE TABLE `config_info_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_tags_relation   */
/******************************************/
CREATE TABLE `config_tags_relation` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = group_capacity   */
/******************************************/
CREATE TABLE `group_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = his_config_info   */
/******************************************/
CREATE TABLE `his_config_info` (
  `id` bigint(64) unsigned NOT NULL,
  `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL,
  `group_id` varchar(128) NOT NULL,
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL,
  `md5` varchar(32) DEFAULT NULL,
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `src_user` text,
  `src_ip` varchar(50) DEFAULT NULL,
  `op_type` char(10) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = tenant_capacity   */
/******************************************/
CREATE TABLE `tenant_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
	`username` varchar(50) NOT NULL PRIMARY KEY,
	`password` varchar(500) NOT NULL,
	`enabled` boolean NOT NULL
);

CREATE TABLE `roles` (
	`username` varchar(50) NOT NULL,
	`role` varchar(50) NOT NULL,
	UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
    `role` varchar(50) NOT NULL,
    `resource` varchar(255) NOT NULL,
    `action` varchar(8) NOT NULL,
    UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

当然,我们实际最推荐的实践是使用带有主从的高可用的数据库集群,但是我们没这么多资源,我们这里就整一个意思下就差不多得了

然后我们具体来配置我们的三个nacos,首先进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf,然后添加内容

127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847

这三个地址是我们的nacos结点的地址,由于我们nacos就在本机中,因此我们这里直接填写本机的地址,在企业开发中前面填写的当然是对应的nacos的地址,后面的则是nacos的对应端口号

然后修改application.properties文件,添加数据库配置

spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://175.178.114.158:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=itheima

接着我们将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3

然后分别修改三个文件夹中的application.properties,

nacos1:

server.port=8845

nacos2:

server.port=8846

nacos3:

server.port=8847

然后打开三个cmd窗口,分别启动三个nacos节点:

startup.cmd

此时能看到cmd窗口上显示nacos启动成功则说明我们已经成功启动了

然后我们安装nginx

修改conf/nginx.conf文件,配置如下:

upstream nacos-cluster {
    server 127.0.0.1:8845;
	server 127.0.0.1:8846;
	server 127.0.0.1:8847;
}

server {
    listen       80;
    server_name  localhost;

    location /nacos {
        proxy_pass http://nacos-cluster;
    }
}

上面的代码放到nginx的http代码块中,server代码块前的位置

而后在浏览器访问:http://localhost/nacos即可。

代码中application.yml文件配置如下:

spring:
  cloud:
    nacos:
      server-addr: localhost:80 # Nacos地址

到此位置,我们实际去访问我们的地址,添加一个对应的全局配置,可以看到数据库中也添加了对应的配置,此时我们的集群搭建就完成了

最后我们来看看项目优化

  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.
  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

Feign(服务调用组件)

快速入门

先来看看我们此前的RestTemplate调用存在的问题

image-20220824132035930

此时我们就可以使用Feign来帮助我们优化我们的代码,先来看看其介绍

image-20220824132245405

要使用Feign客户端,首先要引入对应的客户端依赖

<!--feign客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

image-20220824132318648

然后我们在order-service启动类中添加@EnableFeignClients注解开启Feign功能

然后我们要定义和Feign客户端,我们首先创建clients.UserClient接口

image-20220824132343220

写入其代码如下

@FeignClient("userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

我们上面的代码是根据下面的连接根据一定的格式写入的

        String url = "http://userservice/user/"+order.getUserId();

首先我们本次请求的服务名称是userservice,因此我们的接口类上加入@FeignClient注解,括号内填入userservice,代表该类中所有方法都请求userservice服务,如果想要其他的服务,创建一个新接口写入该注解即可

我们的本次的请求是get请求,因此方法上写上GetMapping,由于我们的url地址后面拼接/user/且需要获得传入的id,因此我们这里也通过GetMapping提供的对应拼接路径的方式

接着我们定义一个具体的方法,我们的返回值需要的是User,因此接口的返回值定义为User,因为要获得传入的Id,因此我们使用PathVariable注解,在方法中定义一个id参数获得传入的数据

image-20220824135810528

那么最终我们可以将我们order模块服务层的代码修改如下

@Autowired
private UserClient userClient;

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2.用Feign远程调用
    User user = userClient.findById(order.getUserId());
    // 3.封装user到Order
    order.setUser(user);
    // 4.返回
    return order;
}

然后我们测试代码,发现没有问题。这里我们有两点要提一下,第一点是由于之前我们做个集群搭建,修改了userservice模块的nacos端口,因此直接启动对应服务的话会报出连接失败错误,只要把userservice的端口号修改为8848即可,第二点是我们的orderservice和userservice不在一个命名空间里,即使服务都启动成功了,调用orderservice也会报com.netflix.client.ClientException: Load balancer does not have available se,解决方法是将命名空间全部改为公共命名空间即可

自定义配置

Feign的配置都先帮我们整好了,一般来说我们是不要改动的,但是我们也可以改动,先来看看我们可以修改的配置,一般来说我们只修改日志级别,默认的日志级别是NONE

image-20220824143331934

我们自定义配置有两种方式,一种是配置文件方式,另一种是注解方式,我们先来看看配置文件方式

image-20220824143713885

然后是注解方式,注解方式我们需要创建config.DeaultFeignConfiguration类,具体写入对应的Bean如下

同样有全局配置和局部配置两种方式

image-20220824143958257

一般来说,没什么事我们不推荐提高日志的等级,因为会降低性能,如果有特殊需求我们再提高等级

性能优化

Feign的底层客户端实现是URLConnetion,其是JDK内部的一个玩意,是不支持连接池的,而不支持连接池,每次一个请求过来就要创建一个连接,其性能自然不会好,所以我们的性能优化就是改变Feign的底层客户端实现,令其支持连接池

首先我们要引入对应的HttpClient的依赖

<!--引入HttpClient依赖-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

image-20220824145917760

然后我们具体配置连接池的配置,我们这里指定最大连接数和单个路径的最大连接数,这个指定并不是随意指定的,一般来说是要做压力测试,调整到一个性能最高的连接数并配置进去

feign:
  httpclient:
    enabled: true # 支持HttpClient的开关
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 单个路径的最大连接数

最佳实践的分析和实现

Feign的最佳实践指的是对于Feign在实际开发中的使用,企业在不断地使用中总结出来的最佳的使用方式,一共有两种,分别指继承抽取

第一种最佳实践的方式是继承,我们可以定义一个统一的接口,接口内写入具体的请求,该接口会让我们的原先的Feign请求继承该接口,而具体的逻辑业务则实现该接口,因为我们的具体的请求和要转换的请求他们最终的格式必然是一样的,只有一样他们才可以完成请求的对接,因此我们可以将其实现统一的一个接口规范

image-20220824152747261

但是这种方式也存在问题,第一个问题是其会令我们的服务耦合,第二是父接口中的映射是不会被继承的,我们在实现具体业务的时候还得写一遍,不过由于其符合面向抽象编程,因此在企业开发中还是经常使用这种方式

第二种方式是抽取,即是将FeignClient抽取为独立的模块,将和接口有关的POJO,配置都放到这个模块中,其他服务如果需要使用到这个模块内的服务,只需要引入该模块的依赖,然后调用该模块的服务即可

image-20220824152956442

这个方式虽然说是成功完成的解耦合,但是存在资源浪费的问题,比如说如果我们的模块中的服务很多,而另一个服务只需要引入其中的一个服务而已,但是却需要将整个模块都引入,这当然是有些浪费了

所以说两种方法都是存在利弊的,具体根据我们的实际业务需求去开发就行了

接着我们来实现第二种最佳实践的方式

首先我们新创建一个模块,本来我们应该直接创建一个空的Maven工程的,但是我们这里还是一直搁那报null问题,所以我们还是创建一个Spring模块,然后更改其pom文件达成我们的目的

image-20220824154859297

首先我们在pom文件中引入对应的feign的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

然后我们将其他工程的内容剪切到新创建的类中

image-20220824165103499

然后我们在其他的服务中引入我们新创建的服务

<!--引入feign的统一api-->
<dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

再把其他的类再自动导包装配一次

这里值得一提的是,我们在service中有自动装配UserClient对象的代码,但是这里由于该对象已经不再SpringBootApplication的扫描包范围了,那就扫描不到FeignClient注解,那就无法生成对应的对象并交由给Spring管理

image-20220824163940435

我们可以有两个方法可以解决该问题,一是指定FeignClient的所在包,二是直接FeignClient的字节码,我们这里采用第二种方式

最终我们经过测试会发现这个是确实可用的

Gateway(网关)

Gateway介绍

我们的网关能做的事情有三,第一件是身份认证和权限校验,其次是服务路由(即将请求正确发送到对应的服务中)和负载均衡,最后是请求限流,防止请求过多

image-20220824170358258

网关的技术实现有二,一是gateway,二是zuul,我们一般是使用前者实现,其属于响应式编程,具备更好的性能

image-20220824170447728

快速入门

本节我们就来搭建我们的网关服务,做一个简单的快速入门,首先我们要创建我们的module,引入对应的Gateway和nacos依赖,之所以要引入nacos依赖是因为我们的网关本身也是一个服务,是要注册到nacos中的

image-20220824171557290

那么我们可以写入我们的依赖如下

<!--nacos服务注册发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--网关gateway依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

然后我们创建一个对应的配置文件,写入我们的配置

image-20220824172400839

我们写入我们的配置如下

server:
  port: 10010
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**

我们这里首先设置了我们的端口号为10010,然后我们将该服务注册到了nacos中,接着进行网关的设置,首先设置路由的唯一标识,然后具体设置路由地址,路由地址是lb拼接一个具体的服务名,lb是一个指代的意思,代表我们寻找该路由中的所有服务,然后我们再做一个路由断言,保证我们传入的地址是符合我们所需要的格式的

同理我们还做了order-service的服务的引入

我们可以用一个图来展示内部发生的过程,用户请求Gateway,网关判断对应的请求地址往Nacos中注册中拉取对应的服务,然后往对应的服务发送请求,这里还做了负载均衡

image-20220824201953472

最后我们来看看我们的网关搭建步骤

image-20220824202931634

事实上,我们的路由配置最后还可以包括一个路由过滤器,我们以后演示这个

路由断言工厂

像我们之前在配置文件中写入的断言规则只是字符串,但是其会被Predicate Factory读取并处理,转换为路由判断的条件,具体的内容请看图

image-20220824203257520

Spring提供了11种基本的断言工厂,我们可以利用这些断言工厂来做我们一些特定的请求拦截

image-20220824203337598

其实简单来说就是我们使用上面的11种配置方式来配置对应的访问权限

过滤器

路由过滤器配置

在Gateway中,还提供了过滤器,过滤器可以有多个,其能够对进入网关的请求和微服务返回的响应对一些对应的处理,可以做一些功能上的增强

image-20220824204853694

Spring中提供了31种不同的过滤器工厂,我们只演示其中的一个

image-20220824204921938

先来看看我们要演示的案例,我们要实现这个案例,就需要在与predicates同列的某一行上加上filters配置

image-20220824210020500

为了让我们能够在控制台上输入该请求头,我们在UserController中的查询方法中获取该请求头的内容并打印

/**
 * 路径: /user/110
 *
 * @param id 用户id
 * @return 用户
 */
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
                      @RequestHeader(value = "Truth",required = false) String truth) {
    System.out.println("truth: "+truth);
    return userService.queryById(id);
}

如果要对所有的路由都生效,可以将过滤器工程写到default下,具体格式如下图

image-20220824210903427

全局过滤器

前面我们学习的过滤器只能够做一些简单的处理,但是如果我们要做一些的复杂的过滤处理的话,就不能够再使用上面的处理器了,此时我们要使用全局过滤器GlobalFilter来帮助完成我们的需求

image-20220824212801059

要实现全局过滤器需要创建一个新的类并实现接口GlobalFilter,其会提供ServerWebExchange和GatewayFilterChain两个参数,前者可以获取Request和Response等信息,后者可以将请求委托给下一个过滤器,如果没有过滤器则结束过滤

它们都需要返回一个Mono对象,这个对象具体是什么并不重要,我们先记着其返回的是这个对象即可

那么我们先来看看我们的案例需求

image-20220824212831535

那么我们创建对应的类并实现GlobalFilter

image-20220824222212794

我们可以写入其代码如下

//@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> params = request.getQueryParams();

        // 2.获取参数中的 authorization 参数
        String auth = params.getFirst("authorization");

        // 3.判断参数值是否等于 admin
        if("admin".equals(auth)){
            // 4.是则放行
            return chain.filter(exchange);
        }

        // 5.反之拦截
        // 5.1设置状态码
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return response.setComplete();
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

我们这里需要给我们的过滤器设置优先级,有两种方式,一种是注解方式,类上加入Order注解,括号内填入int数值,数值越大优先级越低,另一种方式是实现Ordered接口,重写其下的方法,同样返回一个int数值

实现GlobalFilter接口的方法,先取出参数,然后判断参数是否是admin,是则放行,放行调用chain.filter()方法,该方法需要传入一个ServerWebExchange,返回Mono参数,如果还有过滤器则会进入下一个过滤器过滤,反之则直接返回Mono对象

如果不是我们则拦截,拦截直接调用exchange的获得返回对象方法即可,返回对象中我们设置对应的展示状态码,告知用户没有对应的密码我们不给登录,之后返回响应对象即可,调用响应对象的setComplepte方法,其同样返回Mono对象,就可以将响应对象正常返回

过滤器链执行顺序

我们现在一共学习了三个过滤器,我们现在要对这三个过滤器的顺序做一下了解

请求路由之后,网关会将三个不同的过滤器合并到一个过滤器链,也就是集合中,当中必然要将这三个类转换类统一的类

而在我们的网关中存在过滤器适配类,任何的过滤器最终都会统一通过过滤器适配类统一转换成GatewayFilter类,所以我们的三个过滤器最终都可以认为是同一类型的过滤器

image-20220824225056378

我们的三个不同的过滤器如果不设定值,其值会默认按照从上往下运行的方式递增,即使前面的过滤器拥有更高的要优先级,注意,这里的的值在不同的过滤器里是隔离的。举个例子,我们可能设置了多个当前路由的过滤器和DefaultFilter,那么这两者的过滤器在其各自的内部里的order值都应该会是123的形式

当不同的过滤器的order值一样的时,会按照defaultFilter>路由过滤器>GlobalFilter的顺序执行

image-20220824225320590

网关的CORS跨域配置

首先先让我们来复习一下什么跨域,只要访问前后的域名不一致就是跨域

image-20220824230613203

跨域并不是说一定会发生问题,一般的url请求是不会有问题的,但是浏览器是禁止请求发起者与服务端发起ajax请求的,一旦发送AJAX请求,那么就会被浏览器拦截

我们对此的解决方案是CORS

image-20220824230736447

配置CORS的方案很简单,直接按照上图配置即可,具体的配置信息的作用都标记在图上了,这里就不再赘述了

Docker(微服务部署)

初识Docker

由于我们的微服务很大,如果我们直接对其进行部署,那么会存在各种各样的问题,具体可以看下图

image-20220824233918725

此时我们就需要利用Docker,其能够解决依赖的兼容问题,其将每一个应用放到一个隔离容器去运行,避免互相干扰

image-20220824233950251

其解决不同系统环境的问题是将其用户程序的系统和函数库一起打包,由于系统的底层调用的都是Linux,那么Docker只需要做对应的中转工作,就可以实现同样的调用Linux库函数的功能

image-20220825000159543

最后我们来看看我们Docker的图示介绍

image-20220825000218725

总结则如下

image-20220825000759401

Docker和虚拟机的差别

我们之前了解了Docker的实现原理,但是似乎我们的虚拟机也能有同样的功能,那么Docker和虚拟机有什么差别呢?我们本节就来了解这个内容

image-20220825132911639

首先Dcoker是将其对应的使用的函数库也一起打包,其底层实际调用的还是当前的操作系统的功能,只不过Docker给他们做了一些代理,这样便于不同操作系统的交互,这样不管你是哪一个操作系统上衍生出来的产品,最终都可以正确调用当前操作系统的功能,用户无需关心当前的产品的系统,只需调用对应的函数即可

而虚拟机是在操作系统中模拟硬件设备,在操作系统中运行了另外一套操作系统,虚拟机下达的指定会通过Hypervisor转化给实际的操作系统再调用对应的计算机硬件处理(Hypervisor可以模拟出计算机的各种软硬件,实现操作系统中再安装一个操作系统),其由于有两个操作系统,因此效率较慢,且占用空间较大

image-20220825133645622

Docker架构

接着我们来学习Docker的架构,首先我们我们的Docker会将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称之为镜像

而镜像中的应用程序运行形成后的进程就是容器,Docker会给容器做隔离,对外不可见

image-20220825134118214

这里我们值得一提的是,镜像文件是只读的,不允许写入,这样可以防止污染。我们的每一个容器都是一个独立的进程,内部会有自己的一份CPU甚至于是操作系统,内部的进程会认为只有它自己在运行,因此实现了隔离。如果容器想要写入,就往自己的内容里写入,这样不会对其他容器或者是镜像文件造成污染

那么我们的镜像要保存到哪里去呢?此时就需要用到DockerHub

image-20220825134316672

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

最后我们来看看docker的运行时发生的过程,首先docker是一个CS架构的程序,由两部分组成

首先服务端有Docker守护进程,其负责处理Docker指令、管理镜像和容器等

客户端则可以通过命令或者是RestAPI向Docker服务端发送对应的指令,可以本地发送,也可以远程发送,前者用命令,后者用RestAPI

image-20220825134452359

我们调用docker build命令,可以让Docker给我们的为微服务打包成一个镜像文件

而docker pull命令,可以让docker去Registry镜像库中去拉取我们想要的镜像文件到本地上

docker run则是可以将镜像文件创建成一个个进程,也就是容器

image-20220825134531459

Docker的安装

现在我们来安装Docker,Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。我们当然用免费的

Docker CE 支持 64 位版本 CentOS 7,并且要求内核版本不低于 3.10, CentOS 7 满足最低内核的要求,所以我们在CentOS 7安装Docker,正好我们的云服务器正好是CentOS 7,而且内核还正好是3.10,哈哈

如果之前安装过旧版本的Docker,可以使用下面命令卸载:

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine \
                  docker-ce

首先需要安装yum工具

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

然后更新本地镜像源:

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

sudo yum makecache fast

然后输入命令,安装社区免费版docker

yum install -y docker-ce

docker-ce为社区免费版本。稍等片刻,docker即可安装成功

Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙

# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld

通过命令启动docker:

systemctl start docker  # 启动docker服务

systemctl stop docker  # 停止docker服务

systemctl restart docker  # 重启docker服务

输入命令,可以查看docker版本:

docker -v

docker官方镜像仓库网速较差,我们需要设置国内镜像服务:

参考阿里云的镜像加速文档:cr.console.aliyun.com/cn-hangzhou…

Docker基本操作

Docker的基本操作分为镜像操作容器操作以及数据卷,我们按顺序一个个来学习

镜像操作

首先我们先来看看镜像的名称组成

image-20220825143802890

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

然后我们来看看镜像操作命令

image-20220825144046879

  • docker images,查看当前的本地镜像
  • docker rmi,删除某个本地镜像
  • docker pull,从镜像服务器中拉取镜像
  • docker push,推送镜像到镜像服务器中
  • docker save,保存镜像为一个压缩包
  • docker load,加载压缩包为镜像

大概就是这六个命令,不会用或者忘记了没关系,需要的时候再回来看帮助文档就行了

使用docker [具体的命令] --help 可以查看到某个命令的各种操作说明,如果中间的内容不填写那么默认展示所有命令的内容解释

然后我们来看看下面的案例,DockerHub的网址是hub.docker.com/

image-20220825144437687

如果我们要拉取nginx镜像,则写入命令 docker pull nginx

如果我们要查看拉取到的镜像,则调用命令docker images

我们要删除某一个具体的镜像时,可以输入IMAGE ID,也可以拼接REPOSITORY:TAG来实现我们的目的,拉取的时候如果不填写tag,那么就默认拉取最新的版本

image-20220825150436551

docker save的完整命令格式是docker save [OPTIONS] IMAGE [IMAGE...],一般来说,我们的[options]要加入-o,后接打包后形成的文件名和格式,最后要指定具体的镜像文件,可以说是一个,也可以是多个,同样要指定版本号

那么如果我们想要对nginx打包,我们可以写入命令docker save -o nginx.tar nginx:latest

想要删除则写入docker rim nginx:latest

想要将压缩包加载为镜像到本地,则写入命令docker load -i nginx.tar

再来看一个练习

image-20220825152416806

我们首先在DockerHub搜索Redis镜像,这一步就不演示了,自己看

拉取redis的命令是docker pull redis

将其打包为一个redis.tar包的命令是docker save -o redis.tar redis:latest

删除本地的redis:latest的命令是docker rmi redis:latest

利用docker load重新加载redis.tar文件的命令是docker load -i redis.tar

容器操作

接着我们来学习容器相关的操作,同样我们还是先来看看其拥有的命令

image-20220825153338144

  • docker run,让某个镜像生成一个容器
  • docker pause,令某个容器暂停运行,系统会保留其内存,会将进程挂起
  • docker unpause,令某个容器恢复运行,内存空间会恢复,程序继续运行
  • docker stop,令某个容器停止运行,系统会直接杀死该进程,回收其内存
  • docker start,令某个容器开始运行,此处的运行是重新生成一个新的容器(进程),并分配给其运行空间
  • docker rm,删除某个容器
  • docker exec,进入容器执行命令
  • docker logs,查看容器运行日志
  • docker ps,查看所有运行的容器及其状态

那么我们来看看案例练习,假设我们要创建运行一个Nginx容器,我们当然要使用docker run命令,但是对于不同的镜像,其run命令是不同的,所以我们推荐是先去docker hub中搜索对应的镜像,查看其帮助文档的说明,然后学习其如何创建运行容器,我们这里搜索到其运行的镜像的命令是docker run --name containerName -p 80:80 -d nginx,我们下面将一个个解释其作用

image-20220825154913284

首先docker run代表创建和运行容器,--name代表给容器起一个名字,后接名字,-p则是将主机端口与容器端口映射,左侧是主机端口,右侧是容器端口,之所以要这么做,是因为我们的容器是隔离状态的,外界不可以直接访问到容器,但是我们的主机可以,所以我们这里就可以做一个映射,让外界去访问我们的主机,我们的主机一旦接收到请求就去访问对应的容器,再将结果返回给浏览器

-d则代表后台运行容器,不写就是前台运行,最后的nginx是我们的镜像名称,不写tag则默认是调用最新的版本

那么最终我们可以写入我们的命令如下docker run --name mn -p 80:80 -d nginx

运行这个命令之后其会在控制台上显示出一串id,该id是该容器的唯一标识,每一个容器都有这东西,便于我们对容器进行对应的操作

输入docker ps可以查看目前运行的容器的各种信息,CONTAINER ID是简短id,IMAGE是生成容器的镜像,COMMAND是容器内路径,可以不用关心,CREATED是容器的创建时间,STATUS是容器的运行状态,PORTS则是容器的映射端口,NAMES是容器名字

image-20220825161401938

最后我们在浏览器中可以正确访问到nginx

docker logs可以查看容器的日志,调用命令为dockers log mn,如果想要持续查看命令,可以写入命令docker logs -f mn

然后我们再来看一个案例,我们的目的是要进入Nginx容器,修改HTML的文件内容

image-20220825163112136

那么首先我们要进入容器,调用命令docker exec -it mn bash,此处代表进入容器内部,-it 则是给当前进入的容器创建一个标准的输入和输出终端,这样我们才可以在命令行里实现对容器的交互,mn是我们要进入容器的名称,bash则是我们进入容器后执行的命令,我们这里进入内部执行一个终端交互命令,这样我们就可以在控制中对容器进行操作了

我们执行了这个命令之后可以我们已经进入到了容器中,我们的命令行都对应发生了变化,不但如此,我们还可以调用对应的ls或者是pwd命令来查看当前的文件夹的内容或是所处的位置

然后我们进入到nginx中HTML的所在目录,调用vi命令修改里面的内容,但是会提示找不到命令,这是因为我们的docker创建镜像的时候,只会打包其用到的必要的命令,而Nginx里显然不需要vi,所以这里没有这个命令

我们可以用下面两行命令进行代替,也能达到相同的目的

sed -i 's#Welcome to nginx#永失吾爱,举目破败#g' index.html

sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html

最后我们这里要提一点的是,实际上对应的Nginx的目录,以及进入容器的命令,我们都是要通过docker hub查阅nginx的对应的帮助文档才能知道的,这就是为什么我们推荐要多查阅docker hub上对应镜像的说明,因为的确很多具体的信息和其用法都是查阅才能知道的

docker ps可以查看当前的所有进程,但是默认只会展示运行中的进程,如果想要展示暂停中甚至于停止中的进程,那么就要调用docker ps -a命令,容器只有被删除才会完全从进程中消失,暂停和停止都不会

docker rm 命令不可以删除正在运行中的容器,如果非要删除,要在后面添加-f参数

image-20220825170504530

最后我们要说一点,那就是我们的exec命令的确可以进入容器修改文件,但是我们是不推荐的,因为在容器内修改文件是没有记录的,整多了以后自己到底干了啥都不知道,那就寄了

接着我们来看最后一个案例

image-20220825171615596

我们首先进入到docker hub里的redis镜像中

image-20220825171706046

我们看到这里有两个运行的内容,第一个只是运行一个实例,而第二个是运行一个持久化的数据实例,我们要的是第二个,所以我们复制其内容并运行

我们这里执行我们的命令如下docker run --name mr -p 6379:6379 -d redis redis-server --appendonly yes

我们这里设置了名字,映射端口,还有允许持久化数据,之后就能得到一个运行的实例

然后我们再执行docker exec -it mr bash,同样可以进入到容器中进行操作

我们再内部再调用redis-cli,就可以连接上redis了,然后也可以做对应的操作,我们在外部也可以连接上这个redis

值得一提的是我们可以直接在外部调用docker exec -it mr redis-cli命令,这样我们就可以直接连接到redis中了,前面的就相当于是先进入内部并打开控制台,后面是直接进入容器之后连接内部的内容

数据卷操作

我们先来分析一下我们之前的容器存在的问题,最主要的问题是容器与数据耦合的问题,带来的最主要的三点问题是,不便于修改、数据不可复用和升级维护困难,而为了解决这些问题,就需要用到数据卷

image-20220826081509485

数据卷是一个虚拟目录,其指向宿主机文件系统中的某个目录,其实一般来说就是存在于/var/lib/docker/volumes这个目录中,当我们在数据卷中创建某个目录时,实际上也会在对应的Volumes中创建一个相同的目录,以后我们可以用这个目录与容器内部的对应目录进行关联,这样容器内部的修改就可以写入到数据卷中,就可以实现修改和复用了,甚至于以后把容器删除了都没关系,反正关键数据我们已经保存了,而在外部对这些对应的目录内容进行修改,也会对应调整到容器内部,这样也可以实现外部对内部的修改

image-20220826081738281

然后我们来看看操作数据卷的命令

image-20220826081906059

接着我们来做一个案例加深理解

image-20220826082439786

如果我们要创建数据卷,那么我们就应该写入命令docker volume create html,最后指定的要创建的目录的名字

如果要查看当前数据卷的所有目录,那么我们应该写入命令docker volume ls

如果要查看某个数据卷的详细信息,需要写入命令docker volume inspect html,最后指定的是要查看的目录的信息的名字

同理要删除某个数据卷,就写入命令docker volume rm html,最后指定的是名字

调用docker volume prume可以删除所有未使用的数据卷,不需要指定数据卷名

然后我们来学习如何挂载数据卷,其实意思就是怎么做宿主机目录对容器目录的映射

image-20220826084239210

然后我们来看看我们的案例

image-20220826085707623

如果我们想要创建容器挂载数据卷到容器内的HTML目录,我们只需要写入命令docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx,我们这里左边是我们数据卷的目录名称,右边是要挂载的容器内的目录,如果数据卷中一开始没有该目录,那么其会自动创建,其他的命令跟之前学习过的一样,这里就不再赘述了

最后我们在数据卷中修改对应的文件,会发现实际中真的容器内的内容真的发生了改变,此时就说明我们的数据卷已经挂载成功了,这个操作称为数据卷挂载

然后我们再来看最后一个案例

image-20220826090803416

我们这里要做的事情是将宿主主机的目录直接挂载到容器,也就是说,容器内的一个大的目录内容会放到我们的数据卷中,里面会显示所有的数据信息,外界对该目录的修改同样也会应用到容器中

那么为了实现这个目的,我们要先将课前资料的镜像文件上传到虚拟机上并加载为镜像,这里就不下载了,然后我们创建对应的data目录,该目录就是用于将容器内的目录挂载到我们指定的数据卷中

我们还需要创建一个conf目录,内部会存放我们上传的课前资料里提供的hmy.cnf的mysql配置文件,后续我们会将该配置文件在容器创建时就替代对应容器内的对应配置文件,后续对配置文件的修改也会对应到容器中

这个过程称为目录挂载

那么最终我们就可以写入我们的命令如下,这里我们用\进行分隔命令,让我们的命令看起来变得更加有条理

docker run \

--name mysql \ -e MYSQL_ROOT_PASSWORD=123 \

-p 3307:3307 \

-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \

-v /tmp/mysql/data:/var/lib/mysql \

-d \

mysql:5.7.25

最后我们可以来看看数据卷挂载的两种方式,一种方式是由数据卷自动创建,是数据卷挂载,另外一种是我们手动创建,是目录挂载,前者是自动化的,但是缺点是我们不好找对应的目录,后者不是自动化的,目录比较好找,但是需要我们自己来管理目录

image-20220826093448056

image-20220826093520322

自定义镜像

我们之前已经学习过如何拉取docker hub里的镜像了,但是我们还没有学习如何自定义我们的镜像,我们本章节就来学习如何自定义镜像

镜像结构

首先我们来学习镜像结构,先来复习一下镜像的定义,镜像是将应用程序及其需要的系统的函数库、环境、配置、依赖打包而成,注意,这个定义非常重要,我们必须要先记住

那显然,我们的镜像组成无非就是应用程序、需要的系统的函数库、环境、配置、依赖了,但是这只是组成,镜像必然是还存在结构的,接着我们就来学习其结构,我们这里生成mysql镜像为例,来讲解镜像的结构

image-20220826095355665

首先在镜像的最底层配置的是应用所需要的部分系统函数库、环境、配置、文件等,这一层被称为基础镜像,只有搞定了这一层最基础的部分,后面的部分才能继续搞

搞定了这一层之后,我们拷贝mysql安装包到更上一层,在此基础之上继续安装mysql,同时配置mysql的配置文件,等所有的安装搞定之后,我们再提供一个镜像运行入口(一般是程序启动的脚本和参数),便于外部启用该镜像

最后我们从底层搞最后,每一次添加安装包、依赖配置的操作都会形成新的一层,这有什么用呢?其最大的作用是便于我们的后续对镜像的维护,比如后续如果我们的镜像进行了升级,如果没有分层,那么我们就必须从0开始打包,但是如果我们进行了分层,我们就可以进行比对,只替换那些发生了更换的层级就可以实现我们的目的

最后我们来看看总结

image-20220826095304296

DockerFile

接着我们来学习DockerFile,我们要自定义镜像,必须要靠它。DockerFile本质是一个文本文件,其中包含的内容是一个个的指令,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层

下图中给出了部分指令说明,如果想要了解更多指令,可以参考官网文档

image-20220826100644211

先来看看我们的DockerFile里的内容

# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

我们首先指定基础镜像并配置环境变量,接着拷贝jdk和java项目的包,然后安装jdk并配置环境变量,最后暴露端口,这里暴露的端口是可以加也可以不加的,加了就是让用户知道你的项目使用了什么端口,到时候其配置的时候会往这个端口配置,而且这也不是真正意义上的暴露端口,只是告诉给用户你这个项目的使用端口而已

然后我们来看看我们的案例

image-20220826101016844

首先我们创建一个对应的tmp文件夹,然后我们将课程提供的资料都拷贝进去,接着我们执行docker build -t javaweb:1.0 .命令来生成镜像,这里我们的这个命令中的-t代表的是给镜像命名,分号前是名称,分号后是版本号,最后我们加入一个 . ,其代表的意义是在当前文件夹生成

image-20220826103017699

不过其实分析我们的dockerfile文件,我们会发现其实我们真正起作用的操作是

COPY ./docker-demo.jar /tmp/app.jar

其他的操作都是一些重复性的操作,比如一些固定的依赖集成等,那些事情其实大可以做一次就可以了,那么有没有这么一个镜像呢?其实是有的,那个镜像就是java:8-alpine,我们只需要指定其为我们的基础镜像,其他拷贝jdk安装jdk一类的事我们就不用做了

那么我们可以修改我们的dockerfile文件的代码如下

# 指定基础镜像
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 . ,同样可以完成我们的打包

image-20220826103708073

DockerCompose

首先我们来看看DockerCompose的介绍

image-20220826130530760

简单来说,DockerCompose可以理解为是一个脚本,其就相当于是配置了许多个DockerFile,这样就便于我们的容器的自动部署,其下的每一个内容都对应了我们在DockerFile里的配置内容

比如我们上面的service下的mysql就是配置了名字,image则是具体的镜像,environment则是设置密码,volumes则是设置对应的映射目录

web下面的内容分别设置了生成镜像的位置以及配置容器的映射端口号

那么接着我们来安装DockerCompose,我们的课程资料里有提供docker-compose文件,直接将其上传到/usr/local/bin/目录

然后修改文件权限:

# 修改权限
chmod +x /usr/local/bin/docker-compose

接着使用自动补全命令

# 补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

如果这里出现错误,需要修改自己的hosts文件:

echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts

此时我们就已经安装好了

然后我们来做一个案例,将之前学习的cloud-demo微服务集群利用DockerCompose部署

image-20220826155741799

那么首先我们要修改我们的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名,这样我们的docker才能在运行时通过这些名字正确去访问到对应的其他服务

然后我们需要用maven打包工具将项目中的每个微服务都打包为app.jar,我们之前已经配置好了打包的名字了

<build>
    <finalName>app</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

之所以一定要是app这个名字,是因为我们的脚本打包的时候就是用这个关键词

打包Feign时,报出下面的错误

Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile

原因是项目设定的jdk版本和实际的jdk版本不一致,解决方法,blog.csdn.net/fanrenxiang…

打包cloud-demo时,报出下面的错误

Failed to execute goal on project order-service: Could not resolve dependencies for project cn.itcast.demo:order-service:jar:1.0: Failure to find cn.itcast.demo:feign-api:jar:1.0 in

原因是依赖的子模块并没有先加载到本地仓库中,解决方法,www.jianshu.com/p/60b2719b5…

然后我们将打包好的app.jar拷贝到cloud-demo对应的子目录中,然后上传到服务器上,利用docker-compose up -d部署

不过我们部署的时候出了些问题,没有运行成功,这无伤大雅

Docker镜像仓库

接着我们来创建自己的私有仓库

image-20220826160806730

首先我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:

# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.150.101:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker

接着我们正式来讲搭建我们的镜像仓库

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

搭建方式比较简单,命令如下:

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

命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。

不过我们要的是有图形化界面的仓库,所以我们采用另外一种方式

创建一个Docker-Compose.yml文件,并写入其内容如下

version: '3.0'
services:
  registry:
    image: registry
    volumes:
      - ./registry-data:/var/lib/registry
  ui:
    image: joxit/docker-registry-ui:static
    ports:
      - 8080:80
    environment:
      - REGISTRY_TITLE=传智教育私有仓库
      - REGISTRY_URL=http://registry:5000
    depends_on:
      - registry

其作用是使用DockerCompose部署带有图象界面的DockerRegistry,相当于是创建时顺便集成了一个ui进来,这样便于我们查看我们的仓库

image-20220826154614864

最后我们要上传我们的镜像到仓库中,我们必须要先对镜像进行重命名,具体的命令其实都在上面了,第一个写入的当前镜像的名字,后面则要跟我们的远程ip地址+端口号/+新名字(当然也是名字和版本的组合)

然后还是按照之前的命令推送拉取即可,只不过是名字要用我们重命名的名字 值得一提的是,如果我们调用了第一个命令,我们再调用docker images会看到两个相同的镜像,虽然他们名字不同,但是其唯一标识,也就是id是一样的,这也说明了其实这个两个镜像是同一个镜像,只不过多了一个不同名字的文件而已

MQ(异步消息队列)

同步通信与异步通信

先来讲讲同步通讯与异步通讯的异同和优缺点

同步通信,简单来说就是类似于是微信电话,我们和另外一个人通电话,我和他总是可以获得即时的信息,这就是同步通讯,而异步通讯则类似于是微信聊天,我们像另外一个人发信息,我们未必能立刻获得回复,同时对方也未必就能看到,但是他之后如果看到了就可以回复我,同时我们可以一次和多个人进行微信聊天,但是视频聊天显然不能,所以同步通信具有一对一的特性,而异步通信则有一对多的特点

image-20220901081135777

在我们之前实现的微服务中,基于Feign的调用就属于同步方式,我们总是去调用支付服务,然后让支付服务去调用其他服务,由于是同步通讯的方式,因此存在诸多问题

image-20220901081501436

其具有问题的如下所示

image-20220901081559185

接着我们来讲讲使用异步通信时我们的微服务架构要变成什么样子,一旦使用异步通信改造我们的微服务架构,我们的微服务就变成一个实践驱动的微服务架构

我们可以在我们的支付服务之后加入一个Broker类,当用户支付成功时,支付服务就向Broker发布一个成功事件,然后Broker就会通知其下的所有服务执行,被通知的各个服务将执行其对应的功能

image-20220901082724198

该模式具有许多好处,比如耦合度低、吞吐量提升、故障隔离、流量削峰等,当然其自己也具有缺点,具体请看下图

image-20220901083142959

那么同步通信和异步通信,我们到底是哪个使用得多呢?其实是同步通信用的多,但是在一些特殊的业务需求中,我们也是确确实实需要异步通信的,比方说我们对追踪故障需求不大,但是我们要提高并发量,同时需要异步通信的各种优点,那么我们就可以将其改为异步通信

什么是MQ

所谓MQ,其实就是消息队列,其代表的就是我们事件驱动架构中的Broker

实现Broker的具体类有很多种,其中较为出名的就是RabbitMQ、ActiveMQ、RocketMQ、Kafka这四种,其具体异同请看下图

image-20220901084104422

我们这里采用RabbitMQ来实现我们的架构,Kafka一般是大型公司使用的,乐然RabbitMQ则适合各种中小型企业,因此我们这里学习RabbitMQ

RabbitMQ

部署和安装

那么这一节我们就先来安装我们的RabbitMQ,我们这里使用镜像安装,我们这里先演示单机部署而非集群部署

方式一:在线拉取

docker pull rabbitmq:3-management

方式二:从本地加载

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

image-20210423191210349

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

docker load -i mq.tar

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

docker run \
 -e RABBITMQ_DEFAULT_USER=itcast \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management

上面的命令我们设置我们的MQ容器的账号为itcast,密码为123321,名字为mq,host名为mq1,然后配置了MQ管理平台和MQ本身的端口号

我们直接在命令行页面运行该命令即可部署我们的RabbitMQ

然后我们来看看RabbitMQ的结构和概念

首先RabbitMQ中,向其发送消息的对象叫做生产者,发送消息到其位置的对象称为消费者,在MQ中,存在虚拟主机的概念,虚拟主机可以创建多个,MQ中也可以有多个用户,每个用户都有自己的用户密码以及能够管理的虚拟主机,一般来说,为了防止不同用户的设置冲突,我们推荐每一个用户只能管理一个虚拟主机,这样就起到了隔离的作用

每一个虚拟主机只有名字不同,其内部拥有的内容是一致的,生产者将消息先投放到交换机exchange中,然后exchange将信息投放到对应的消息队列中,消息队列然后消费者会从具体的队列中获取自己的消息

image-20220901090808103

我们管理平台的端口是15672,我们可以以ip地址名和端口名的方式来实现对MQ管理平台的访问

最后我们可以来看看总结

image-20220901092826664

消息模型实现

MQ的官方文档中给出了MQ的五个Demo示例,分别对应几种不同写法,我们可以五种Demo具体细分为五种队列,具体信息请看下图

image-20220901164816710

我们接着来基于最基础的消息队列来实现一个案例,下图是案例的结构

image-20220901164927248

案例的具体需求请看下图

image-20220901165048521

那么首先我们将我们的课前资料导入,这里我们导入了课前资料之后发生了只有demo模块是视为是微服务模块的情况,解决这个问题我们只需要选中mq-demo然后右键选择Add Framework Support...,选择对应的Spring和Maven支持即可

首先我们来看发送者PublisherTest的类的代码,也就是我们的发送者类执行的功能

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("175.178.114.158");
        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();

    }
}

我们这里在进行的了对应的设置之后,就建立连接,建立连接之后我们就可以在MQ管理的网址中的Connection栏目中看到对应的连接

image-20220901212532223

接着是创建通道Channel,只有创建了通道发送者才可以通过通道传输消息

同样的,创建了通道之后,我们可以在对应的Channels栏目中看到对应的通道信息

image-20220901212656764

接着我们创建队列,同样可以在Queues中看到对应的信息

image-20220901212741701

然后我们发送消息,发送的消息会压入到队列中,可以在队列中看到,我们点击队列的名字,可以具体查看队列的内的消息,选择Get message即可获得队列中的消息,我们能够查看到里面存放的消息的确就是我们的消息,最后我们的生产者就单纯的把消息压到队列中,接着其就会关闭连接,剩下的事情就不关他的事了

然后我们来看看消费者的类的代码

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("175.178.114.158");
        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("等待接收消息。。。。");
    }
}

我们这里先建立连接,然后创建通道,接着创建队列,这个很好理解,但是为什么这里还需要再创建一次队列呢?因为可能并不是我们的发送者先运行的,是有可能我们的消费者先运行的,那到时候要是没有队列那就很尬了,所以我们这里会再创建一次队列,作为保险,同时,如果已经有队列存在,那么这个创建队列的代码不会再创建一个全新的队列

接着我们的代码会将订阅消息的与队列进行绑定,注意这里是进行绑定,是绑定这个动作先执行,执行了之后其就会自动开始从队列中获取消息并处理了,而之后我们的代码也会继续往下面走,也就是说,消费者从队列中获取消息和消费者类往下执行是一个异步的过程,是互不干扰的

那么到此为止,我们的简单项目就实现完毕了

SpringAMQP

我们首先来看看什么是AMQP,其实AMQP是一种规范,是一个传递业务消息的开放标准,由于该协议与平台无关,因此更加符合微服务中独立性的要求

image-20220901224142384

Spring AMQP是基于AMQP协议定义的一套API规范,其提供了模板来发送和接受消息,分为基础抽象spring-amqp和底层实现spring-rabbit两层

简单消息模型

接着我们来看看我们要实现的上面的简单消息模型的案例

image-20220901224552183

首先我们要往我们的测试模块mq-demo中引入AMQP依赖

image-20220901224618940

其引入依赖的代码如下

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

注意是要在父工程中引入,这样其子工程都可以使用到,不要引入到其下的子工程中

然后我们需要在publisher编写一个测试方法来向我们的simple.queue队列发送消息

image-20220901224728459

在写代码之前,我们需要先配置我们对应的配置信息,我们这里通过Spring标签来管理我们的rabbitmq,设置了其ip地址,以及对应的端口,账号密码还有虚拟空间

logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 175.178.114.158
    port: 5672 # 端口
    username: itcast
    password: 123321
    virtual-host: /

然后我们可以写入其启动类的代码如下,我们这里设置了一个启动类,然后获得AMQP的对应的实现类,接着我们调用实现类中的方法完成我们的消息的传递

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue() {
        String queueName = "simple.queue";
        String message = "hello, spring amqp!";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

最终我们在MQ管理平台中可以看到对应的消息已经被推送到队列中了,可以看到我们上面的推送代码可比我们以前的代码要简洁多了

接着我们来实现在consumer中的消费逻辑,来监听simple.queue队列

image-20220902130000045

我们首先要引入对应的依赖,然后写入对应的配置,接着我们需要写入我们的代码如下

@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        System.out.println("消费者接受到simple.queue的消息:"+msg);
    }
}

可以看到我们这里首先让该类让Spring管理,然后我们编写一个方法,该方法加入RabbitListener注解,括号内填入我们要监听的队列的名称,内部的方法中接受的信息类型是String类型,因为我们最开始传入的类型也是String类型的,我们这做的事情就是打印这个消息

由于这个类是交给Spring管理的,因此如果想要使用就需要开启整个Spring,我们进入Spring启动类启动Spring,之后可以在控制台中看到我们的打印的消息

最后我们可以做一个总结

image-20220902133655355

Work Queue

接着我们来实现消息模型中的另外一个模型,Work Queue工作队列

image-20220902133950546

有时候,可能我们的一个消费者处理信息速度的并不够快,会造成队列内消息的堆积,此时我们可以在队列里挂载两个消费者,这样两个消费者的速度就会高于队列内消息的堆积速度,就不会出现消息堆积的情况

我们现在看看我们的步骤

image-20220902134110646

那么首先我们要引入对应的依赖,这一步我们之前就已经做过了,接着我们我们现在publisher中定义测试方法,令其每秒产生50条消息并发送到simple.queue中

那么我们可以写入我们的代码如下

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

可以看到我们这里就是通过循环压入50条消息,我们每次压入都让当前线程休眠20ms,这样正好可以在1s中将消息全部压入

接着我们写入我们的consumer的代码如下

@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws Exception{
    System.out.println("消费者接受到simple.queue的消息:"+msg+" "+ LocalDateTime.now());
    Thread.sleep(20);
}

@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws Exception{
    System.err.println("消费者接受到simple.queue的消息.........:"+msg+" "+ LocalDateTime.now());
    Thread.sleep(200);
}

可以看到我们这里只是直接定义了两个消息监听者,第一个令其20ms处理一条,另外一个令其200s处理一条,为了便于区分,我们不但打印了每条消息的时间,还让两个方法输出了不同颜色的文字到控制台上,可以看到我们输出红颜色的文字的方法就是采用System.err方法,普通输出则调用out的形式

但是我们运行容器,之后发送消息到队列中,我们会发现监听器1只处理偶数的消息,而监听器2则只处理奇数的队列,最后造成的结果是监听器1早早就处理完了消息,而监听器2却要话费5s长的时间才处理完所有消息,效率反而更烂了

这是怎么回事呢?这是因为为了提高我们的消息处理的效率,在MQ中存在消息处理预取机制,在队列中会将消息一个个先分配给对应的监听器进行处理,这就是为什么我们的一个监听器处理偶数,一个监听器处理奇数,因为其先分配好了都

想要解决这个问题,我们可以在配置中配置下列信息

Spring:
	rabbitmq:
        listener:
          simple:
            prefetch: 1

其代表的意思是每个处理器都必须要处理完自己的1个消息之后才能向队列中获取新消息,这样我们就可以解决我们的消息分配不均的问题,配置好上面的代码之后我们再来重新启动测试我们的代码,会发现此时我们的代码就能成功发挥作用了

FanoutExchange

我们前面学习的消息队列模型都是将一个消息供给给一个消费者使用的,现在我们要学习一个消息供给给多个消费者使用的模型,这个模型和发布订阅有关,其实现的方式是publisher将消息发送给exchange(消费者),由消费者将消息转发给队列

这个交换机就类似于网吧中的管理机和上网机的关系,管理机可以管理具体的机子,也可以将消息发送给具体的机子,这里就相当于是通过exchange来进行对其他机子的管理

image-20220902144229337

我们常见的exchange类型包括三种,分别是Fanout(广播)、Direct(路由)、Topic(话题) ,我们下面将分别实现这三种模型

最后值得一提的是exchange只负责将消息转发给对应的队列,也就是路由,不具有存储功能,如果路由失败,那么消息就会丢失

首先我们来实现Fanout Exchange,先来看看其结构

image-20220902145251997

该消息模型会将接受到的消息路由到每一个和他绑定的queue

先来看看我们的实现步骤

image-20220902150237739

然后我们再来看看其下的各种类的结构关系,还有我们本次要实现的结构

image-20220902150327174

那么首先我们需要在consumer中创建一个类,先添加Configuration注解,我们在其下创建我们的队列和交换机

@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");
    }

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

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

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

我们这里首先创建交换机,其方法类型是FanoutExchange,我们内部直接返回一个新的FanoutExchange对象即可,括号内填入我们的交换机的名字,下面我们则创建对应队列的代码,绑定的代码我们则是返回一个Binding对象,方法内传入要绑定的队列和交换机,然后调用BindingBuilder内的bind方法,传入队列,再调用其下的to方法,传入交换机对象即可

剩下的代码则是对第二个队列的创建和绑定

然后我们写入我们的发送者的测试代码如下

@Test
public void testSendFanoutExchange() {
    //交换机名称
    String exchangeName = "itcast.fanout";
    //消息
    String message = "hello, every one!";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"",message);
}

我们这里将消息发送给了交换机,中间传入空字符串,不知道有什么用没关系,先记着就行

最后我们在消费者中写入两个监听者,分别监听不同的队列

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

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

在控制台中我们可以看到两个队列的确正确获得了相同的消息

DirectExchange

接着我们来实现第二种模式,路由模式,路由模式需要交换机和队列都设置一个BindingKey,通过这个关键字可以将交换机与队列进行另一层的绑定,一个队列和路由可以设置多个关键字,关键字可以相同,发布者发布消息时,需要指定消息的RoutingKey,路由模式交换机会将消息路由到与RoutingKey相同的BindingKey队列

image-20220902153803064

那么接着我们就来实现这个案例,先来看看步骤

image-20220902153913756

此时如果我们还要用以前的Bean方法来创建对应的类就太麻烦了,我们这里来学习使用注解的方法在创建对应的消费者方法时同时进行队列和交换机的创建并进行绑定

@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);
}

首先我们创建对应的监听方法,然后在其上使用RabbitListener注解,该注解下能使用@QueueBinding注解,该注解下使用其value属性,在该属性下使用@Queue注解并指定名字即可对应队列的创建,而同理用exchange属性并使用Exchange注解即可创建对应的交换机,其默认实现是路由方式,如果我们想要其他的实现,可以通过type属性并使用ExchageTypes中的枚举类来进行指定。最后key则是关键字的设置,我们这里设置了两个关键字,在其下的交换机和队列会自动完成绑定

然后我们同样再绑定一个新的队列和关键字到交换机中,同时再设置一个监听者

最后我们可以写入我们的发送者的代码如下

@Test
public void testSendDirectExchange() {
    //交换机名称
    String exchangeName = "itcast.direct";
    //消息
    String message = "hello, red!";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"red",message);
}

我们这里指定我们的rotingKey为"red",那么最终这个消息就会被转发到两个队列中,并被对应的监听器监听器

TopicExchange

接着我们来学习最后一种模式,也就是话题模式,话题模式和路由模式非常相似,不同的是在话题模式中Queue和Exchange指定BindingKey时可以使用通配符,其中 *代表一个单词,而#代表0或多个单词

image-20220902172826146

我们首先来看看实现该案例的步骤

image-20220902172948066

首先我们通过注解构造我们的监听器的代码如下

@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);
}

这里我们要指定我们的交换机的类型为TOPIC,并且我们的关键字也按照话题模式的格式进行指定

接着我们可以写入我们的测试用例如下

@Test
public void testSendTopicExchange() {
    //交换机名称
    String exchangeName = "itcast.topic";
    //消息
    String message = "永失吾爱,举目破败";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"china.weather",message);
}

当我们指定为china.weather时,只有一个监听器能处理到消息,当指定为china.news时,两个监听器都可以,这是由于监听器的关键字设定而会发生的情况,可以通过我们指定的关键字实现只对一些特定的队列发送消息

消息转换器

我们之前往我们的队列中存入消息,我们会发现其实我们的队列需要的数据类型是Object类型的,也就是说,其实我们可以在队列中存入任意类型的数据,那我们现在就来试试

首先我们创建一个新的队列

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

然后我们在测试代码中创建一个Map对象,然后将该数据类型存入

@Test
public void testSendObjectQueue() {
    Map<String,Object> msg = new HashMap<>();
    msg.put("name","viego");
    msg.put("age","5000");
    rabbitTemplate.convertAndSend("object.queue",msg);
}

之后我们可以在MQ的管理网页上查看该队列,可以看到里面确实有消息了,但是我们查看的时候会发现不但对象很大,而且还根本看不出来这是啥玩意,这是因为在传输的过程中,JDK内部将其序列化了,这种默认的存储方式我们是不推荐的,不但不利于阅读,还浪费空间,我们推荐使用MessageConverter来处理对象,使用JSON方式来对对象进行序列化

来看看其实现步骤

image-20220903130953079

我们首先在父类工程中引入MessageConverter的依赖

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

然后我们在发送者模块中的启动类中引入其对应的Bean交由Spring管理

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

然后我们同样发送该消息,就可以在队列中查看到对应的json数据

那么我们要如何接受这种数据呢?我们来看看步骤

image-20220903131934540

首先我们同样要引入对应的依赖,然后也是引入Bean类型,接着我们写入我们的代码如下

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

可以看到我们这里接受的消息类型也是之前的类型,注意我们写入什么类型,接受的时候就要写出什么类型,要不然会出错

最后我们可以来做一个总结

image.png