SpringCloud

SpringCloud是什么

  1. SpringCloud是一个微服务框架,将一种分布式架构落地的方案
    1. 单体架构:业务所有功能集中在一个项目中开发,架构简单,但是耦合度高
    2. 分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。
  2. 将项目进行拆分后,需要考虑多个问题
    1. 服务拆分力度
    2. 服务集群地址如何维护
    3. 服务之间如何互相调用,也就是各个业务功能模块互相调用,需要注册中心进行维护
    4. 服务健康状态感知,也就是服务之间互相调用时,在一个服务出现问题时,其他服务需要感知并作出处理
    5. 微服务配置,需要统一在配置中心进行配置
    6. 用户访问,则需要通过一个统一的服务网关进行访问,由网关将请求路由到微服务集群

知识栈

远程调用

  1. 远程调用就是指一个业务的功能需要访问另一个业务的数据,此时需要用到远程调用,而不能直接去另一个模块的数据库去取,因为每一个业务模块都独立的。

示例

订单业务

订单服务项目目录

  1. 服务启动后,查询订单数据

    订单数据

用户业务

用户服务目录结构

  1. 服务启动后,查询用户id为1的

    id为1的数据

远程调用

  • 我们需要在查询到订单数据时,连带的查询用户数据库中与订单数据里的userId相匹配的人物信息,由于每个模块是独立的,所以不能直接去用户数据库中进行访问,所以可以通过模拟发送请求的方式来获取对应的用户信息。
注册RestTemplate
  1. 在OrderApplication.java中创建RestTemplate并注入Spring容器

    @SpringBootApplication
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }

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

    }
  2. 在Controller中将请求回来的订单数据中的id作为参数,手动发送请求,获取对应的用户数据

    @RestController
    @RequestMapping("order")
    public class OrderController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
    Order order = orderService.queryOrderById(orderId);
    // user服务在8081端口,将查询到的ID作为参数发送请求
    String url = "http://localhost:8081/user/" + order.getUserId();
    //get请求用getForObject,post则用postForObject
    User user = restTemplate.getForObject(url, User.class);
    order.setUser(user);
    // 根据id查询订单并返回
    return order;
    }
    }

  3. 结果

    远程调用结果

Eureka注册中心

  1. 上面的这种方法进行远程调用,可以看到有许多硬编码信息,这是不合理的,所以我们可以通过注册中心,管理服务。

  2. 服务提供者(被访问的服务)启动时,都会在注册中心注册自己的信息,并交给eureka保存,服务消费者(访问别人的服务)根据服务名称从注册中心获取需要的服务

  3. 如果注册中心返回消费者多个服务,则消费者利用负载均衡从服务列表中选择一个

  4. 提供者每隔30s向注册中心发送心跳请求,以保持连接,心跳不正常的则被剔除,以保证消费者获取的都是最新的信息

  5. 在Eureka架构中主要分为两类

    1. EurekaServer:注册中心,也就是服务端,记录服务信息,心跳监控
    2. EurekaClient:客户端,也就是所有的服务
      • Provider:服务提供者
      • Consumer:服务消费者

Eureka注册中心使用

Eureka服务搭建

  1. 新建项目模块eureka-service,引入spring-cloud-starter-netflix-eureka-server依赖,这里使用maven8版本,创建项目默认的11版本会报错

    <parent>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>cloud-demo</artifactId>
    <version>1.0</version>
    </parent>

    <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
    <!-- eureka服务器依赖 这里不用指定版本,因为其父工程的配置文件中已经指定了版本-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    </dependencies>

    项目目录

  2. 添加注解。创建Main文件,并在Main文件中添加注解**@EnableEurekaServer使用nacos也要添加该注解**,开启注册中心服务

    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaApplication.class, args);
    }
    }
  3. 新建application.yml文件,进行服务配置,这里eureka自己也是一个服务,所以eureka服务信息也需要配置,把自己注册到eureka

    server:
    port: 10086
    spring:
    application:
    name: eurekaserver
    eureka:
    instance:
    hostname: localhost
    client:
    service-url: ## eureka server的地址信息
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  4. 配置完成之后,启动eureka服务的Main文件,访问localhost:10086

    eureka服务信息

Eureka服务注册

  1. 其他服务注册,首先pom中引入eureka-client包

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. application.yml配置文件中进行服务注册

    server:
    port: 8081
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/springcloud_test?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    application:
    name: userservice

    eureka:
    instance:
    hostname: localhost
    client:
    service-url: ## eureka server的地址信息(这里是服务器地址信息)
    defaultZone: http://${eureka.instance.hostname}:10086/eureka/
  3. 刷新localhost:10086,可以看到其他两个服务也注册上了

    eureka信息

服务发现

  1. 现在我们进行服务间的访问,和远程调用示例实现的功能一样

  2. 修改controller中的代码,将url的访问路径由ip+端口的模式修改为服务名称

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
    Order order = orderService.queryOrderById(orderId);
    String url = "http://userservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);
    order.setUser(user);
    return order;
    }
  3. 在启动类OrderApplication.java的中为RestTemplate添加负载均衡注解

    /**
    * 创建RestTemplate并注入Spring容器
    * @return
    */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }
  4. 访问该接口,也可以返回用户的信息。

负载均衡(Eureka)

  1. 上文中的RestTemplate是对httpClient的封装,类似于axios,可以通过它发送请求

  2. 在创建RestTemplate的地方添加@LoadBalance注解,那么使用RestTemplate发送的请求都会被Ribbon拦截

  3. 当一个http://userservice/user/的请求发送过来,到Ribbon,Ribbon将其中的userservice发送到eureka-service获取对应的服务地址,然后进行访问,流程如下所所示

    访问流程

  4. 负载均衡就是在访问服务器的时候,保证访问量的一个均衡

负载均衡示例

复制一个userserver服务

  1. 右键userserver,复制一个服务,注意改变端口,启动

    新建userServer2服务

访问orderserver

  1. 访问6次userserver,可以看出,1,3,6访问8081服务,2,4,5访问8082服务
8081服务 8082服务

结论

  1. 配置LoadBalance注解,则开启负载均衡,默认负载均衡规则为轮询,除轮询外,还有一下负载均衡规则可以配置

负载均衡规则

