Spring Cloud 学习笔记(第一季)

目录

NO.1 Eureka与Nacos注册中心、Ribbon负载均衡

1. 注册RestTemplate

2. Eureka注册中心

1. 创建一个独立的微服务:EurekaServer 服务

1. 创建 EurekaServer ,导入依赖

2. 启动类中加入@EnableEurekaServer注解

3. 编写配置文件

4. 启动微服务,浏览器输入 http://127.0.0.1:10086

2. 服务注册:将user-service注册到eureka-server中去

1. 引入依赖

2. 配置文件

3. 启动多个 userservice 实例

3. 服务发现:向eureka-server拉取user-service的信息

1. 引入依赖

2. 配置文件

3. 服务拉取和负载均衡

3. Ribbon负载均衡

1. 自定义负载均衡策略

2. 饥饿加载

4. Nacos注册中心

1. Nacos注册中心的创建与使用

1. 引入依赖

2. 配置nacos地址

3. 登录nacos管理页面

2. 集群

1. 给 user-service 添加集群

2. 修改负载均衡规则

3. 权重

4. 环境隔离

5. Nacos与Eureka的区别

 NO.2 Nacos 配置管理、Feign 远程调用、Gateway 服务网关

1. Nacos 配置管理

1. 统一配置管理

1. 引入 Nacos 的配置管理客户端依赖:

2. 新建 bootstrap.yaml

3. 读取配置信息

2. 配置管理的热更新

1. 方式一:@Value注入的变量所在类上添加注解@RefreshScope

2. 方式二:@ConfigurationProperties注解代替@Value注解

3. 多环境配置共享

4. 配置文件优先级

扩展:Nacos集群搭建

1. 新建Nacos数据库

2. 修改Nacos配置文件

3. 启动

4. nginx中修改conf/nginx.conf文件(反向代理)

5. 修改代码中application.yml文件配置:

6. 访问:输入 http://localhost/nacos 即可。

2. Feign远程调用

1. Feign 替代 RestTemplate

1. 引入依赖:

2. 在 order-service 的启动类添加注解开启 Feign 的功能

3. 新建 Client 包,在包下新建 UserClient 接口用来处理需要查询 User 库的请求

4. Feign 替代 RestTemplate 

2. Feign 的日志

1. 全局配置 - 配置文件:

2. 局部配置 - 配置文件:

3. 全局配置 - java 代码方式

1. 声明bena

2. 放到@EnableFeignClients注解中

4. 局部配置 - java 代码方式

 1. 声明bena

2. 放到@FeignClient注解中

3. Feign 优化

1. 连接池配置

2. 抽取 FeignClient

3. Gateway 服务网关

1. Gateway 快速用法

1. 新建模块 gateway 导入依赖

 2. 新建配置文件 application.yml

3. 启动服务,通过 http://localhost:10010/user/1 查询数据表。

2.  断言工厂

3.  过滤器工厂

4. 全局过滤器

5. 网关跨域问题

NO.3 Docker

1.  Docker概念

1. Docker解决依赖兼容问题

2. Docker解决操作系统环境差异

3. Docker与虚拟机的区别

4. Docker镜像和容器

5. DockerHub

6. Docker结构:

7. Docker安装

 1. 卸载旧版本的Docker

2. 安装docker

3. 更新本地镜像源

4. 输入命令

5. 关闭防火墙

6. 启动docker

7. 查看docker版本

8. 配置国内镜像加速

2. Docker的操作

3. 数据卷(容器数据管理)

1. 数据集操作命令

2. 实现挂载

1. 数据卷实现挂载

2. 宿主机直接挂载模式

4. Dockerfile自定义镜像

1. 基于Ubuntu构建Java项目

1. 准备Java项目、jdk8

2. 将Dockerfile编写以下内容:

3. 将这三个文件上传至虚拟机,在它的目录里输入:

4. 启动容器,运行项目在浏览器ip地址

2. 基于java8构建Java项目

1. 只需要利用java:8-alpine作为基础镜像(人家定好的)

2. 编写 Dockerfile 文件:

3.  使用docker build命令构建镜像

4. 使用docker run创建容器并运行

5. Docker-Compose

1.  Docker-Compose的下载

1. 下载

2. 修改权限

3.  Base自动补全命令:

2. Docker-Compose的使用

6.  Docker镜像仓库

1. 搭建私有镜像仓库

1. 简化版镜像仓库

2. 带有图形化界面版本

3. 配置Docker信任地址

2. 推送、拉取镜像

 NO.4 MQ

1. MQ概述

1. 同步和异步通讯

2. 常见的MQ

2. 使用RabbitMQ

1. 在虚拟机中安装 RabbitMQ

2. RabbitMQ的基本使用

1.  启动MQ容器

2. 编写publisher

3. 编写consumer

 4. 启动consumer会等待消息,启动publisher发送消息!

3.  SpringAMQP

1. BasicQueue 简单队列模型

1. 配置

2. 编写 publisher 服务

3. 编写 consumer 服务

4. 注意 

2. WorkQueue 任务模型

1. 配置

2. 编写 publisher 服务

3. 编写 consumer 服务

4. 结论:

3. 发布/订阅,以下都属于此类别

4. Fanout 广播模式

1. 声明交换机,两个队列,并进行绑定

 2. 编写publisher,向交换机发送消息

3. 编写consumer,接受各个队列消息

4. 总结

5. Direct

2. 消息发送

3. 总结

6. Topic

1. 消息发送

2. 消息接收

3. 总结

7. 消息转换

配置JSON转换器

NO. 5 分布式搜索引擎elasticsearch

1. elasticsearch基础

1. elasticsearch的概述

1. elasticsearch的作用

2. ELK技术栈

3. elasticsearch和lucene

2. 倒排索引

1. 倒排索引两个非常重要的概念

2. 创建倒排索引概念

3. 倒排索引执行流程

4. 总结

3. Mysql与ES

4. 安装es、kibana

1. 创建网络

2. 加载镜像

3. 同理还有kibana的tar包也需要这样做。

4. 运行

5. kibana的部署

5. 分词器

1. 安装分词器(离线方式)

2. 测试使用

3. 扩展词词典

3. 停用词词典

2. elasticsearch的索引库操作

1. mapping映射属性

2. 索引库的增删改查

1. 添加索引库

2. 查询索引库

3. 修改索引库(只能添加)

4. 删除索引库

3. elasticsearch的文档操作

1. 新增

2. 查询