负载均衡规则配置

在应用文件中配置

  1. 在OrderApplication中配置

    @SpringBootApplication
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }

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

    // 配置随机规则,规则接口是IRule
    @Bean
    public IRule randomRule(){
    return new RandomRule();
    }
    }
  2. 重启order服务后,重新访问6次可以看出,4,6在8082端口访问,1,2,3,5在8081端口访问

    8081服务访问情况

    8082服务访问情况

配置文件配置

  1. 在order-service服务的配置文件application.yml中添加以下配置

    userservice: #对userservice开启以下负载均衡规则
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机负载均衡策略
  2. 这种方式也开启了随机负载均衡策略

不同

  1. 第一种方式是对所有的服务请求都开启了随机负载均衡
  2. 第二种方式可以指定相应的微服务。

负载均衡加载方式

  1. Ribbon默认加载方式为懒加载,第一次访问时才会去创建负载均衡的这个东西,所以第一次访问时,时间会很长

  2. 饥饿加载则会在项目启动的时候创建,降低第一次访问的耗时。

  3. 饥饿加载的配置在配置文件application..yml中配置

    # 指定服务配置负载均衡
    userservice:
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机负载均衡策略
    # 配置加载方式
    ribbon:
    eager-load:
    enabled: true # 开启懒加载
    clients: userservice #使用这种加载方式的服务

Nacos注册中心

服务启动

  1. 安装完Nacos之后,到bin文件夹下,打开cmd,运行下面命令即可启动Nacos

    .\startup.cmd -m standalone

服务使用

  1. 在父工程pom.xml中添加alibaba的依赖,Nacos是属于阿里巴巴的

    <!-- Nacos管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2021.0.5.0</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
  2. 将子工程中引入的eureka依赖删除

  3. 子工程的pom文件中引入nacos客户端依赖

    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  4. 修改application.yml配置文件,配置nacos服务的地址信息

    spring:
    cloud:
    nacos:
    server-addr: 10.130.15.112:8848 #nacos服务器地址
    application:
    name: coupon # 在 Spring Boot 应用程序中,如果不指定 spring.application.name 属性,应用程序将无法在注册中心(如 Eureka、nacos 等)进行注册。spring.application.name 用于标识应用程序的名称,这是注册中心用来区分不同服务的关键属性。
  5. 之后启动服务,并打开Nacos可视化界面,可以看到服务都已被注册,并可正常访问

    启动的服务

服务列表

Nacos服务分级存储模型

  1. 一级是服务,二级是集群(一般指地域),三级是实例

  2. 一个userserver服务,有三个实例,两个在第一个集群中,一个在第二个集群中。

  3. 设置实例的集群属性:在application.yml中添加spring.cloud.nacos.discovery.cluster-name属性即可。将userservice的集群先设置成HZ,启动8082的实例,在改变集群为BJ,启动8081端口的实例

    spring:
    cloud:
    nacos:
    server-addr: 127.0.0.1:8848
    discovery:
    cluster-name: HZ #集群名称为杭州
  4. 在Nacos上点击服务的详情,可以看到对应集群的服务

    集群分布

Nacos负载均衡

  1. Nacos默认的负载均衡策略也为轮询负载均衡,当我们配置orderservice集群为HZ,并启动服务,希望负载均衡策略优先考虑属于同一集群的服务,这时需要去配置负载均衡的策略了。

  2. 在orderservice下配置负载均衡规则

    userservice: #针对哪个服务
    ribbon:
    # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机负载均衡策略
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 同集群负载均衡
  3. 再次启动orderservice服务,并访问,可以看到访问的都为同一集群的服务,当同一集群的服务都关掉时,访问其他集群的服务,并会爆出警告

Nacos权重

  1. 当属于同一集群的服务性能不同时,我们希望更多的请求访问性能越好的,此时需要配置属于同一集群的服务的权重来实现,如下所示

    权重配置

  2. 权重值的设置一般在0-1之间,权重越高,被访问的频率越高,权重设置为0则完全不会被访问

Nacos环境隔离

  1. 上面说过Nacos分级模型一级是服务,二级是集群(一般指地域),三级是实例,除此之外,在公司内部,还会有不同的环境,比如开发环境,测试环境等等,此时对于不同环境的服务,需要进行环境隔离,设置命名空间,除此之外,同一命名空间内部,关联交强的可以设置同一组内。

  2. 设置命名空间,首先需要在Nacos上新建一个命名空间

    新建命名空间

    展示

  3. 在orderservice的application.yml中进行配置,将其放到dev环境中

    spring:
    cloud:
    nacos:
    server-addr: 127.0.0.1:8848
    discovery:
    cluster-name: HZ
    namespace: 49430540-2aa3-46a2-9438-e4d53806fb57 # 命名空间ID
  4. 此时再次启动服务,由于有环境隔离,所以是访问不到usersevice服务的

Nacos配置管理服务

  1. 通过在Nacos平台新建配置文件,让服务进行读取,对于一些通用的配置,可以不用在每个微服务下都进行配置,并且直接在Nacos更改配置,可以实现配置热更新,不用重启微服务(之前更改微服务的配置文件,需要重启微服务)

    新建配置

    新建配置

  2. 新建配置之后,就可以去微服务进行配置加载Nacos配置文件

  3. 微服务获取配置的流程

​ 项目启动 ===> 读取本地配置文件 ===> 创建Spring容器 ===> 加载bean

  1. 在新建了Nacos配置后,我们需要微服务加载该配置,所以流程需要更改。

    ​ 项目启动 ===> 读取Nacos配置文件 ===> 读取本地配置文件 ===> 创建Spring容器 ===> 加载bean

  2. 由于Nacos地址信息都在本地配置文件中,读取Nacos配置文件,必须得要先知道Nacos地址信息,所以我们需要新建一个bootstrap.yml文件,该文件为引导文件,优先级高于本地配置文件applicatioon.yml,在bootstrap配置文件中进行Nacos配置

微服务中加载Nacos配置管理服务

在pom文件中引入Nacos配置管理依赖

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

resources目录下新建bootstrap.yml文件并对Nacos进行配置

  1. 服务名称,环境名称,后缀名都是和配置列表的名称一一对应的。

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

    对应的配置名称

  2. 在Nacos下配置的配置文件如下,也就是日期格式化的配置。

    pattern:
    dateformat: MM-dd HH:mm:ss:SSS

测试配置

  1. 为了测试这个配置是否生效,可以通过写一个接口来测试,配置文件中的属性通过@Value读取注入,访问now接口时,会返回一个格式化日期

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {

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

    @GetMapping("now")
    public String now() {
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
    }
  2. 访问localhost:8081/user/now

    访问结果

新版本配置方式

  1. Spring Cloud Alibaba 2.2.5 版本及以上,Spring Boot 2.4 及以上版本中,配置中心的配置方式发生了一些变化。你需要在配置文件中添加 spring.config.import 属性来指定 Nacos 配置中心。

添加nacos配置依赖同上

在application.yml添加spring.config.import 属性

spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos注册中心的地址
config:
server-addr: 127.0.0.1:8848 # nacos配置中心地址
file-extension: yaml # 配置文件后缀名
config:
import: # coupon.yaml代表nacos上的Data Id,虽然配置了refreshEnabled=true,但是需要热更新还是需要在控制类上添加注解@RefreshScope
- nacos:coupon.yaml?refreshEnabled=true&group=DEFAULT_GROUP # 从nacos中导入配置文件
application:
name: coupon

使用方式同上,使用@Value注解读取配置

Nacos配置热更新实现

在注入配置属性的类上方加入@RefreshScope注解

  1. 增加@RefreshScope后,更改了Nacos配置文件后,不用重启微服务即可更新
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope //在@Value所在的类上方增加注解
public class UserController {

@Autowired
private UserService userService;

@Value("${pattern.dateformat}") //在这里注入的属性
private String dateformat;

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

新建类实现

  1. 在SpringBoot文件中,有讲到,读取配置文件可以通过新建一个类,并通过@ConfigurationProperties注解实现读取,通过这种方式读取配置文件不需要添加其他东西即可实现热更新

Nacos配置共享

  1. 服务加载读取Nacos的时候,主要读取两个文件,一个是 [服务名称]_[环境名称].[后缀名],例如userservice-dev.yaml,另一个是[服务名称].[后缀名],例如userservice.yaml
  2. 当不同环境的服务拥有相同的配置时,可以将该配置以第二种方式命名来实现

配置共享实现

  1. Nacos下新建userservice.yaml

userservice.yaml配置

  1. 由于使用@Value读取配置文件的方式在未配置该属性的时候会报错,所以使用新建配置Pattern类的方式来实现

    @Data
    @Component
    @ConfigurationProperties(prefix = "pattern")
    public class Pattern {
    private String dateformat;
    private String name;
    }
  2. 在controller中,通过自动注入来进行属性的读取

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private Pattern pattern;

    @GetMapping("profile")
    public ArrayList<String> profile() {
    ArrayList<String> list = new ArrayList<>();
    list.add(pattern.toString());
    return list;
    }

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

  3. 将8081服务的userservice的环境设置为dev,8082端口的userservice服务设置为test环境,启动服务并访问

  4. 访问结果如下

8081结果

8082结果

  1. 可以看到对于共享的属性,不管哪个环境都可以访问到,dev环境的配置只有dev环境的可以访问到。

Nacos集群配置

  1. 在上面启动Nacos时的命令为.\startup.cmd -m standalone,因为Nacos是默认集群模式启动,所以单例模式下需要加上-m standalone。

  2. 集群架构

    1. Nacos节点的负载均衡使用nginx实现
    2. 这里的Nacos三个节点应该是部署在三台不同的服务器上的,这里由于条件有限,所以使用复制粘贴的方式,使用不同的端口模拟Naco服务节点。三个Nacos服务节点的端口分别为8845,8856,8847
    3. 使用Mysql数据库集群取代Nacos的内置数据库

    集群架构

Nacos集群配置流程

准备

  1. 新建nacos数据库,并创建多个mysql表

    mysql中的表

  2. ,首先将nacos=>config目录下配置文件cluster.config.example,更名为cluster.config,并在该文件中添加三个Nacos节点的地址,高版本Nacos不要配置连续的地址,不然会被错

    127.0.0.1:8843
    127.0.0.1:8845
    127.0.0.1:8847
  3. 修改application.properties文件,将Mysql的配置取消注释

    spring.datasource.platform=mysql

    ### Count of DB:
    db.num=1

    ### Connect URL of DB:
    db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user.0=root
    db.password.0=root

启动

  1. 将Nacos复制三份,模拟三个nacos节点,并在各自的application.properties文件中修改各自的端口号

    server.port=8843
    server.port=8845
    server.port=8847
  2. 分别在cmd启动三个Nacos节点

    startup.cmd
  3. 配置Nginx反向代理,下载Nginx后,进入config-nginx.config,添加以下代码

    http {
    include mime.types;
    default_type application/octet-stream;

    #新增的 配置集群地址
    upstream nacos-cluster {
    server 127.0.0.1:8843;
    server 127.0.0.1:8845;
    server 127.0.0.1:8847;
    }

    sendfile on;

    keepalive_timeout 65;


    server {
    listen 80;
    server_name localhost;

    #新增的 使用/nacos代理集群地址,并负责负载均衡
    location /nacos {
    proxy_pass http://nacos-cluster;
    }
    }
    }
  4. 启动Nginx

    start nginx.exe
  5. 由于设置了反向代理,直接访问localhost/nacos,就可以访问nacos,80端口默认省略

  6. 服务中配置nacos端口则也是80,通过80访问到nginx,nginx通过负载均衡随机分配到nacos服务器上

总结

  1. 使用nginx会有负载均衡效果,所以nginx会在三个nacos下做一个负载均衡的效果,默认轮询负载均衡

  2. 在Nacos新建的服务配置,命名空间等都会存入对应的表中,但是现在新建服务配置会失败,可能和nacos,mysql版本对应有关系。存入数据库中,其他Nacos也可以访问该配置。

    各配置存储的地方

  3. 微服务中配置的nacos地址为nginx地址

正向代理和反向代理

  1. 正向代理

    用户 ===> 代理服务器 ===> 服务地址

  2. 反向代理,用户是不感知服务被代理的

    用户 ===> 反向代理地址 <=== (服务地址1,服务地址2,服务地址3)

Feign

  1. Feign为一个http客户端,之前我们使用的是RestTemplate,现在使用Feign来进行发送请求

相比于RestTemplate的不同

  1. 之前使用RestTemplate发送请求,参数复杂时难以维护

    String url = "http://userservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);

使用Feign进行http请求(微服务之间进行请求)

  1. 引入依赖,

    <!--   Feign依赖 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- 引入负载均衡依赖,不然会报错 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
  2. 添加注解,在启动文件中添加注释@EnableFeignClients

    @SpringBootApplication
    @EnableFeignClients // 开启Feign客户端,可以指定需要扫描的package,例如:@EnableFeignClients(basePackages = "com.qr.mall.member.feign"),不指定则扫描整个项目中的所有Feign客户端接口。可能会导致命名冲突。
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }
    }
  3. client文件夹下新建UserClient(要远程调用user服务,就写UserClient)接口文件,编写Feign客户端,客户端需要声明以下信息,和Restful规则一样

    1. 服务名称
    2. 请求方式
    3. 请求路径
    4. 请求参数
    5. 返回值类型
    @FeignClient("userservice") //远程调用客户端服务名称
    public interface UserClient {
    // 想要调用的服务的签名,也就是他的mapping和方法的第一行复制过来即可
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
    }
  4. Controller中使用

    @RestController
    @RequestMapping("order")
    public class OrderController {

    @Autowired
    private OrderService orderService;
    @Autowired
    private UserClient userClient;

    // feign调用
    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
    Order order = orderService.queryOrderById(orderId);
    User user = userClient.findById(order.getUserId());
    order.setUser(user);
    // 根据id查询订单并返回
    return order;
    }
  5. 这样也可以正常调用,使用Feign使代码看起来更优雅