3. 修改

1. 全量修改

2. 增量修改

4. 删除

4. RestAPI - 操作索引库

1. 初始化操作

1. 引入依赖

 2. 初始化RestHighLevelClient

3. 为了测试,可以放到一个测试类HotelIndexTest中,利用@BeforeEach初始化

2. 创建索引库

3. 删除索引库

4. 判断索引库是否存在

5. RestAPI - 操作文档

1. 初始化操作

2. 新增文档

3. 查询文档

4. 删除文档

5. 修改文档

6. RestAPI - 批量导入数据


NO.1 Eureka与Nacos注册中心、Ribbon负载均衡

微服务是什么?

微服务可以理解为微服务架构。之前学的都是单体架构,将功能放到一个模块下,将业务的所有功能集中在一个项目中开发,打成一个包部署,这样一来对于大型企业开发,业务模块数量居多,导致于后期维护困难,此时就要进化为微服务架构,它可以将这些模块逐一拆分,方便后期维护。

Spring Cloud是什么?

微服务架构的实现每个企业都要有自己的方案,在Java领域,很出名的一种方案就是 Spring Cloud,他是一种微服务框架,集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配。因此呢,我们在使用SpringCloud时,底层又是依赖于SpringBoot的,所以版本一定要兼容

1. 注册RestTemplate

微服务架构中,各模块之间需要完成相互调用,当我们在 order 模块时需要通过商品 id 在 user 的数据库中查询数据时,该怎么办呢?

  • 注册一个RestTemplate的实例到Spring容器

  • 修改order-service服务中的OrderService类中的queryOrderById方法,根据Order对象中的userId查询User

  • 将查询的User填充到Order对象,一起返回

代码实现:

    // 将RestTemplate注册到spring容器
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2.查询用户
    String url = "http://localhost:8081/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);
    // 3.封装 user 信息
    order.setUser(user);
    // 4.返回
    return order;
}

2. Eureka注册中心

user 作为服务提供者,如果他有多个服务提供者呢?比如8081,8082等都是userservice,此时我们服务消费者该如何去访问呢,又该访问那个呢?可以利用Eureka注册中心解决这个问题,下面是使用的步骤/原理:

  • 当userservice被启动时,会去 eureka-server 注册中心注册服务信息,并且每 30 s都会向 eureka-server 注册中心发送心跳(存活状态)。

  • order-service服务向 eureka-server 注册中心拉取 userservice 服务的信息(端口号),拿到了地址信息,就会在底层会采用负载均衡算法进行远程调用

代码实现:

1. 创建一个独立的微服务:EurekaServer 服务

1. 创建 EurekaServer ,导入依赖

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

2. 启动类中加入@EnableEurekaServer注解

@EnableEurekaServer

3. 编写配置文件

server:
  port: 10086   # Eureka 服务端口
spring:
  application:
    name: eureka-server   # Eureka 服务名字
eureka:
  client:
    service-url: 
      defaultZone: http://127.0.0.1:10086/eureka    # Eureka 的访问路径

4. 启动微服务,浏览器输入 http://127.0.0.1:10086

2. 服务注册:将user-service注册到eureka-server中去

1. 引入依赖

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

2. 配置文件

spring:
  application:
    name: userservice
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

3. 启动多个 userservice 实例

在 Services 中右击一个服务,点击 Copy Config... 将 VM options 写上:-Dserver.port=8082;表示我们启动了另一个端口的 userservice 。

注意:如果没有 Services ,需要在 View 中 打开 Services,并且在配置模块里添加 spring boot。

3. 服务发现:向eureka-server拉取user-service的信息

1. 引入依赖

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

2. 配置文件

spring:
  application:
    name: orderservice
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

3. 服务拉取和负载均衡

给RestTemplate这个Bean添加一个@LoadBalanced注解;修改访问的url路径,用服务名代替ip、端口。通过结果我们会发现采用了负载均衡,并且是轮询的方式!为什么呢?

    // 将RestTemplate注册到spring容器
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2.查询用户
    String url = "http://userservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);
    // 3.封装 user 信息
    order.setUser(user);
    // 4.返回
    return order;
}

3. Ribbon负载均衡

添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?

原理是底层源码之间的互相转换,SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。 我们只要这要知道负载均衡的规则都定义在IRule接口中。

1. 自定义负载均衡策略

代码实现方式:

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

配置文件实现方式:

userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:

    # 负载均衡规则 
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

2. 饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:。

ribbon:

        eager-load:  

                enabled: true  

                clients: userservice

4. Nacos注册中心

 SpringCloudAlibaba也推出了一个名为Nacos的注册中心。

Nacos 比 Eureka 更好用,差异:依赖不同、服务地址不同

1. Nacos注册中心的创建与使用

1. 引入依赖

在总的父工程引入依赖。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.6.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

在user-service和order-service中的pom文件中引入nacos-discovery依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2. 配置nacos地址

spring:
  cloud:
    nacos:
      server-addr: localhost:8848

3. 登录nacos管理页面

通过 localhost:8848/nacos;默认本机账号密码=都为 nacos;

2. 集群

一个服务可以有多个实例,而我们又可以根据地域划分一下集群,即一个服务下有多个集群,一个集群下有多个实例,集群一般都是 HZ、SH、BJ等城市的名。接下来实现一下。

1. 给 user-service 添加集群

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

2. 修改负载均衡规则

必须修改负载均衡规则,让他优先于自己所在的集群:

userservice:
    ribbon:

        # 负载均衡规则
       NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule

3. 权重

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求;Nacos 提供了权重配置来控制访问频率,权重越大则访问频率越高

4. 环境隔离

在 Nacos 控制台可以创建 namespace ,用来隔离不同环境了,如果环境不同的话,会导致找不到userservice,控制台会报错。

修改 order-service 的 application.yml ,添加 namespace :

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID

5. Nacos与Eureka的区别

Nacos的服务实例分为两种类型:

  • 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。

  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

Nacos与Eureka的区别

  • Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式

  • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除

  • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时

  • Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

 NO.2 Nacos 配置管理、Feign 远程调用、Gateway 服务网关

1. Nacos 配置管理

Nacos 不仅可以服务管理,管理每一个微服务,又可以统一管理管理配置文件。

1. 统一配置管理