Feign自定义配置

  1. 日志打印自定义的两种配置方式,两种配置都可以规定该配置是针对于某个服务或者全局的
    1. 配置文件中配置
    2. 自定义类,通过在启动类的@EnableFeignClients注解中添加该类的类名来加载配置的日志类

Feign性能优化

更换Feign底层http客户端

  1. Feign底层使用的http客户端为URLConnection,该客户端不支持连接池,所以可以将其更换为httpClient或者OKHttp

  2. 更换为httpClient客户端

    1. 导包
    <!--     引入httpClient依赖 -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    </dependency>
    1. 配置Feign

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

Feign配置抽取

  1. 当我们有多个模块都需要引入用户信息,都需要写一个client,POJO类,都是重复的配置,所以可以将其抽取为一个单独的模块,通过在配置页面中引入坐标来加载需要的配置

配置抽取实现

  1. 新建module,将一些公共部分进行抽取,像POJO类,Client文件

    feign-api模块目录结构

  2. 在服务的配置文件中引入feign-api坐标

    <!-- 对应的封装feign-api -->
    <dependency>
    <groupId>cn.qr.feign</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
    </dependency>
  3. 将微服务中引入feign-api那些文件的地方进行引入路径的修改

  4. 由于Spring扫描路径默认为启动类所在的包,例如order-service,spring默认扫描的包为cn.qr.order,而feign-api中clients文件夹下的UserClient路径为cn.qr.feign,所以此时启动order-service服务会报错,找不到这个类,所以需要在orderservice的启动文件的@EnableFeignClients注解后,增加要加载客户端类

    @SpringBootApplication
    @EnableFeignClients(clients = UserClient.class) // 开启Feign客户端
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }
    }
  5. 服务可以正常启动,并且其他微服务也可以复用该api配置

网关Gateway

  1. 网关的作用
    1. 对用户进行身份认证,权限校验
    2. 将用户请求路由到微服务,并实现负载均衡
    3. 对用户请求进行限流

搭建网关服务

新建module gateway

  1. 目录结构如图所示

    gateway目录结构

  2. 将Main文件配置为SpringBoot启动类,或者直接新建一个SpringBoot的模块(配置就直接配好了),并在pom配置文件中引入网关依赖和Nacos服务发现依赖,引入nacos依赖是因为网关服务也需要注册到nacos。

    <dependencies>
    <!-- 网关依赖 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- nacos服务发现依赖 -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    </dependencies>
  3. 在application.yml中进行配置

    server:
    port: 10010 # 网关端口
    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: localhost:8848 # nacos地址
    gateway:
    routes:
    - id: user-service # 路由id
    uri: lb://userservice # uri指向的服务 lb代表负载均衡 userservice是服务名
    predicates:
    - Path=/user/** # 断言,路径相匹配的进行路由,判断是否以/user开头
    - id: order-service
    uri: lb://orderservice
    predicates:
    - Path=/order/**
  4. 启动服务后,通过localhost:10010/user/1 ,localhost:10010/order/1 进行访问,通过网关进行访问

  5. 路由服务流程

    服务流程

网关过滤器

  1. 对请求和响应的结果进行过滤,可以添加请求头,响应头等等操作

使用

  1. 网关中对order服务添加过滤器

    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: localhost:8848 # nacos地址
    gateway:
    routes:
    - id: user-service # 路由id
    uri: lb://userservice # uri指向的服务 lb代表负载均衡 userservice是服务名
    predicates:
    - Path=/user/** # 断言,路径相匹配的进行路由,判断是否以/user开头
    - id: order-service
    uri: lb://orderservice
    predicates:
    - Path=/order/**
    filters:
    - AddRequestHeader=token, blue # 过滤器,添加请求头
  2. 在controller中添加参数@RequestHeader(value = “token” , required = false) String token并打印,这个token必须和配置文件中逗号前的名称相同

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId , @RequestHeader(value = "token" , required = false) String token) {
    System.out.println("token = " + token);
    Order order = orderService.queryOrderById(orderId);
    User user = userClient.findById(order.getUserId());
    order.setUser(user);
    // 根据id查询订单并返回
    return order;
    }
  3. 通过配置defaultFilters属性,与routes在同一层级下,可以配置对所有服务都生效的过滤器

全局过滤器

  1. 相比于gatefilter,全局过滤器是通过自己写代码配的,而网关过滤器是通过配置文件的方式实现的

全局过滤器实现

  1. 网关服务中新建一个过滤器文件,这里新建名为AuthorizeFilter的过滤器

    @Order(1) //过滤器的优先级,数字越小,优先级越高
    @Component //注入到Spring中
    public class AuthorizeFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    /**
    * @param exchange 请求上下文,里面可以获取Request Response等信息
    * @param chain 过滤器链,可以通过chain继续向下执行,传递给下一个过滤器
    * @return {@code Mono<Void>}返回表示当前过滤业务结束
    */
    // 获取请求参数,是query参数中的第一个参数authorization是否为admin
    ServerHttpRequest request = exchange.getRequest();
    MultiValueMap<String, String> queryParams = request.getQueryParams();
    // 获取authorization参数
    String authorization = queryParams.getFirst("authorization");
    // 判断authorization值是否为admin
    if("admin".equals(authorization)){
    // 放行
    return chain.filter(exchange);
    }
    // 不是admin,设置状态码为401,UNAUTHORIZED代表的就是401
    exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
    // 直接结束
    return exchange.getResponse().setComplete();
    }
    }
  2. 重启网关后,正常访问localhost:10010/order/101,报401,访问localhost:10010/order/101?authorization=admin才会返回数据