我们可以在 Nacos 客户端进行加入配置管理

  • 唯一名字:Data ID(一般为:服务名称-环境.yaml 或 服务名称.yaml)
  • 指定分组:Group(一般为:默认即可)
  • 配置格式:目前支持 YAML 和 Properties

 对于读取配置文件,会优先读取 Nacos 配置的文件,但是一些 Nacos 的配置文件,比如端口号、服务名都写在了 Application.yaml 文件中,因此我们需要在读取Nacos 配置的文件之前就需要知晓端口号、服务名等引导信息,需要创建优先级高的 bootstrap.yml 文件。

 实现步骤:

1. 引入 Nacos 的配置管理客户端依赖:

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

2. 新建 bootstrap.yaml

可以看到填写的就是配置文件中的那三个参数:服务名称-环境.yaml

spring:
  application:
    name: userservice # 服务名称
  profiles:
    active: dev #开发环境,这里是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 文件后缀名

3. 读取配置信息

使用 @Value 读取文件

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

2. 配置管理的热更新

1. 方式一:@Value注入的变量所在类上添加注解@RefreshScope
@RefreshScope
public class UserController {
    @Value("${pattern.dateformat}")
    private String dateformat;
}
2. 方式二:@ConfigurationProperties注解代替@Value注解
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}

3. 多环境配置共享

所谓多环境配置,就是无论什么环境都会读到这个配置文件,那么只需要在编写 Nacos 配置文件时编写:服务名称.yaml

4. 配置文件优先级

bootstrap.yaml > 服务名-环境.yaml > 服务名称.yaml > 本地配置

扩展:Nacos集群搭建

使用一个 Nacos 就是只有一个节点,当我们访问量大时,就有问题,需要多台 Nacos 来处理,此时就用到了Nacos集群搭建

步骤:

1. 新建Nacos数据库

Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。官方推荐的最佳实践是使用带有主从的高可用数据库集群。

2. 修改Nacos配置文件

  1. 进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
    添加内容
    127.0.0.1:8845
    127.0.0.1.8846
    127.0.0.1.8847
  2. 修改application.properties文件,添加数据库配置

    spring.datasource.platform=mysql

    db.num=1

    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=1234

3. 启动

  1. 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3,表示三个节点
  2. 分别修改三个文件夹中的application.properties,端口号为8845、8846、8847
  3. 输入 startup.cmd 启动三个 nacos 节点

4. 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;
    }
}

5. 修改代码中application.yml文件配置:

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

6. 访问:输入 http://localhost/nacos 即可。

2. Feign远程调用

1. Feign 替代 RestTemplate

问题:

之前我们的调用,采用 RestTemplate,因为 url 难以维护,我们需要一种方式,使得在代码中(OrderService)采用 Feign 客户端(Client)直接发送请求,使 UserService 查找。

步骤:

1. 引入依赖:

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

2. 在 order-service 的启动类添加注解开启 Feign 的功能
@EnableFeignClients()
public class OrderApplication {
}
3. 新建 Client 包,在包下新建 UserClient 接口用来处理需要查询 User 库的请求
@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

注意:要与 此 findById() 要和 User 方法一致。 

4. Feign 替代 RestTemplate 
    @Autowired
    private UserClient userClient;
    
    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2. 根据订单 id 查询用户
        User user = userClient.findById(order.getUserId());
        // 3. 向 order 中添加 user
        order.setUser(user);
        // 4.返回
        return order;
    }

2. Feign 的日志

1. 全局配置 - 配置文件:

feign:
        client:
                config:
                        default:  #这里写default,则是针对所有微服务的配置
                                loggerLevel: BASIC  #日志级别:仅打印路径

2. 局部配置 - 配置文件:

feign:
        client:
                config:
                        userservice:  #这里写服务名称,则是针对某个微服务的配置
                                loggerLevel: BASIC  #日志级别:仅打印路径

3. 全局配置 - java 代码方式
1. 声明bena
public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}
2. 放到@EnableFeignClients注解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
4. 局部配置 - java 代码方式
 1. 声明bena
public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}
2. 放到@FeignClient注解中
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)

3. Feign 优化

Feign 底层的客户端实现:
• URLConnection :默认实现,不支持连接池
• Apache HttpClient :支持连接池
• OKHttp :支持连接池

优化手段:

① 使用连接池代替默认的 URLConnection
② 日志级别,最好用 basic 或 none

1. 连接池配置

导入依赖

<!--httpClient的依赖(Feign优化,使用连接池代替默认的URLConnection)-->

<dependency>

        <groupId>io.github.openfeign</groupId>

        <artifactId>feign-httpclient</artifactId>

</dependency>

配置文件

feign: # feign相关的配置

         httpclient: # 使用的连接池的这个配置

                 enabled: true # 开启feign对HttpClient的支持

                 max-connections: 200 #最大连接数

                 max-connections-per-route: 50 #每个路径的最大连接数

2. 抽取 FeignClient

这是FeignClient 对 UserService 发起的请求,我们将 feign 封装到一个模块下,这样,那个需要对 UserService 发起请求,直接加入 feign 的依赖就可了。

步骤:

1. 将 feign 的配置类、user类、客户端封装到 feign-api 中。

2. 删除 order-service 中feign 的配置类、user类、客户端。

3. 修改 order-service 中user的导包,需要导入 feign-api 中的。

4. 此时 SpringBootApplication 无法扫描到 FeignClient 以下两种方式:

1. @EnableFeignClients(basePackages ="cn.itcast.feign.clients")

2. @EnableFeignClients(clients = {UserClient.class}) 

3. Gateway 服务网关

网关的作用:
• 对用户请求做身份认证、权限校验
• 将用户请求路由到微服务,并实现负载均衡
• 对用户请求做限流

1. Gateway 快速用法

完成通过访问网关ip地址可以通过userId查询到user,并利用路由断言按照指定规则进行路径匹配(只要以/user/开头就符合要求),并且指定服务的负载均衡

1. 新建模块 gateway 导入依赖
<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>
 2. 新建配置文件 application.yml
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
3. 启动服务,通过 http://localhost:10010/user/1 查询数据表。

2.  断言工厂

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。像这样的断言工厂在SpringCloudGateway还有十几个:

名称说明示例
After是某个时间点后的请求- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before是某个时间点之前的请求- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between是某两个时间点之前的请求- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie请求必须包含某些cookie- Cookie=chocolate, ch.p
Header请求必须包含某些header- Header=X-Request-Id, \d+
Host请求必须是访问某个host(域名)- Host=.somehost.org,.anotherhost.org
Method请求方式必须是指定方式- Method=GET,POST
Path请求路径必须符合指定规则- Path=/red/{segment},/blue/**
Query请求参数必须包含指定参数- Query=name, Jack或者- Query=name
RemoteAddr请求者的ip必须是指定范围- RemoteAddr=192.168.1.1/24
Weight权重处理

3.  过滤器工厂

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理

Spring提供了31种不同的路由过滤器工厂。例如:

名称说明
AddRequestHeader给当前请求添加一个请求头
RemoveRequestHeader移除请求中的一个请求头
AddResponseHeader给响应结果中添加一个响应头
RemoveResponseHeader从响应结果中移除有一个响应头
RequestRateLimiter限制请求的流量

案例:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!

修改gateway服务的application.yml文件,添加路由过滤

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/** 
        filters: # 过滤器
        - AddRequestHeader=Truth, my name is xz! # 添加请求头

此时,可以通过springboot提供的:@RequestHeader获取请求头

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          //表示请求头名Truth,可以不传递此参数
           @RequestHeader(value = "Truth", required = false) String name) {
        System.out.println(name);
        return userService.queryById(id);
    }

默认过滤器:对所有的路由都生效

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/**
      default-filters: # 默认过滤项
      - AddRequestHeader=Truth, my name is xz! 

4. 全局过滤器

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。定义方式是实现 GlobalFilter 接口。

案例演示: 

定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

  • 参数中是否有authorization

  • authorization参数值是否为admin

@Component
@Order(-1) //越小优先级越高
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 1.获取请求参数
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
        // 2.获取authorization参数
        String auth = queryParams.getFirst("authorization");
        // 3.校验
        if ("admin".equals(auth)) {
            // 放行
            return chain.filter(exchange);
        }
        // 4.拦截
        // 4.1.禁止访问,设置状态码 404
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 4.2.结束处理
        return exchange.getResponse().setComplete();
    }
}

指定了这么多过滤器,他们有时候应该先执行那个呢?

  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定

  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。

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

5. 网关跨域问题

浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。

我们userservice 可以访问到 orderservice 是因为没有 ajax 请求。

当浏览器有一个 ajax 请求到网关 loalhost:10010 会发生跨域问题。

解决方案:

添加配置(CORS):

spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]': #拦截所有路径
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:8090"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

NO.3 Docker

大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:

  • 依赖关系复杂,容易出现兼容性问题

  • 开发、测试、生产环境有差异

1.  Docker概念

1. Docker解决依赖兼容问题

  • 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包

  • 将每个应用放到一个隔离容器去运行,避免互相干扰

2. Docker解决操作系统环境差异

操作的结构分为

  • 计算机硬件:CPU、内存、磁盘。
  • 内核:内核与计算机硬件交互,对外提供内核指令用于操作计算机硬件。
  • 系统应用:操作系统有各种应用,对应着会有各自的函数库,函数库封装内核指令。

存在问题

在Ubuntu版本的Linux操作系统中的Mysql安装到CentOS版本,Mysql会调用Ubuntu版本的函数库,导致无法找到。

Docker解决方案

既然 系统应用 产生的各种函数库都不一致,那么在将Mysql部署的时候,直接也将Mysql的Ubuntu版本的函数库打包,让他自己处理后封装内核指令到Linux,就可以了。

3. Docker与虚拟机的区别

  • docker是一个系统进程;虚拟机是在操作系统中的操作系统

  • docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般

4. Docker镜像和容器

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

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

5. DockerHub

开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像。

DockerHub:DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。

6. Docker结构:

Docker结构:

  • 服务端:接收命令或远程请求,操作镜像或容器

  • 客户端:发送命令或者请求到Docker服务端

7. Docker安装

 1. 卸载旧版本的Docker

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

2. 安装docker

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

3. 更新本地镜像源

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

yum makecache fast

4. 输入命令

yum install -y docker-ce

5. 关闭防火墙

Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,练习需要关上。

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

6. 启动docker

systemctl start docker  # 启动docker服务

systemctl stop docker  # 停止docker服务

systemctl restart docker  # 重启docker服务

7. 查看docker版本

 docker -v

8. 配置国内镜像加速

 阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

2. Docker的操作

镜像命令
docker pull nginx指定最新版本nginx进行拉取nginx的镜像
docker images查看拉取到的镜像
docker save --help查看save命令用法
docker save -o nginx.tar nginx:latest将nginx镜像导出磁盘,起名为nginx.tar,可以用 load 加载
docker rmi nainx:latest删除nainx:latest镜像
docker load -i nginx.tar运行命令,加载本地文件
容器命令

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

创建并运行一个起的名字为 mn 的容器,指定宿主机端口80(左侧)与容器端口80(右侧)映射,并后台运行nginx此镜像

docker exec -it mn bash

进入容器内部,执行 -it 命令,表示给mn这个容器创建一个标准输入、输出终端,允许我们与容器交互,bash表示进入容器后执行的命令

docker logs -f 

查看容器日志,添加 -f 参数可以持续查看日志

docker ps -a

查看容器状态,加 -a 包括已经停止的

docker rm -f <containerid>删除指定id的容器

基本步骤:

1)docker pull nginx 拉取想用的镜像

2)docker run --name mn -p 80:80 -d nginx 运行一个容器,后台运行nginx镜像

3)docker exec -it mn bash 进入容器,可以执行命令。

在容器中想要修改一些镜像的东西,总是很麻烦,比如,我们需要进入nginx中,修改默认的主页面index.html,我们可以进入index.html的目录后使用命令:

sed -i -e 's#Welcome to nginx#xz,nginx中遨游吧!#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html

有什么办法吗?有!数据卷(容器数据管理)

3. 数据卷(容器数据管理)

数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。比如将nginx与宿主机目录完成数据卷挂载,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了

1. 数据集操作命令

docker volume [COMMAND] 表示对数据卷操作[COMMAND]如下命令

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

案例:创建一个数据卷,并查看数据卷在宿主机的目录位置

步骤:

  1. docker volume create html 创建一个数据卷
  2. docker volume ls 查看所有数据
  3. docker volume inspect html 查看数据卷的详细信息
  4. 就可以看到数据卷关联的宿主机目录

2. 实现挂载

1. 数据卷实现挂载

我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下:

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

-v html:/root/html :把html数据卷挂载到容器内的/root/html这个目录中

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

步骤:

  1. 创建容器并挂载数据卷到容器内的HTML目录
    docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx
  2. 查看html数据卷所在位置
    docker volume inspect html
  3. 进入该目录
    cd /var/lib/docker/volumes/html/_data
  4. 修改文件
    vi index.html
2. 宿主机直接挂载模式

容器不仅仅可以挂载数据卷,也可以直接挂载到宿主机目录上。

  • 带数据卷模式:宿主机目录 --> 数据卷 ---> 容器内目录

  • 直接挂载模式:宿主机目录 ---> 容器内目录

语法

目录挂载与数据卷挂载的语法是类似的:

  • -v [宿主机目录]:[容器内目录]

  • -v [宿主机文件]:[容器内文件]

  • 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找

  • 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看

4. Dockerfile自定义镜像

镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。简单来说,镜像就是在系统函数库、运行环境基础上,添加应用程序文件、配置文件、依赖文件等组合,然后编写好启动脚本打包在一起形成的文件。

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

1. 基于Ubuntu构建Java项目

案例:基于Ubuntu镜像构建一个新镜像,运行一个java项目

1. 准备Java项目、jdk8
2. 将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

3. 将这三个文件上传至虚拟机,在它的目录里输入:

docker build -t javaweb:1.0 .    表示在当前文件夹下构建镜像 

4. 启动容器,运行项目在浏览器ip地址

2. 基于java8构建Java项目

虽然我们可以基于Ubuntu基础镜像,添加任意自己需要的安装包,构建镜像,但是却比较麻烦。所以大多数情况下,我们都可以在一些安装了部分软件的基础镜像上做改造。

例如,构建java项目的镜像,可以在已经准备了JDK的基础镜像基础上构建。

案例:基于java8构建Java项目

1. 只需要利用java:8-alpine作为基础镜像(人家定好的)
2. 编写 Dockerfile 文件:

FROM java:8-alpine
COPY ./app.jar /tmp/app.jar      # 左边的是项目打包名,右边是jdk8中名字
EXPOSE 8090
ENTRYPOINT java -jar /tmp/app.jar

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

5. Docker-Compose

我们发现,我们在运行项目时,每次都需要创建和运行容器,有没有什么可以解决?

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

1.  Docker-Compose的下载

Linux下需要通过命令下载:

1. 下载

curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

2. 修改权限

chmod +x /usr/local/bin/docker-compose 

3.  Base自动补全命令:

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

如果出错:

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

2. Docker-Compose的使用

我们在项目部署时,需要将mysql、nacos完成部署,Docker-Compose可以一步到位将他们一起放到容器中并运行

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

1.  Docker-Compose的配置文件

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"

  • nacos:作为注册中心和配置中心

    • image: nacos/nacos-server: 基于nacos/nacos-server镜像构建

    • environment:环境变量

      • MODE: standalone:单点模式启动

    • ports:端口映射,这里暴露了8848端口

  • mysql:数据库

    • image: mysql:5.7.25:镜像版本是mysql:5.7.25

    • environment:环境变量

      • MYSQL_ROOT_PASSWORD: 123:设置数据库root账户的密码为123

    • volumes:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据

  • userserviceorderservicegateway:都是基于Dockerfile临时构建的

2. 将数据库、nacos地址都命名为docker-compose中的服务名

就是项目中访问路径 localhost 要改为 微服务名,比如访问的 localhost:3306 要改为 mysql:3306;localhost:8848 要改为 nacos:8848。

3. maven打包工具,将项目中的每个微服务都打包为app.jar

要有此 maven 配置:

<build>
  <!-- 服务打包的最终名称 -->
  <finalName>app</finalName>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

4. 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中

cloud-demo 项目每个模块都放至文件夹里,对应的模块有两个文件,一个为 app.jar 是对应的 jar 包,一个是 DockerFile 文件,

5. 将mysql打包

要提供 mysql 的 date 数据和配置 cnf

6. 将cloud-demo上传至虚拟机,利用 docker-compose up -d 来部署

进入cloud-demo目录,然后运行下面的命令:

docker-compose up -d

 注意:有可能有bug,需要 docker restart 一下。

6.  Docker镜像仓库

1. 搭建私有镜像仓库

搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。

官网地址:https://hub.docker.com/_/registry

1. 简化版镜像仓库

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 目录,这是私有镜像库存放数据的目录。

访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像  

2. 带有图形化界面版本

使用DockerCompose部署带有图象界面的DockerRegistry,命令如下:

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

3. 配置Docker信任地址

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

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

2. 推送、拉取镜像

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

① 重新tag本地镜像,名称前缀为私有仓库的地址:192.168.150.101:8080/

docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 

② 推送镜像

docker push 192.168.150.101:8080/nginx:1.0 

③ 拉取镜像

docker pull 192.168.150.101:8080/nginx:1.0 

 NO.4 MQ

1. MQ概述

1. 同步和异步通讯

同步通讯:就像打电话,需要实时响应。

异步通讯:就像发邮件,不需要马上回复。

异步通讯:

我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。

在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。

订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。

为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。

2. 常见的MQ

几种常见MQ的对比:

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka

2. 使用RabbitMQ

1. 在虚拟机中安装 RabbitMQ

  1. docker pull rabbitmq:3-management  # 在线拉取镜像
  2. docker load -i mq.tar        # 虚拟机中加载镜像
  3. 执行命令运行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

2. RabbitMQ的基本使用

案例:通过publisher和consumer实现消息的发送和订阅消息。

1.  启动MQ容器
2. 编写publisher
public class PublisherTest {
    @Test
    public void testSendMess() throws IOException, TimeoutException {
        //1. 建立连接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2. 设置:主机名、端口号、vhost、用户名、密码
        connectionFactory.setHost("192.168.204.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("itcast");
        connectionFactory.setPassword("123321");
        //3. 建立连接
        Connection connection = connectionFactory.newConnection();
        //4. 创建通道Channel
        Channel channel = connection.createChannel();
        //5. 创建简单队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
        //6. 发送消息
        String message = "hello, xz";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送成功");
        //7. 关闭通道、连接
        channel.close();
        connection.close();
    }
}
3. 编写consumer
public class ConsumerTest {
    public static void main(String[] args){
        //1. 建立连接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //2. 设置:主机名、端口号、vhost、用户名、密码
        connectionFactory.setHost("192.168.204.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("itcast");
        connectionFactory.setPassword("123321");
        //3. 建立连接
        Connection connection = null;
        try {
            connection = connectionFactory.newConnection();
            //4. 创建通道Channel
            Channel channel = connection.createChannel();
            //5. 创建简单队列
            String queueName = "simple.queue";
            channel.queueDeclare(queueName, false, false, false, null);
            //6. 订阅消息
            channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                           AMQP.BasicProperties properties, byte[] body) {
                    //7. 处理消息
                    String message = new String(body);
                    System.out.println("接受到消息:【" + message + "】");
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("等待接收消息中");
    }
}
 4. 启动consumer会等待消息,启动publisher发送消息!

3.  SpringAMQP

SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。SpringAMQP提供了三个功能:

  • 自动声明队列、交换机及其绑定关系

  • 基于注解的监听器模式,异步接收消息

  • 封装了RabbitTemplate工具,用于发送消息

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

1. BasicQueue 简单队列模型

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

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

配置MQ地址,在publisher服务和consumer服务的application.yml中添加配置

spring:
  rabbitmq:
    host: 192.168.150.101 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码

2. 编写 publisher 服务
@SpringBootTest
@RunWith(SpringRunner.class)
public class PublisherTest2 {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testBasicQueue() {
        //队列什么名
        String queueName = "basic.queue";//注意:不会给创建队列
        //什么消息
        String message = "hello, xz!";
        //发送
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

 此时可以测试,在浏览器输入:虚拟机名:5672 输入账号密码,此队列会有一条消息

3. 编写 consumer 服务
@Component
public class SpringRabbitLister {
    @RabbitListener(queues = "basic.queue")
    public void listenBasicQueueMessage(String msg){
        System.out.println("消息为:【" + msg+ "】");
    }
}

 启动 consumer 服务,可以将队列消息打印出,并毁灭队列中存在的消息

4. 注意 

注意:

  1. 在启动容器时,需要配上用户名密码与端口映射。
  2. rabbitTemplate.convertAndSend() 不支持创建队列,需要先有个队列。

2. WorkQueue 任务模型

刚刚模型会使消息约堆越多,无法及时处理。

解决方案:让多个消费者绑定到一个队列,共同消费队列中的消息。

1. 配置

跟上一步一样

2. 编写 publisher 服务

模拟生产者速度快,不停向队列发消息,迅速将队列堆满。

    @Test
    public void testWorkQueue() throws InterruptedException {
        //队列什么名
        String queueName = "basic.queue";//注意:不会给创建队列
        //什么消息
        String message = "hello, xz!";
        //发送
        for (int i = 0; i < 50; i++) {
            rabbitTemplate.convertAndSend(queueName, message);
            Thread.sleep(2);
        }
    }
3. 编写 consumer 服务

模拟有多个消费者,同时向队列订阅消息,各自打印各自的消息。

    @RabbitListener(queues = "basic.queue")
    public void listenWorkQueueMessage1(String msg) throws Exception {
        System.out.println("Work1消息为:【" + msg+ "】" + LocalTime.now());
        Thread.sleep(20);
    }
    @RabbitListener(queues = "basic.queue")
    public void listenWorkQueueMessage2(String msg) throws Exception {
        System.out.println("Work2消息为:【" + msg+ "】" + LocalTime.now());
        Thread.sleep(200);
    }
4. 结论:

可以看到Work1很快完成了自己的25条消息。Work2却在缓慢的处理自己的25条消息。

也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。

解决方案:

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

3. 发布/订阅,以下都属于此类别

比起上两个,在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给(交换机)

  • Exchange:交换机。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:

    • Fanout:广播,将消息交给所有绑定到交换机的队列

    • Direct:定向,把消息交给符合指定routing key 的队列

    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

  • Consumer:消费者,与以前一样,订阅队列,没有变化

  • Queue:消息队列也与以前一样,接收消息、缓存消息。

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

4. Fanout 广播模式

在广播模式下,消息发送流程是这样的:

  • 1) 可以有多个队列

  • 2) 每个队列都要绑定到Exchange(交换机)

  • 3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定

  • 4) 交换机把消息发送给绑定过的所有队列

  • 5) 订阅队列的消费者都能拿到消息

1. 声明交换机,两个队列,并进行绑定
@Configuration //不要忘记
public class FanoutConfig {

    @Bean //交换机
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }

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

    @Bean //绑定已经返回的fanoutQueue1
    public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

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

    @Bean //绑定已经返回的fanoutQueue2
    public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

}
 2. 编写publisher,向交换机发送消息
    @Test
    public void testFanoutQueue(){
        // 交换机名称
        String exchangeName = "itcast.fanout";
        // 消息
        String message = "hello, everyone!";
        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }
3. 编写consumer,接受各个队列消息
    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueueMessage1(String msg) throws Exception {
        System.out.println("Fanout1消息为:【" + msg+ "】");
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueueMessage2(String msg) throws Exception {
        System.out.println("Fanout2消息为:【" + msg+ "】");
    }
4. 总结

交换机的作用是什么?

  • 接收publisher发送的消息

  • 将消息按照规则路由到与之绑定的队列

  • 不能缓存消息,路由失败,消息丢失

  • FanoutExchange的会将消息路由到每个绑定的队列

声明队列、交换机、绑定关系的Bean是什么?

  • Queue

  • FanoutExchange

  • Binding

5. Direct

 在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)

  • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey

  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息

1. 基于注解生明队列和交换机

基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。

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

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue2"),
    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
    System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
2. 消息发送
@Test
public void testSendDirectExchange() {
    // 交换机名称
    String exchangeName = "itcast.direct";
    // 消息
    String message = "山东菏泽曹县!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
3. 总结

描述下Direct交换机与Fanout交换机的差异?

  • Fanout交换机将消息路由给每一个与之绑定的队列

  • Direct交换机根据RoutingKey判断路由给哪个队列

  • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似

基于@RabbitListener注解声明队列和交换机有哪些常见注解?

  • @Queue

  • @Exchange

6. Topic

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#:匹配一个或多个词

*:匹配恰好1个词

举例:

item.#:能够匹配item.spu.insert 或者 item.spu

item.*:只能匹配item.spu

1. 消息发送
@Test
public void testSendTopicExchange() {
    // 交换机名称
    String exchangeName = "itcast.topic";
    // 消息
    String message = "枉我大曹县!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
2. 消息接收

注意交换机类型

@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 + "】");
}
3. 总结

描述下Direct交换机与Topic交换机的差异?

  • Topic交换机接收的消息RoutingKey必须是多个单词,以 **.** 分割

  • Topic交换机与队列绑定时的bindingKey可以指定通配符

  • #:代表0个或多个词

  • *:代表1个词

7. 消息转换

传输过程中,不一定是文字,如果是Java对象,spring会消息序列化为字节发送给MQ,接受对象时,再回把字节反序列化成Java对象,所以有下问题:数据体积过大、可读性差等。

配置JSON转换器

显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。

在publisher和consumer两个服务中都引入依赖:

<dependency>    

        <groupId>com.fasterxml.jackson.dataformat</groupId>    

        <artifactId>jackson-dataformat-xml</artifactId>    

        <version>2.9.10</version>

</dependency>

配置消息转换器。

在启动类中添加一个Bean即可:

@Bean

public MessageConverter jsonMessageConverter(){

   return new Jackson2JsonMessageConverter();

}

NO. 5 分布式搜索引擎elasticsearch

1. elasticsearch基础

1. elasticsearch的概述

1. elasticsearch的作用

elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容。

2. ELK技术栈

elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域,而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

3. elasticsearch和lucene

elasticsearch底层是基于lucene来实现的。

Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:Apache Lucene - Welcome to Apache Lucene

2. 倒排索引

倒排索引的概念是基于MySQL这样的正向索引而言的。

正向索引:

如果是根据id查询,那么直接走索引,查询速度非常快。但是逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

1. 倒排索引两个非常重要的概念
  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息

  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

2. 创建倒排索引概念

创建倒排索引是对正向索引的一种特殊处理,流程如下:

  • 将每一个文档的数据利用算法分词,得到一个个词条

  • 创建表,每行数据包括词条、词条所在文档id、位置等信息

  • 因为词条唯一性,可以给词条创建索引,例如hash表结构索引

3. 倒排索引执行流程

1)用户输入条件"华为手机"进行搜索。

2)对用户输入内容分词,得到词条:华为手机

3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。

4)拿着文档id到正向索引中查找具体文档。

4. 总结
  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程

  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程

3. Mysql与ES

我们统一的把mysql与elasticsearch的概念做一下对比:

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD
  • Mysql:擅长事务类型操作,可以确保数据的安全和一致性

  • Elasticsearch:擅长海量数据的搜索、分析、计算

4. 安装es、kibana

部署单点es

1. 创建网络

因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:

docker network create es-net

2. 加载镜像

下载es镜像的tar包传到虚拟机中,然后运行命令加载即可:

docker load -i es.tar

3. 同理还有kibana的tar包也需要这样做。
4. 运行

运行docker命令,部署单点es:

docker run -d \
    --name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1

解释:

  • -e "cluster.name=es-docker-cluster":设置集群名称

  • -e "http.host=0.0.0.0":监听的地址,可以外网访问

  • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小

  • -e "discovery.type=single-node":非集群模式

  • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录

  • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录

  • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录

  • --privileged:授予逻辑卷访问权

  • --network es-net :加入一个名为es-net的网络中

  • -p 9200:9200:端口映射配置

在浏览器中输入:http://192.168.204.100:9200/

5. kibana的部署

kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。

运行docker命令,部署kibana

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1

解释:

  • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中

  • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch

  • -p 5601:5601:端口映射配置

kibana启动一般比较慢,需要多等待一会,可以通过输入http://192.168.204.100:5601/

5. 分词器

我们知道elasticsearch有时对一条数据可以设置的 index 属性,他就是表示这个字段会被查询要进行分词了,他会将词语分逐一分解,一个词语对应一个文档,用来快速查找。但针对于中文,他们的分词器并不能合理的拆分我们的中文词语,因此需要用到 IK 插件

1. 安装分词器(离线方式)
  1. 查看数据卷目录
    安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:
    docker volume inspect es-plugins

  2. 解压缩分词器安装包并上传
    将 ik 下载后,解压并重名名为:ik;并将此文件夹放置第一步的数据卷目录里

  3. 重启容器
    docker restart es

2. 测试使用

IK分词器包含两种模式:ik_smart:最少切分、ik_max_word:最细切分

通过启动的 kibana 客户端,在DevTools页面下可以输入DL语句进行测试

GET /_analyze

{

        "analyzer": "ik_max_word",

        "text": "山东菏泽曹县他来了"

}

可以发现,他将text拆分了很多

3. 扩展词词典

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。

步骤:

  1. 打开IK分词器config目录:

  2. 在IKAnalyzer.cfg.xml配置文件内容添加:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
            <entry key="ext_dict">ext.dic</entry>
    </properties>
  3. 新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
    奥利给
    哈拉少

  4. 重启elasticsearch
    docker restart es

3. 停用词词典

在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,那么我们在搜索时也应该忽略一些词汇。IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。

步骤:

  1. IKAnalyzer.cfg.xml配置文件内容添加:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典-->
            <entry key="ext_dict">ext.dic</entry>
             <!--用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典-->
            <entry key="ext_stopwords">stopword.dic</entry>
    </properties>
    
  2. 在 stopword.dic 添加停用词
    沙比
  3. 重启服务
    docker restart es

2. elasticsearch的索引库操作

索引库就类似数据库表,mapping映射就类似表的结构。我们要向es中存储数据,必须先创建“库”和“表”。这就像是在设计表结构

1. mapping映射属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type:字段数据类型,常见的简单类型有:

    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)

    • 数值:long、integer、short、byte、double、float

    • 布尔:boolean

    • 日期:date

    • 对象:object

  • index:是否创建索引,默认为true

  • analyzer:使用哪种分词器

  • properties:该字段的子字段

2. 索引库的增删改查

1. 添加索引库

#! 放到test1,在mappings里制定约束,
#! properties为它的子字段增加数据
PUT /test1

  "mappings": {     
    "properties": {
      "name":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "age":{
        "type": "integer"
      }
    }
  }
}

2. 查询索引库

GET /test1

3. 修改索引库(只能添加)

PUT /test1/_mapping
{
  "properties":{
    "many":{
      "type": "keyword"
    }
  }
}

4. 删除索引库

DELETE /test1

3. elasticsearch的文档操作

文档是真实的数据,代表的是每一条信息

1. 新增

POST /test1/_doc/1
{
    "name": "天霸动霸tua",
    "age": 21,
    "many": {
        "hoppy": "喜欢篮球",
        "desc": "一个公正的人"
    }
}

2. 查询

GET /test1/_doc/1

3. 修改

1. 全量修改

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的id删除文档

  • 新增一个相同id的文档

PUT /test1/_doc/1
{
    "name": "天霸tua",
    "age": 21,
    "many": {
        "hoppy": "喜欢乒乓球",
        "desc": "不公正"
    }
}}

2. 增量修改

增量修改是只修改指定id匹配的文档中的部分字段。

POST /test1/_update/1
{
  "doc": {
    "name": "动霸tua"
  }
}

4. 删除

DELETE /test1/_doc/1

4. RestAPI - 操作索引库

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。

我们学习的是Java HighLevel Rest Client客户端API         

1. 初始化操作

1. 引入依赖
1)引入es的RestHighLevelClient依赖:
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)会出错,因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

 2. 初始化RestHighLevelClient
RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(HttpHost.create("http://192.168.150.101:9200") ));
3. 为了测试,可以放到一个测试类HotelIndexTest中,利用@BeforeEach初始化
public class HotelIndexTest {
    private RestHighLevelClient client;
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.204.100:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

注意:使用@BeforeEach需要引入:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
</dependency> 

2. 创建索引库

注意 CreateIndexRequest 的导包:

import org.elasticsearch.client.indices.CreateIndexRequest;

MAPPING_TEMPLATE是一个自己定义的常量,DL语句,一般都放到constants包下表示常量。

@Test
void createHotelIndex() throws IOException {
    // 1.创建Request对象,索引库名字hotel
    CreateIndexRequest request = new CreateIndexRequest("hotel");
    // 2.准备请求的参数:一个是DSL语句,一个是JSON类型
    request.source(MAPPING_TEMPLATE, XContentType.JSON);
    // 3.发送请求,indices返回对象中包含索引库操作的所有方法
    client.indices().create(request, RequestOptions.DEFAULT);
}

3. 删除索引库

@Test
void testDeleteHotelIndex() throws IOException {
    // 1.创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    // 2.发送请求
    client.indices().delete(request, RequestOptions.DEFAULT);
}

4. 判断索引库是否存在

断索引库是否存在,本质就是查询,对应的DSL是: GET /hotel

@Test
void testExistsHotelIndex() throws IOException {
    // 1.创建Request对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    // 2.发送请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    // 3.输出
    System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

5. RestAPI - 操作文档

为了与索引库操作分离,我们再次参加一个测试类,做两件事情:

  • 初始化RestHighLevelClient

  • 我们的酒店数据在数据库,需要利用IHotelService去查询,所以注入这个接口

1. 初始化操作

  1. 要编写pojo实体类,与索引库所对应,如果遇到两两数据合并的,要写一个Doc,比如索引库一个字段 many 有两个值 hoppy 和 desc,doc中要写构造方法合并后与之对应
  2. 依赖什么的已在操作索引库演示完毕

2. 新增文档

新增文档的DSL语句如下:

POST /{索引库名}/_doc/1
{
    "name": "Jack",
    "age": 21
}

我们导入酒店数据,基本流程一致,但是需要考虑几点变化:

  • 酒店数据来自于数据库,我们需要先查询出来,得到hotel对象

  • hotel对象需要转为HotelDoc对象

  • HotelDoc需要序列化为json格式

    @Test
    void testAddDocument() throws IOException {
        // 1.根据id查询酒店数据
        Hotel hotel = hotelService.getById(61083L);
        // 2.转换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 3.将HotelDoc转json
        String json = JSON.toJSONString(hotelDoc);

        // 1.准备Request对象
        IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
        // 2.准备Json文档
        request.source(json, XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    }

 因此,代码整体步骤如下:

  • 1)根据id查询酒店数据Hotel

  • 2)将Hotel封装为HotelDoc

  • 3)将HotelDoc序列化为JSON

  • 4)创建IndexRequest,指定索引库名和id

  • 5)准备请求参数,也就是JSON文档

  • 6)发送请求

3. 查询文档

查询的DSL语句如下: GET /hotel/_doc/{id}

  • 准备Request对象

  • 发送请求

@Test
void testGetDocumentById() throws IOException {
    // 1.准备Request
    GetRequest request = new GetRequest("hotel", "61082");
    // 2.发送请求,得到响应
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    // 3.解析响应结果
    String json = response.getSourceAsString();

    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    System.out.println(hotelDoc);
}

与之前类似,也是三步走:

  • 1)准备Request对象。这次是查询,所以是GetRequest

  • 2)发送请求,得到结果。因为是查询,这里调用client.get()方法

  • 3)解析结果,就是对JSON做反序列化  

4. 删除文档

删除的DSL为是这样的: DELETE /hotel/_doc/{id}

与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:

  • 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id

  • 2)准备参数,无参

  • 3)发送请求。因为是删除,所以是client.delete()方法

@Test
void testDeleteDocument() throws IOException {
    // 1.准备Request
    DeleteRequest request = new DeleteRequest("hotel", "61083");
    // 2.发送请求
    client.delete(request, RequestOptions.DEFAULT);
}

5. 修改文档

如果新增时,ID已经存在,则修改;如果新增时,ID不存在,则新增

与之前类似,也是三步走:

  • 1)准备Request对象。这次是修改,所以是UpdateRequest

  • 2)准备参数。也就是JSON文档,里面包含要修改的字段

  • 3)更新文档。这里调用client.update()方法

@Test
void testUpdateDocument() throws IOException {
    // 1.准备Request
    UpdateRequest request = new UpdateRequest("hotel", "61083");
    // 2.准备请求参数
    request.doc(
        "price", "952",
        "starName", "四钻"
    );
    // 3.发送请求
    client.update(request, RequestOptions.DEFAULT);
}

6. RestAPI - 批量导入数据

  • 利用mybatis-plus查询酒店数据

  • 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)

  • 利用JavaRestClient中的BulkRequest批处理,实现批量新增文档

@Test
void testBulkRequest() throws IOException {
    // 批量查询酒店数据
    List<Hotel> hotels = hotelService.list();

    // 1.创建Request
    BulkRequest request = new BulkRequest();
    // 2.准备参数,添加多个新增的Request
    for (Hotel hotel : hotels) {
        // 2.1.转换为文档类型HotelDoc
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 2.2.创建新增文档的Request对象
        request.add(new IndexRequest("hotel")
                    .id(hotelDoc.getId().toString())
                    .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
    }
    // 3.发送请求
    client.bulk(request, RequestOptions.DEFAULT);
}
  • 1)创建Request对象。这里是BulkRequest

  • 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest

  • 3)发起请求。这里是批处理,调用的方法为client.bulk()方法

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

                                                                                                                                                           

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值