过滤器执行顺序

执行顺序

网关跨域问题处理

  1. application.yml中添加以下配置代码

    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: localhost:8848 # nacos地址
    gateway:
    routes:
    - id: user-service # 路由id
    uri: lb://userservice # uri指向的服务 lb代表负载均衡 userservice是服务名
    predicates:
    - Path=/user/** # 断言,路径相匹配的进行路由,判断是否以/user开头
    - id: order-service
    uri: lb://orderservice
    predicates:
    - Path=/order/**
    filters:
    - AddRequestHeader=token, blue # 过滤器,添加请求头
    # 跨域
    globalcors:
    corsConfigurations:
    '[/**]':
    allowedOrigins: "*" # 允许所有域名
    allowedMethods: # 允许的请求方式
    - GET
    - POST
    - PUT
    - DELETE
    allowedHeaders:
    - "*"
    allowCredentials: true # 是否允许携带cookie
    maxAge: 1800 # 跨域时间
    add-to-simple-url-handler-mapping: true # option请求被拦截问题

Docker

  1. Docker将开发中的应用,依赖,函数库,配置一起打包,形成可移植的镜像
  2. Docker应用 运行在容器中,使用沙箱机制,相互隔离
  3. Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的linux内核,因此可以在任意Linux操作系统上运行,Linux内核版本不能低于3.10
  4. 镜像:Docker将应用程序等东西打包在一起称为镜像
  5. Dockerhub:Docker镜像的托管平台,这样的平台称为Docker Registry

Docker服务命令

#安装Dokcer

systemctl start docker # 每次使用都需要开启服务 启动docker服务

systemctl enable docker # 设置Dokcer服务每次开机自启

systemctl stop docker # 停止docker服务

systemctl restart docker # 重启docker服务

Docker命令

# 构建镜像
docker build
# 查看镜像
docker images
# 删除镜像
docker rmi
# 推送镜像到服务器
docker push
# 从服务器拉取镜像
docker pull
# 保存镜像为压缩包
docker save
# 加载压缩包为镜像
docker load
# 更新707容器每次开机自启
docker update 707 --restart=always

Dockers容器命令

docker ps # 查看所有运行的容器及状态
docker logs # 查看容器运行日志
- docker logs mn # mn:容器名字
- docker logs -f mn #-f代表日志可以热更新
docker exec # 进入容器执行命令
- docker exec -it mn bash # -it代表进入容器后创建终端,mn代表容器名字,bash代表终端类型,像redis进入容器后,还要进入redis中,所以bash可以直接替换成redis-cli,直接进入redis中

docker run # 运行容器(直接dockerhub上查看需要的命令)
- docker run --name mn -v html:/root/html -p 8800:80 -d nginx
# --name:指定名称
# -p:指定端口映射,将服务器的8080端口与容器的80端口映射,因为每个容器都是隔绝的,所以需要端口映射来访问
# -d:后台运行
# -v: 创建容器时可以使用-v参数来挂载一个数据卷到某个容器中目录


docker pause # 暂停
docker unpause #取消暂停

docker stop # 停止
- docker stop mn
docker start #重新生成容器
docker rm #删除容器
- docker rm mn

mysql容器

# 启动mysql,--restart=always自动启动 -d后台运行 -p端口映射 -e环境变量设置,设置登录密码. -v代表挂载 将mysql容器内部的/var/log/mysql /var/lib/mysql /etc/mysql挂载到宿主机上的/mydata/mysql/log /mydata/mysql/data /mydata/mysql/conf位置,以后更改不用进容器内部修改,这些文件主要是日志,数据以及配置文件,最后的mysql代表启动哪个mysql版本
docker run --restart=always -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD="123zhao." \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql mysql
# 进入容器,-i:表示以交互模式运行容器内的命令,允许输入数据,
# -t:表示分配一个伪TTY终端,让命令运行在一个交互式的终端中。
# bash:终端类型
docker exec -it mysql bash
# 进入终端之后 登录mysql
mysql -uroot -p"123zhao."
# 登录之后,查看数据库
show databases

# firewall-cmd:是Firewalld防火墙的命令行管理工具。
# --zone=public:指定作用域为“public”区域,这是Firewalld预设的安全区域之一,用于定义不同网络区域的不同安全等级。
# --add-port=3306/tcp:表示允许TCP协议下3306端口的流量通过防火墙。在这里,3306是MySQL数据库服务的默认端口。
# --permanent:标志表示这个规则应该是持久化的,即重启防火墙服务后规则仍然有效。
firewall-cmd --zone=public --add-port=3306/tcp --permanent
#重启防火墙
systemctl restart firewalld.service
firewall-cmd --reload
# 连接数据库后,使用哪个表
use gym;
# 展示所有表
show tables;

redis容器

# 由于redis容器中/etc/redis并没有redis.conf文件,所以会导致在宿主机中将redis.conf当为目录,而不是文件,因此需要提前创建文件
mkdir -p /mydata/redis/conf # 创建文件夹
touch /mydata/redis/conf/redis.conf # 创建文件
# 在配置文件中持久化数据,redis中的数据一般是存储在内存中的,重启之后就会消失,可以通过配置将其持久化到硬盘中
# redis.conf中添加一行:appendonly yes
# 启动服务
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf # 服务启动时,读取/etc/redis/redis.conf配置文件

Tomcat命令

# 启动一个名为 "my-tomcat" 的 Tomcat 容器,并将容器内 8080 端口映射到主机的 8080 端口
# -d 表示在后台运行容器(守护进程模式)
# -p 8080:8080 将容器内部的 8080 端口映射到主机的 8080 端口
# --name tomcat 设置容器的名称为 "tomcat"
# -e TZ=Asia/Shanghai 设置环境变量TZ为上海时区,以便容器内应用正确处理时间
# tomcat:latest 是要使用的镜像标签,这里表示使用最新版本的 Tomcat 镜像
docker run -d -p 8080:8080 --name tomcat -e TZ=Asia/Shanghai tomcat:10.1.19

nginx容器

# docker run: 执行Docker容器运行命令。
# -d: 后台模式运行容器,容器会在后台启动并持续运行。
# --name nginx: 为容器指定一个名字,便于管理和识别,此处命名为 mynginx。
# --restart=always: 设置容器的重启策略为“总是重启”,这意味着无论容器因何种原因退出,Docker都将尝试重新启动它。
# -v /home/nginx/nginx.conf:/etc/nginx/nginx.conf: 使用数据卷挂载功能,将宿主机的 /home/nginx/nginx.conf 文件挂载到容器的 /etc/nginx/nginx.conf 位置。这样,容器内的Nginx将使用宿主机的配置文件,从而实现配置的持久化。
# -v /home/nginx/default.conf:/etc/nginx/conf.d/default.conf: 类似地,将宿主机的 /home/nginx/default.conf Nginx配置文件挂载到容器的 /etc/nginx/conf.d/default.conf。conf.d目录下存放的是额外的server块配置文件,这些配置会被主配置文件(nginx.conf)引用并合并生效。
# -p 81:80: 端口映射,将宿主机的81端口映射到容器的80端口,使得外界可以通过宿主机的81端口访问到容器内部运行的Nginx服务。
# -t: 给容器分配一个伪TTY终端,这对于交互式容器有用,但对于Nginx这类服务型容器不是必须的。
# nginx: 指定使用的镜像名称,这里是官方的Nginx镜像。
docker run -d --name nginx --restart=always -v /mydata/nginx/html:/usr/share/nginx/html -v /mydata/nginx/logs:/var/log/nginx -v /mydata/nginx/conf:/etc/nginx -p 80:80 -t nginx

EacticSearch容器

mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
# 将配置写入/mydata/elasticsearch/config/elasticsearch.yml
echo "http.host:0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

# 9200: 给elasticsearch发请求的端口
# 9300: elasticsearch在分布式集群状态下节点之间的通信端口
# discovery.type=single-node: 单节点模式运行
# ES_JAVA_OPTS="-Xms64m -Xmx512m": 运行后初始占用64M,最大占用128M内存
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

权限问题

# 文件挂载后启动服务,没有权限时,使用以下命令
# -R代表递归
# 777代表读写执行权限都允许
# /mydata/elasticsearch : 代表elasticsearch下的所有文件
chmod -R 777 /mydata/elasticsearch

Kibana容器(EacticSearch的可视化界面)

# ELASTICSEARCH_HOST=http://192.168.202.1:9200: 代表EacticSearch服务地址和端口
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.202.1:9200 -p 5601:5601 -d kibana:7.4.2

后端服务

# root文件夹下,构建当前文件夹下的Dockerfile
docker build -t gym-server .
# 启动后端服务
docker run -d --name gym-server -p 8080:8080 -e TZ=Asia/Shanghai --restart=always gym-server

#删除为none的镜像
docker rmi $(docker images -q --filter "dangling=true")

数据卷Volume

  1. 容器与容器中的数据耦合度过高,像nginx中的index.html文件,不能使用vi编辑,很不方便,所以数据卷就是解决这个问题,数据卷将容器中的数据映射到宿主机上的某个位置。两个数据双向绑定。除此之外,多个容器之间的数据共享也可以通过这个实现了,只需要全部指向同一个数据卷即可。

数据卷命令

docker volume [command]
- [command]:
create # 创建一个数据卷
inspect # 显示一个或多个volume信息
ls # 列出所有volume
prune # 删除未使用的volume
rm # 删除一个或多个指定的volume

不使用数据卷

  1. 其实我们可以自己创建目录,在运行容器的时候使用-v进行挂载,相比于数据卷,我们可以自定义目录位置,比较方便
  2. 区别
    1. 数据卷自动化管理,不用操心目录的管理
    2. 自己创建目录自己可以随意创建目录位置,但是需要目录管理

Dockerfile自定义镜像

  1. 镜像时分层的,每一层称为layer

    1. BaseImage层:包含基本的函数库,环境变量,文件系统
    2. Entrypoint层:入口,是镜像中应用启动的命令
    3. 其他:在BaseImage基础上添加依赖,安装程序,完成整个应用的安装和配置
  2. Docker基本操作,可以看出,我们可以通过对Dockerfile文件进行构建来生成镜像

    基本操作

  3. Dockerfile文件,类似于命令行文件,下面为一个Dockerfile文件,自定义一个Java项目的镜像

    # 指定基础镜像
    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
  4. 使用步骤

    1. 构建,通过docker build构建Dockerfile文件,-t代表文件的标签,也就是名字,.代表Dockerfile所在的文件目录

      docker build -t java:1.0 .
    2. 构建完成之后,镜像就被加载到本地了,就可以运行容器

  5. 在构建Java项目时,Dockerfile文件每次都要配置环境变量,很麻烦,所以java:8-alpine基础镜像就包含了上面的环境配置步骤。

    # 指定基础镜像
    FROM java:8-alpine

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

DockerCompose

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

  2. compose文件是一个文本文件,通过指令定义容器中的每个容器如何运行,也就是将docker run命令拆分为yml格式运行

  3. ll命令查看文件的权限,通过chmod命令增加权限

    chmod +x /usr/local/bin/docker-compose    #增加执行权限
  4. DockerCompse的使用

    1. 安装DockerCompose

    2. 自动补全命令,

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

      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"
    4. 之前学过的微服务,通过maven都打成jar包,将其制作为镜像

      文件目录

jar包和Dockerfile文件

FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar
  1. 由于各个微服务之间互相引用,之前都是使用localhost进行访问的,可能各个服务不再同一台机器上,所以需要改成服务名称访问

    nacos地址

改为通过名称访问

  1. 文件上传到虚拟机上后,执行命令

    docker-compose up -d

Docker私有镜像仓库

  1. 本地私有仓库部署

MQ(MessageQueue)

  1. 异步调用常见实现就是事件驱动模式,
    • 当使用同步调用,调用A事件后,需要执行B,C,D事件会导致如果再有后续事件添加不方便,耦合性较高,一个事件失败会导致剩余事件阻塞。
    • 异步调用,将事件交给一个中间人broker,所有事件都订阅broker,当A事件结束了,就返回给用户结果,通知broker,broker发送通知说A事件结束,订阅了broker的事件就会执行自己。一个事件执行失败不会影响其他事件的执行。
  2. MQ就是事件驱动架构中的中间人,MQ常见的实现方式
    • RabbitMQ
    • ActiveMQ
    • RocketMQ
    • kafka

RabbitMQ

图示:

RabbitMQ图示

概念

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

安装

  1. 找到mq镜像文件

  2. 在虚拟机中加载镜像

    docker load -i mq.tar
  3. 运行MQ容器

    docker run \
    -e RABBITMQ_DEFAULT_USER=root \
    -e RABBITMQ_DEFAULT_PASS=root \
    --name mq \
    --hostname mq1 \
    -p 15672:15672 \ (浏览器访问端口)
    -p 5672:5672 \ (用户连接端口)
    -d \
    rabbitmq:3-management

常见消息模型

HeloWorld案例

一对一

  1. publisher服务中

    1. 建立连接

      ConnectionFactory factory = new ConnectionFactory();
    2. 设置连接参数

      factory.setHost("192.168.202.1"); //虚拟机地址
      factory.setPort(5672);
      factory.setVirtualHost("/");
      factory.setUsername("root");
      factory.setPassword("root");
    3. 建立连接

      Connection connection = factory.newConnection();
    4. 创建通道

      Channel channel = connection.createChannel();
    5. 创建队列

      String queueName = "simple.queue";
      channel.queueDeclare(queueName, false, false, false, null);
    6. 发送消息

       String message = "hello, rabbitmq!";
      channel.basicPublish("", queueName, null, message.getBytes());
    7. 关闭通道和连接

      channel.close();
      connection.close();
  2. consumer服务中

    1. 建立连接

      ConnectionFactory factory = new ConnectionFactory();
      factory.setHost("192.168.202.1");
      factory.setPort(5672);
      factory.setVirtualHost("/");
      factory.setUsername("itcast");
      factory.setPassword("123321");
      Connection connection = factory.newConnection();
    2. 创建通道

      Channel channel = connection.createChannel();
    3. 创建队列

      String queueName = "simple.queue";
      channel.queueDeclare(queueName, false, false, false, null);
    4. 订阅消息:当queueName队列里有消息,后面的回调函数就会立即执行

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

SpringAMQP

  1. AMQP(Advanced Message Queuing Protocol),高级消息队列协议
  2. SpringAMQP基于AMQP协议的规范API,提供模版来接收和发送消息,包含两部分,spring-amqp是基础抽象,spring-rabbit是底层默认的实现。
  3. 由于rabbitmq的实现的代码过于复杂,所以出现了SpringAMQP

使用

  1. 引入依赖

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  2. 配置连接,在publisher服务中编写application.yml,添加mq连接信息

    spring:
    rabbitmq:
    host: 192.168.202.1 # RabbitMQ server IP
    port: 5672 # RabbitMQ server port
    username: root # RabbitMQ server username
    password: root # RabbitMQ server password
    virtual-host: / # RabbitMQ server virtual host
  3. 编写publisher测试,通过调用RabbitTemplate中的convertAndSend发送消息

    @Autowired
    private RabbitTemplate rabbitTemplate;

    String queueName = "simple.queue";
    String message = "hello spring amqp";
    rabbitTemplate.convertAndSend(queueName, message);
  4. consumer服务中除了在application.yml中新增连接配置外,还需要编写接收类来接收消息

    @Component
    public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void simpleQueueListener(String msg) {
    System.out.println("simple.queue receive msg: " + msg);
    }
    }

常见消息模型(续)

Work Queue

workqueue

  1. 主要是为了提高消息处理的速度,避免消息堆积,加入队列中一共50条消息,两个消费者各处理25条,比单个消费者处理50条消息速度要快。

  2. 实现直接在消费者服务中定义两个消费者即可

  3. 对于处理消息快的消费者,则可以多分担一点,但是rabbit中有个默认的机制:消息预取,当消息到达队列中时,消费者所属的通道就会拿一个过来排着,所以如果不进行配置,则不同处理速度的消费者所处理的消息数量是一样的。我们可以在application.yml中进行如下配置,就不会出现消息堆积。

    spring:
    rabbitmq:
    listener:
    simple:
    prefetch: 1 # The number of messages that can be processed at the same time
Publisher/Subscribe

Publisher-Subscribe

  1. 发布订阅模式允许将同一消息发送给多个消费者,实现方式是加入了交换机

  2. exchange负责消息路由,而不是存储,路由失败则消息丢失。常见的exchange类型包括

    1. fanout:广播,将接收到的消息路由到每一个跟其绑定的queue
    2. Direct:路由,将接受的消息根据规则路由到指定的Queue。每一个Queue都与Exchange设置一个BindingKey,发布者发送消息时指定一个RoutingKey,Exchange将消息路由到RoutingKey与BindingKey相等的Queue
    3. Topic:话题
  3. fanoutExchange

    1. 在consumer服务创建一个配置类,添加@configuration注解,并声明FanoutExchange,Queue和绑定关系对象Binding,代码如下

      @Configuration
      public class FanoutConfig {
      // 声明交换机
      @Bean
      public FanoutExchange fanoutExchange() {
      return new FanoutExchange("fanout.exchange");
      }

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

      // 绑定队列到交换机
      @Bean
      public Binding fanoutBinding1() {
      return BindingBuilder.
      bind(fanoutQueue1()).
      to(fanoutExchange());
      // return new Binding("fanout.queue1", Binding.DestinationType.QUEUE, "fanout.exchange", "", null);
      }

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

      // 绑定队列到交换机
      @Bean
      public Binding fanoutBinding2() {
      return BindingBuilder.
      bind(fanoutQueue2()).
      to(fanoutExchange());
      }
      }
    2. 在consumer中监听

      @Component
      public class SpringRabbitListener {

      @RabbitListener(queues = "fanout.queue1")
      public void simpleQueueListener(String msg) {
      System.out.println("fanout.queue receive msg: " + msg);
      }

      @RabbitListener(queues = "fanout.queue2")
      public void simpleQueue2Listener(String msg) {
      System.out.println("fanout.queue2 receive msg: " + msg);
      }
      }
    3. publisher中发送消息到交换机

      @Test
      public void testSendFanoutExchange() {
      String exchangeName = "fanout.exchange";
      String message = "hello fanout exchange";
      rabbitTemplate.convertAndSend(exchangeName, "", message);
      }
  4. DirectExchange

    1. 在消息监听类中利用@RabbitListener声明Exchange,Queue,RoutingKey(使用注解方式声明,是上面的使用Bean声明的简化版本,所以之前的单独的队列,也可以使用Bean进行声明)

      @Component
      public class SpringRabbitListener {

      @RabbitListener(bindings = @QueueBinding(
      value = @Queue(name = "direct.queue1"),
      exchange = @Exchange(name = "direct.exchange", type = "direct"),
      key = {"red","blue"} //key可以写多个,这个key为bindingkey
      ))
      public void listenerDirectQueue1(String msg) {
      System.out.println("direct.queue1 receive msg: " + msg);
      }



      @RabbitListener(bindings = @QueueBinding(
      value = @Queue(name = "direct.queue2"),
      exchange = @Exchange(name = "direct.exchange", type = "direct"),
      key = {"red","yellow"}
      ))
      public void listenerDirectQueue2(String msg) {
      System.out.println("direct.queue2 receive msg: " + msg);
      }
      }
    2. publisher

      @Test
      public void testSendDirectExchange() {
      String exchangeName = "direct.exchange";
      String message = "hello direct exchange";
      rabbitTemplate.convertAndSend(exchangeName, "red", message); //red为指定的RoutingKey
      }
  5. TopicExchange,和directKey非常相似,区别在于routingKey必须是多个单词的列表,并以点分割,例如beijing.food,beijing.people,shanxi.food,shanxi.people,在指定bindingKey时,可以使用通配符,例如beijing.#,#.food

消息转换器

  1. 发送Map,List数据时,底层会将数据序列化,导致数据安全性和速度受到影响,所以,我们需要消息转换器,将其转换为json数据进行传输

  2. 在父工程中引入依赖(publisher服务和consumer服务都需要)

    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    </dependency>
  3. 在Publisher服务和Consumer服务的启动类中添加以下代码

    @Bean
    public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
    }
  4. 之后只需要发布者发送什么类型,接受者在接收时声明对应类型即可。

ElasticSearch

  1. Elasticsearch 是一个分布式的、基于** RESTful API** 的搜索和分析引擎

  2. Mysql中名词对应关系

    索引 (Index) —– database

    类型(Type) (新版本已经废弃,之后的文档都是直接存在索引下的) —— Table

    文档(Document) —— Table中一条条的数据

    属性 —– 某一列的属性

  3. 倒排索引机制: 用来快速查找包含某个词的所有文档.

    1. 按词汇进行索引,快速查找包含该词的所有文档。
    2. 优点是对搜索效率极高,尤其是当我们查询某个词或一组词时,能够快速定位所有相关文档。
    3. 缺点是需要额外的空间存储索引表。
  4. 倒排索引使用: 有4条文档, 1–小米手机, 2–华为手机, 3– 小米手环, 4–华为小米充电器. 倒排索引在存储时,会将其拆分成词条进行如下存储, 当搜索华为手机时,会命中两个词条, 华为和手机, 对应1,2,4三个文档ID, 文档命中数多的排在前面. 命中数少的排在后面.

    表头: 词条(Term)    文档ID
    小米 1,3,4
    手机 1,2
    华为 2,4
    手环 3
    充电器 4
  5. _cat

    1. GET /_cat/nodes :查看所有节点
    2. GET /_cat/health: 查看es健康状况
    3. GET /_cat/master: 查看主节点的信息
    4. GET /_cat/indices: 查看所有索引, 相当于show databases
  6. 保存一个文档(相当于添加一条数据):

    1. PUT /customer/external/1 :第一次发送是新建操作, 再次使用同一ID发送就变为更新操作, PUT方法必须指定ID, 一般用来更新操作.

    2. POST /customer/external/1: 不指定ID会自动生成唯一ID, 指定ID就会修改ID对应的这条数据. 一般用来新增操作

      // 在customer索引下的external类型下保存一号数据如下
      {
      "name":"jone"
      }
  7. 查询数据

    1. GET /customer/external/1 : 查询 customer索引下的external类型下的1号数据
  8. 更新数据

    1. POST /customer/external/1 /_update : 带_update,的请求会对比原来的数据, 与原来一样, 数据不会更新,version, seq_no也不会变

      {
      // post使用_update, 要更新的数据必须在doc下
      "doc": {
      name: "sam"
      }
      }
    2. POST/PUT /customer/external/1: 不带update, 每次发送请求都会执行更新操作, ,version, seq_no都会更新

  9. 删除文档&索引

    1. DELETE /customer/external/1:
    2. DELETE /customer
  10. bulk批量API: 批量导入数据

  11. POST /customer/external/_bulk

    // 两行为一组, 第一行为需要执行的操作.第二行为请求的数据,删除操作时只有一行,指定ID即可
    {"index":{"_id":"1"}}
    {"name":"John Doe"}

    {"index":{"_id":"2"}}
    {"name":"Jane Doe"}
  12. 语法格式

    POST /索引/类型/_bulk

    {action:{metadata}}
    {requestBody}

    {action:{metadata}}
    {requestBody}
Author: Yang Wa
Link: https://blog.wxywxy.cn/2023/11/30/SpringCloud/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.