JAVA入门基础_SpringCloud入门学习

目录

分布式的最佳实践:微服务的大致框架

  • 首先微服务,会将我们的功能模块进行拆分

    • 拆分的粒度如何界定?
    • 服务之间如何调用?
    • 服务的调用关系如何管理。
  • 微服务的主要特点有几个

    • 要求拆分的一个个微服务有着单一指责,每个微服务都有自己的数据库
    • 自治:团队独立、技术独立、数据独立、独立部署与交付
    • 面向服务:每个微服务对外暴露接口来提供服务
    • 隔离性强:服务调用之间做好隔离、容错、降级、避免出现级联问题
  • 综上所述,微服务是一种经过良好架构设计的分布式架构方案

单体架构(优缺点)

image

缺点

  • 只要一个地方出问题,那么整个项目都不能运行
  • 只有一个数据库,所有的功能都访问一个数据库(数据库压力大)
  • 如果需要集群,则只能复制一份(造成资源的浪费,因为并不是每个功能都需要那么多的并发量)
  • 技术栈统一(导致没法使用多种不同的技术来解决不同的问题)
  • 后期维护困难

优点

  • 简单
  • 前期的开发成本低

垂直架构

image

缺点

  • 只有一个数据库,所有功能访问同一个数据库
  • 开发成本比单体架构更高
  • 工程之间没有关联关系,因此在不同的工程中需要使用另一个工程的功能(需要把代码全部复制一份过来),此时就造成了代码的大量重复,这是致命缺点

优点

  • 模块之间解耦(但又没完全解)

分布式架构(SOA)

image

微服务架构(知道API网关与服务的区别)

image

API网关:相当于客户端,是对用户端暴露的接口
服务: 就是我们API网关来调用到一个个服务,不过这个中间会夹着一个注册中心

SOA到微服务的主要区别

1、服务的粒度拆分问题

  • SOA:例如商品管理拆分成一个服务。
  • 微服务:商品管理拆分成: 品类服务、商品服务(其实就是拆分粒度更加细)

2、SOA中的服务一般指的只有service层的代码,微服务中的服务是包含整个工程的完整代码(controller、service、dao、entity、utils等等)

3、SOA是集中式管理Service、微服务是集中式管理Controller

什么是SpringCloud呢?

  • springcloud帮助我们集成了各种微服务需要使用到的功能组件,实现开箱即用,大幅的降低了程序员的工作量。

  • SpringCloud是依赖于SpringBoot来构建的一套微服务解决方案,因此SpringCloud与SpringBoot的版本需要进行对应

  • 大致的版本信息如下,更加全面的需要查阅官方文档
    image

  • 本文中所采用的版本为

    • springboot: 2.3.9
    • springcloud:Hoxton.SR10

服务注册与发现、配置中心、负载均衡、远程调用、网关

进行一个最基础的服务拆分与远程调用

定义父工程,引入所需要的必备依赖

- 引入springcloud
- 引入springboot
- 管理mysql的版本
- 管理mybatis的版本
- 管理java的版本为jdk1.8
    <packaging>pom</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
        <mysql.version>5.1.47</mysql.version>
        <mybatis.version>2.1.1</mybatis.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- springCloud,这里就相当于引入了第二个父工程 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <!-- 设置打包方式为pom -->
                <type>pom</type>
                <!-- 将该pom导入到当前项目中 -->
                <scope>import</scope>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

在父工程下创建2个模块(SpringBoot模块)

image

加入一些必备的依赖(web启动器等)

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

编写远程调用的代码RestTemplate(5个条件)

  • URL

  • 请求方式

  • 服务地址/服务名(需要注册中心的服务发现)

  • 发送的参数

  • 返回值类型

  • 在需要进行远程调用的类中添加如下依赖(不加也可以)

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
  • 在任意一个Spring配置类中配置一个RestTemplate的Bean
@Configuration
public class Config {
    @Bean
    @LoadBalanced // 开启负载均衡,内部使用的是Ribbon
    public RestTemplate restTemplate(RestTemplateBuilder builder){
        return builder.build();
    }
}
  • 编写一个远程访问的请求代码
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);

        // 2、调用User服务, 2.1 URL、服务地址、请求参数
        String url = "http://127.0.0.1:8081/user/" + order.getUserId();
                        // 2.1  请求方式、返回值类型
        User user = restTemplate.getForObject(url , User.class);

        // 3、封装结果
        order.setUser(user);
        // 4.返回
        return order;
    }

使用Eureka注册中心

搭建Eureka服务注册中心(服务端)

创建一个模块,添加依赖

    <!-- 1、 引入Eureka注册中心服务端的依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

修改配置,使自身注册到服务中心

spring:
  application:
    name: eureka-server # 服务名称
server:
  port: 10086 # Eureka注册中心服务端口
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka # 由于本身也是一个微服务,也要注册到服务中心

其实也可以不将自己注册到服务中心

spring:
  application:
    name: eureka-server
server:
  port: 10086 # Eureka注册中心服务端口
eureka:
  client:
    service-url:
      # 由于本身也是一个微服务,也要注册到服务中心
      defaultZone: http://127.0.0.1:10086/eureka
    # 是否向注册中心注册自己,缺省:true、一般情况下,Eureka服务端是不需要再注册自己的
    register-with-eureka: false
    # 是否从Eureka获取注册信息,缺省:true, 一般情况下,Eureka服务端是不需要的
    fetch-registry: false

添加一个启动类、启动服务

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

使其他的服务注册到服务中心(客户端)

引入依赖

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

修改配置

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

远程调用的url需要修改

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

注意: Ribbon使用远程调用时,服务名不能用下划线,否则会报错

Request URI does not contain a valid hostname:http://xxx

记住了

查看Eureka的管理界面

  • 访问:localhost:10086即可
    image

Eureka注册中心服务调用的简易流程

  • 当服务注册到了Eureka注册中心后,需要每30秒向Eureka发送心跳,告知当前服务器的运行状态

  • 服务之间的调用时,消费者需要到注册中心中获取可使用的服务列表(服务拉取),找到之后再进行访问调用,此时服务地址就需要换成对应的服务名

使同一个服务开启2次

  • 右键复制一个服务
    image

  • 添加配置-Dserver.port=xxxx,修改个端口号,当然,也可以改变其profile环境
    image
    image

Eureka配置实例名称,以及高可用

首先,Eureka自身需要注册到服务中心中,并且也需要拉取服务列表,因此我们准备2台Eureka

spring:
  application:
    name: eureka-server
eureka:
  instance:
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
---

spring:
  profiles: eureka1
server:
  port: 8050 # Eureka注册中心服务端口
eureka:
  client:
    service-url:
      # 由于本身也是一个微服务,也要注册到服务中心
      defaultZone: http://127.0.0.1:8060/eureka
---

spring:
  profiles: eureka2
server:
  port: 8060 # Eureka注册中心服务端口
eureka:
  client:
    service-url:
      # 由于本身也是一个微服务,也要注册到服务中心
      defaultZone: http://127.0.0.1:8050/eureka

其他需要使用到注册中心的服务,需要配置多个注册中心的列表,OrderService的application.yml

server:
  servlet:
    context-path: /order
  port: 8081

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8050/eureka,http://127.0.0.1:8060/eureka

Ribbon负载均衡

负载均衡策略IRule(默认轮询)

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的、<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

默认的实现就是ZoneAvoidanceRule,是一种轮询方案

自定义负载均衡的2种方式

  • 在使用到负载均衡的服务任意配置类中,配置一个Bean(全局生效)
@Bean
public IRule randomRule(){
    return new RandomRule();
}
  • 修改配置文件方式,指定某个具体使用的服务
user-server: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule 

使Ribbon进行饥饿加载

ribbon:
  eager-load:
    enabled: true
	# 需要指定使用哪个服务时饥饿加载,可以指定多个,逗号分隔
    clients: userservice

负载均衡的两种方式(客户端、服务端)

1、例如nginx就是服务端的负载均衡,因为客户端并不知道具体访问的服务列表

2、注册中心中的服务发现使用的负载均衡,就是客户端的负载均衡,客户端知道能够访问的服务列表,并根据不同的策略进行负载均衡的分流调用

Ribbon的重试机制以及会带来的问题(接口幂等性)

没有请求重试时会出现的问题:
  首先一个服务宕机了,但是注册中心中的服务列表此时还没有剔除,那么我们客户端在拉取服务列表的时候,就会拉取到这个已经宕机的服务,那么访问时,一旦访问到了已经宕机的服务,那么将会直接给用户响应错误界面,这个显然不合理。

请求重试:就是当前请求的实例不可用,那么重试其它服务

1、添加依赖(在进行服务拉取并调用到服务中添加,例如order调用product,那么在order服务中添加)

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

2、修改配置

# 修改指定服务的负载均衡策略
product-server:
  ribbon:
    # 指定负载均衡策略为:随机策略
    #NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
    ConnectTimeout: 250 # Ribbon的连接超时时间 简历http连接的时间
    ReadTimeout: 1000 # Ribbon的数据读取超时时间,请求响应的时间
    OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
    MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
    MaxAutoRetries: 1 # 对当前实例的重试次数

2.2、如果想要配置全局

ribbon:
  # 指定负载均衡策略为:随机策略
  #NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
  ConnectTimeout: 250 # Ribbon的连接超时时间 简历http连接的时间
  ReadTimeout: 1000 # Ribbon的数据读取超时时间,请求响应的时间
  OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
  MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
  MaxAutoRetries: 1 # 对当前实例的重试次数

Nacos注册中心

搭建Nacos注册中心

下载与安装Nacos并启动服务(服务端)

  • 下载地址

  • 下载后将其解压到一个非中文目录下

  • 以为管理员身份打开cmd命令,在其bin目录下运行如下命令
    startup.cmd -m standalone

  • 接下来可以通过如下地址访问:http://192.168.22.1:8848/nacos/index.html默认的账号和密码都是: nacos
    image

在父工程、客户端中引入依赖,并在客户端修改配置

  • 父工程POM
<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>
  • 客户端POM
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  • 修改客户端配置文件
spring:
  cloud:
    nacos:
      server-addr: http://localhost:8848

Nacos服务分级存储模型

  • Nacos可以让我们的一个个客户端实例划分到相对应的集群中,可以在服务列表的一个个详情中看到所在集群。

  • 优先访问相同集群之内的服务实例,相当集群内没有可用的服务实例才考虑其他集群的。但是需要配置ribbon中的负载均衡规则。

  • 修改客户端的配置服务

spring:
  cloud:
    nacos:
      server-addr: http://localhost:8848
      discovery:
        cluster-name: shanghai # 集群

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

权重配置,实现优雅的重启系统

  • 在Nacos中可以配置实例的权重,权重越低,被访问的频率越低

  • 当设置为0时,将不会再被访问

  • 可以逐渐降低某个需要更新的实例的权重,最后设置为0后,再重启该实例
    image

Nacos的环境隔离

  • 默认情况下,所有的客户端都在同一个namespace下

  • 可以通过Nacos创建一个新的namespace,然后通过修改客户端的配置实现访问指定的namespace
    image
    image

spring:
  cloud:
    nacos:
      server-addr: http://localhost:8848
      discovery:
        cluster-name: shanghai
        namespace: 0c222dc3-b003-4fea-80a8-3f3db0baa171
  • 不同namespace下的实例相互之间是不可见的。

为一个服务配置为永久实例(非临时实例)

spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置为非临时实例

Nacos与Eureka作为注册中心的区别

  • Eureka、Nacos都支持接收客户端的心跳检测

  • Eureka、Nacos都可以让客户端拉取服务列表

  • Nacos 可以主动为非临时实例发送请求保证非临时实例的可用性。非临时实例宕机也不会从服务列表中剔除。

  • Nacos 会主动向客户端发送服务列表,增加客户端响应请求的速度。

  • 两者对于宕机的临时实例都会剔除。

搭建Nacos集群

新建一个数据库nacos,执行如下脚本

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

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


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

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

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

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

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


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


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

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

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

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

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

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

复制3个Nacos文件夹,并分别修改2个配置文件(集群配置、数据库配置、端口配置)

  • 复制3个nacos
    image

  • 修改它们配置文件下的cluster.conf.example文件,将其改名为cluster.conf,然后进行追加如下配置

  • 注意~~~ ,这里可以全都改成自动生成的 ip地址,不然可能会出现异常。

127.0.0.1:8848
127.0.0.1:8849
127.0.0.1:8850
  • 修改它们的application.properties文件,分别改成不同的端口
nacos1 : server.port=8848
nacos2 : server.port=8849
nacos3 : server.port=8850
  • 修改它们的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=abc123

搭建Nginx代理服务器,追加如下配置,放在http标签内

upstream nacos-cluster {
    server 127.0.0.1:8848;
	server 127.0.0.1:8849;
	server 127.0.0.1:8850;
}

server {
    listen       80;
    server_name  localhost;

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

启动3台nacos再启动nginx,并且微服务客户端中的nacos注册地址需要换成nginx的地址

  • 启动nacos,需要进入到nacos的bin目录中,开启cmd命令行startup.cmd

  • 启动nginx,双击nginx.exe即可

  • 浏览器通过:http://localhost/nacos即可访问到nacos服务
    image

Nacos 配置管理

  • Nacos可以实现配置的集中管理与热更新

  • 统一配置管理一般就放那些需要热更新的数据

在Nacos中添加配置文件

image

添加客户端的依赖

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

修改客户端的配置,在bootstrap.yml中(服务名、profile)

spring:
  application:
    name: orderservice # 1、服务名称
  profiles:
    active: dev   # 2、profile
  cloud:
    nacos:
      server-addr: http://localhost:80
      config:
        file-extension: yaml # 3、文件后缀名

# 刚好能够构成: orderservice-dev.yaml配置文件

客户端获取配置的2种方式

  • 通过@Value注入,这个类上需要加注解
@RestController
@RefreshScope
public class ConfigController {
    @Value("${pattern.formatter}")
    private String pattern;

    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("/date")
    public String getDate() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
        System.out.println(patternProperties);
        return LocalDateTime.now().format(dateTimeFormatter);
    }
}
  • 通过@ConfigurationProperties和@Component的方式
@Component
@ConfigurationProperties("pattern")
@Data
public class PatternProperties {
    private String formatter;
}

配置共享 以及 3种配置的优先级

  • 有2种命名的配置文件可以作为配置管理,被读取到

  • 第一种就是 : 服务名-profile.后缀名,例如: orderservice-dev.yaml

  • 第二种是: 服务名.后缀名,例如: orderservice.yaml
    image

  • 优先级: orderservice-dev.yaml > orderservice.yaml > 本地配置(本地配置是优先级最低的

nacos扩展公共配置

1、新建配置
image

2、引用通用配置文件

# 指定nacos配置中心的地址
spring:
  cloud:
    nacos:
      discovery:
        server-addr: http://localhost:8848
        username: nacos
        password: nacos
      config:
        # 指定读取配置文件的格式
        file-extension: yaml
        shared-configs:
            # 通用的配置文件的ID,一定要带上后缀名
          - data-id: cloud.demo.common.yaml
            # 自动刷新
            refresh: true

3、配置的优先级

SpringCloudAlibabaNacosConfig目前提供了三种配置能力从Nacos拉取相关的配置。

A:通过spring.cloud.nacos.config.shared-configs支持多个共享DataId的配置
B:通过spring.cloud.nacos.config.ext-config[n].data-id的方式支持多个扩展DataId的配置
C:通过内部相关规则(应用名、应用名+Profile)自动生成相关的DataId配置

当三种方式共同使用时,他们的一个优先级关系是:A < B < C

Feign远程调用

  • Feign可以实现优雅的调用远程服务,不用像之前的RestTemplate那么繁琐

  • Feign通过RestFul定义接口的方式来让其完成远程调用

搭建一个Feign远程调用环境

在需要进行远程调用的服务中引入依赖、添加启动类注解

  • 引入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 在启动类上添加注解
@EnableFeignClients

编写一个接口,添加注解,配置服务间调用的5个条件

@FeignClient("user-server") // 1、服务名
public interface UserClient {

    @GetMapping("/user/{id}") // 2.3.4.5、 请求方式、 请求URL、请求参数、返回值
    User getUserById(@PathVariable Long id);
}

需要使用到的地方,就只需要依赖注入一下即可

    @Autowired
    private UserClient userClient;

修改Feign的自定义配置

类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign. Contract 支持的注解格式 默认是SpringMVC的注解
feign. Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

根据配置文件配置的2种方式

  • 配置对所有的服务都生效
feign:
  client:
    config:
      default:  # 使用default就是对所有服务都生效
        loggerLevel: FULL #  日志级别 ,一般推荐使用 NONE或者BASIC
  • 配置对指定的服务生效
feign:
  client:
    config:
      user-server:  # 使用服务名则代表对单个服务生效
        loggerLevel: FULL #  日志级别 ,一般推荐使用 NONE或者BASIC

JAVA代码配置的2种方式

  • 先创建一个Feign的配置类,注入点Bean
public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}
  • 在启动类上进行指定,相当于全局配置
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
  • 对单个服务生效,在@FeignClient上配置
@FeignClient(value = "user-server",configuration = DefaultFeignConfiguration.class)
public interface UserClient {

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

Feign配置支持连接池的HTTP请求

  • Feign默认底层使用的是URLConnection,而请求方式并不支持连接池

  • 而支持连接池的有Apache HttpClient 和 OKHttp,因此我们可以为Feign配置一个支持连接池的HTTP请求技术

添加HttpClient的依赖

<!--httpClient的依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

在配置文件中修改feign的配置

feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

image

Feign配置数据压缩,以及与ribbon配置重复的情况优先级问题

# feign的相关配置
feign:
  client:
    config:
      product-server:
        connectTimeout: 3000 # 连接超时时间 默认是2S
        readTimeout: 3000 # 处理请求的时间 默认是5S
  compression:
    request:
      # 开启请求数据压缩(默认值为false)
      enabled: true
      # 支持压缩的数据类型
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的数据大小下限
      min-request-size: 2048
    response:
      # 开启响应压缩(默认值为false)
      enabled: true

如果Feign的配置与Ribbon重复,优先使用Feign的

Feign在实际生产中解决代码重复的2种方式

  • 在Feign的使用当中,我们发现FeignClient中的接口代码,与UserService服务所暴露的接口,声明方式是一模一样的。

  • 解决方案一:创建一个模块,该模块中定义UserService暴露服务的统一接口。然后让UserService模块去实现统一接口。再让OrderService也去实现统一接口。

    • 弊端:增加了耦合。2个不同的模块竟然同时依赖于相同的接口
  • 解决方案二:定义一个模块feign-api ,里面专门定义需要进行远程访问的FeignClient,哪个服务需要使用到远程调用,就依赖于feign-api这个模块即可

    • 弊端:即便Feign-api中有些服务在当前服务用不到,但也会引入到当前服务当中,占用资源。

演示解决方案二,定义一个feign-api模块

  • 引入如下依赖(feign和httpclient)
<!--httpClient的依赖 -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
		
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
  • 将UserClient迁移到feign-api模块当中,结构如下
    image

  • 在原有的OrderService服务中引入feign-api模块

<dependency>
    <groupId>cn.codestars.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>
解决扫描包的问题
  • 由于现在UserClient已经不在当前Order模块中了,因此无法扫描到对应的包。有如下2个解决方案

  • 解决方案一:扫描整个包

@EnableFeignClients(basePackages = "com.codestars.api")
  • 解决方案二:单独注册一个个FeignClient(推荐)
@EnableFeignClients(clients = UserClient.class)

GateWay服务网关

  • 有了网关之后,用户以后就访问网关,然后由网关判断该用户是否有权限访问,并在进行了一系列过滤后,为其提供服务

  • 网关大致有如下几个作用:

    • 权限控制:可以校验用户是否有资格访问,没有则进行拦截
    • 路由和负载均衡:网关通过某种规则请求转发到指定的某个微服务(这个过程叫做路由),当目标的路由有多个时,还需要做负载均衡。
    • 限流:当请求流量过大时,网关可以按照下流的处理速度来放行请求,避免服务压力过大。
      image

GateWay快速入门

GatWay也是一个微服务,因此需要创建一个gateway模块引入依赖(并且也需要注册到服务中心)

    <!--网关-->
    <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>

创建启动类

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

修改配置文件

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: http://localhost:80
    gateway:
      routes:
        - id: user-server # 路由id,自定义,只要唯一即可
          uri: lb://user-server  # lb 代表负载均衡, 后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求,意思是:访问/user/开头的请求就会使用该路由
server:
  port: 10010

启动服务后尝试访问(只能处理配置了路由的请求)

http://localhost:10010/user/2

断言工厂、过滤器工厂、全局过滤器

断言工厂分类

名称 说明 示例
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 权重处理
  • 我们只需要掌握Path这一种断言工厂就可以了

过滤器工厂(针对单个路由、默认过滤器)

  • 可以对进入网关的请求进行过滤(进入了网关,意味着已经通过了断言工厂

  • 过滤器工厂的种类,Spring中提供了30多种,这里简单列举几个
    | 名称 | 说明 |
    | -------------------- | ---------------------------- |
    | AddRequestHeader | 给当前请求添加一个请求头 |
    | RemoveRequestHeader | 移除请求中的一个请求头 |
    | AddResponseHeader | 给响应结果中添加一个响应头 |
    | RemoveResponseHeader | 从响应结果中移除有一个响应头 |
    | RequestRateLimiter | 限制请求的流量 |

  • 测试添加一个AddResponseHeader过滤器工厂和一个默认过滤器

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: http://localhost:80
    gateway:
      routes:
        - id: user-server # 路由id,自定义,只要唯一即可
          uri: lb://user-server  # lb 代表负载均衡, 后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求,意思是:访问/user/开头的请求就会使用该路由
          filters:
            - AddResponseHeader=responseTitle, responseTitleBody  # 响应头的key与value使用逗号分割
      default-filters:
        - AddResponseHeader=name, zhangsan  # 默认过滤器,针对所有路由,key与value使用逗号分割
        - AddRequestHeader=name, zhangsan  # 默认过滤器,针对所有路由,key与value使用逗号分割
server:
  port: 10010

全局过滤器

  • 定义一个类,实现GlobalFilter接口
@Order(-1) // 执行优先级,数值越低优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取请求参数
        MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
        // 2.获取authorization参数
        String auth = params.getFirst("authorization");
        // 3.校验
        if ("admin".equals(auth)) {
            // 放行
            return chain.filter(exchange);
        }
        // 4.拦截
        // 4.1.禁止访问,设置状态码
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 4.2.结束处理
        return exchange.getResponse().setComplete();
    }
}

过滤器执行顺序

  • 首先按照优先级执行,也就是Order

  • 其次是按照defaultFilters > 路由的过滤器 > 全局过滤器

路径重写过滤器(重要)

spring:
  application:
    name: gulimall-gateway
  cloud:
    nacos:
      server-addr: localhost:8848
    gateway:
      routes:
        # 2、匹配商品服务的请求
        - id: gulimall-product
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*), /product/$\{segment}
        # 3、匹配第三方服务的请求
        - id: gulimall-third-party
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/third-party/**
          filters:
            - RewritePath=/api/third-party/?(?<segment>.*), /$\{segment}
        # 1、匹配后台管理界面的renren-fast请求
        - id: renren-fast
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/?(?<segment>.*), /renren-fast/$\{segment}
server:
  port: 88

跨域问题解决

  • 在gateway中修改一下配置文件
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: http://localhost:80
    gateway:
      routes:
        - id: user-server # 路由id,自定义,只要唯一即可
          uri: lb://user-server  # lb 代表负载均衡, 后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求,意思是:访问/user/开头的请求就会使用该路由
          filters:
            - AddResponseHeader=responseTitle, responseTitleBody  # 响应头的key与value使用逗号分割
      default-filters:
        - AddResponseHeader=name, zhangsan  # 默认过滤器,针对所有路由,key与value使用逗号分割
        - AddRequestHeader=name, zhangsan  # 默认过滤器,针对所有路由,key与value使用逗号分割
      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 # 这次跨域检测的有效期
server:
  port: 10010

GateWay全局过滤器不生效的情况

问题描述: GateWay网关配置的全局过滤器不生效,检查后发现该过滤器已经成功添加到容器当中。

问题原因:
一定要匹配到了断言的请求,才会进入到过滤器当中,如果使用了loadbance负载均衡,还要保证那个服务必须在注册中心中存在。

Docker容器化技术

为什么要用?有哪些问题需要解决?为什么能够解决出现的问题?

- 因为我们需要管理项目中各个组件非常的繁琐,例如mysql、redis、nginx、elasticsearch、rabbitMQ等等一系列的,首先是**环境安装比较麻烦**。其次是在**不同的Linux环境下**可能导致不可用。
  • 为什么Docker能够解决这些问题?
    • 1、Docker把一个软件所需要的配置、运行环境、运行函数库通通进行了打包,形成了一个镜像,我们只需要通过镜像来创建一个个容器(应用程序)即可轻松完成环境的搭建

    • 2、为什么能够跨不同的Linux平台仍然能运行? 因为它打包了程序运行所需要的函数库,是直接与Linux内核打交道的,与Linux到底是什么平台无关。

Docker在Linux上的下载与安装

卸载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

安装yum-utils工具(安装前最好配置一下yum的下载镜像)

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

更新本地镜像源

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

yum makecache fast

安装Docker,并且关闭Linux防火墙、配置镜像加速器

yum install -y docker-ce

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

启动Docker并查看版本

systemctl start docker  # 启动docker服务

systemctl stop docker  # 停止docker服务

systemctl restart docker  # 重启docker服务

docker -v # 查看docker版本

Docker的常用命令

与镜像有关的命令

- 构建镜像(用于dockerFile),后面的./只是个示例,标识dockerfile的文件地址
docker build ./

- 拉取镜像
docker pull mysql:5.7.2

- 删除镜像
docker rmi mysql:5.7.2

- 查看所有镜像
docker images

- 保存镜像
docker save -o mysqlBak.tar mysql:5.7.2

- 加载镜像
docker load -i mysqlBak.tar

与容器有关的命令

# 运行一个容器,--name当前容器名称,-d后台运行,最后跟 镜像:版本,将本机的80端口映射到容器的80端口
docker run --name mnginx -p 80:80 -d nginx

# 进入一个容器
docker exec -it mnginx bash

# 删除一个容器,-f代表强制删除
docker rm -f mnginx

# 查询当前所有的容器状态
docker ps -a

# 查看容器的日志
docker logs mnginx

# 开启容器 与 重启容器 与 关闭容器
docker start mnginx
docker restart mnginx
docker stop mnginx

# 暂时 与 解除暂停
docker pause
docker unpause

与数据卷相关的命令

# 创建一个数据卷
docker volumn create myHtml

# 删除指定数据卷  与 删除未使用的数据卷
docker volumn rm myHtml
docker volumn prune

# 列出已有的数据卷 与 查看指定的数据卷详情信息
docker volumn ls
docker volumn inspect myHtml

数据卷的默认地址

  • 创建的数据卷默认放在:/var/lib/docker/volumes/

  • 测试一下

docker volume create myHtml

docker volume inspect myHtml

image

小案例(使用docker安装mysql 5.7.2)

  • 拉取镜像docker pull mysql:5.7.25

  • 查看是否拉取成功docker images

  • 执行如下命令完成运行

# -- name 为容器取名
# -e 配置容器的环境变量
# -v 配置数据卷, /etc/mysql/conf.d是容器中数据库的扩展配置, 顺便配置了数据目录
# -d 以后台方式运行
# 注意:/opt/dockerTest/mysql/myConf/my.cnf 这个是需要进行扩展的配置文件,可以自行定义
docker run --name mysql2\
           -p 3306:3306\
           -e MYSQL_ROOT_PASSWORD=abc123\
           -v /opt/dockerTest/mysql/myConf/my.cnf:/etc/mysql/conf.d/my.cnf \
           -v /opt/dockerTest/mysql/data:/var/lib/mysql \
           -d mysql:5.7.25
  • 扩展配置文件/opt/dockerTest/mysql/myConf/my.cnf
[mysqld]
skip-name-resolve
character_set_server=utf8  # mysql 5.7,一定要配置一下字符集
datadir=/var/lib/mysql # 这个地址很重要,因为在容器中,数据目录的地址就是这
server-id=1000

DockerFile自定义镜像

DockerFile文件的几大组成部分

  • (1)From 需要来自于哪个镜像作为初始镜像

  • (2)ENV 配置镜像所需的环境变量

  • (3)COPY 复制当前Linux系统的文件到镜像当中

  • (4)RUN 在镜像中运行一些指令

  • (5)EnterPoint 入口,指的是执行docker run 时会执行的命令

准备一个DockerFile文件,完成springBoot项目的搭建

  • 随便打包一个springboot项目

  • 准备一个jdk8的安装包

  • 编写DockerFile文件

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

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./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 8080
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
  • 最终目录结构
    image

  • 运行DockerFile文件

# -t 设置镜像的名称和版本 ./ 代表在当前目录找DockerFile文件
docker build -t javaweb:1.0 ./
  • 通过docker命令运行该镜像
docker run --name myJar -p 8080:8080 -d javaweb:1.0

通过alpine来创建一个镜像

  • 编写DockerFile文件
# 指定基础镜像, 该镜像已经安装好了JDK环境
FROM java:8-alpine

# 拷贝java项目的包
COPY ./demo.jar /tmp/app.jar

# 暴露端口
EXPOSE 8081

# 入口,java项目的启动命令,这里稍微修改了下启动参数,因为application.yml中配置的端口号是8080
ENTRYPOINT java -jar -Dserver.port=8081 /tmp/app.jar
  • 拖一个java项目到当前目录下,改名为demo.jar

  • 运行docker命令来构建

docker build -t javaweb:2.0 ./
  • 创建该镜像的容器运行
docker run --name myJar2 -p 8081:8081 -d javaweb:2.0

docker-compose 快速部署多个分布式应用(DockerFile的集群版本,能够基于镜像创建容器,也能bulid一个镜像再创建容器)

下载与安装、修改文件权限、自动补全命令配置

  • 通过如下命令下载,并且放在/usr/local/bin目录当中
# 安装
sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  • 修改文件权限
chmod a+x /usr/local/bin/docker-compose
  • 配置自动补全命令

部署微服务集群(示例)docker-compose up -d

准备项目的jar包(注意项目中的nacos与mysql地址)

# 修改的原因:由于待会打docker-compose中会配置指定的服务,可以让其通过服务访问
nacos地址需要修改:server-addr: nacos:8848
mysql地址需要修改:url: jdbc:mysql://mysql:3306/cloud_user?useSSL=false

image

如果最后阶段出现了message from server: "Host '172.25.0.2' is not allowed to connect to this MySQL server之类的错误
  • 在出错的微服务的pom文件中的打包插件中添加如下内容后重新打包
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <!--加入下面两项配置-->
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <includeSystemScope>true</includeSystemScope>
            </configuration>
        </plugin>
    </plugins>

准备Dockerfile文件,每个微服务一份(注意需要修改的地方),注意端口号、文件名

# 指定基础镜像, 该镜像已经安装好了JDK环境
FROM java:8-alpine

# 拷贝java项目的包
COPY ./gateway-1.0.jar /tmp/app.jar

# 暴露端口
EXPOSE 10010

# 入口
ENTRYPOINT java -jar /tmp/app.jar

准备项目的mysql数据目录和配置文件(从开发环境下复制mysql的data目录,并收集其配置文件,注意user表的权限,需要开放能够远程访问)

image
image

  • 配置文件如下(自行修改)
[mysqld]
skip-name-resolve
character_set_server=utf8  # mysql 5.7,一定要配置一下字符集
datadir=/var/lib/mysql # 这个地址很重要,因为在容器中,数据目录的地址就是这
server-id=1000

编写docker-compose.yml 文件

  • services标签下,就是一个个的容器名称

  • 容器名称下面可以配置 所用到的镜像、环境变量、端口、数据卷等,甚至也可以通过DockerFile来构建对应的镜像再生成容器。(注意这里填写的服务名,将来可以使用docker-compose restart mysql nacos userservice)这样使用

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: abc123
    volumes:
      - "$PWD/mysql/data:/var/lib/mysql"
      - "$PWD/mysql/my.cnf:/etc/mysql/conf.d/my.cnf"
  userservice:
    build: ./userservice
  orderservice:
    build: ./orderservice
  gateway:
    build: ./gateway
    ports:
      - "10010:10010"

检查当前的目录结构,并执行docker-compose命令

image

  • 执行如下命令执行docker-compose.yml(注意先cd进入到目录结构当中)
# -d 代表后台方式运行
docker-compose up -d

# 可以通过如下命令查看日志
docker-compose logs -f

# 因为必须nacos先启动,但是它启动的比较慢,所以可能导致另外的服务启动失败,因此可以重启
docker-compose restart userservice gateway orderservice

搭建Docker镜像仓库

  • 先安装registry镜像
docker pull registry

简化版(没有图形界面)

  • 输入如下命令运行
docker run -d \
    --restart=always \
    --name registry	\
    -p 5000:5000 \
    -v registry-data:/var/lib/registry \
    registry

带图形化界面版(编写docker-compose.yaml文件、添加信任、执行文件)

  • 编写一个docker-compose文件
version: '3.0'
services:
  registry:
    image: registry
    volumes:
      - ./registry-data:/var/lib/registry
  ui:
    image: joxit/docker-registry-ui:static
    ports:
      - 8888:80
    environment:
      - REGISTRY_TITLE=个人私有仓库
      - REGISTRY_URL=http://registry:5000
    depends_on:
      - registry
  • 添加信任地址
    image
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:别忘了逗号分割
"insecure-registries":["http://192.168.22.100:8888"]
# 重新加载
systemctl daemon-reload
# 重启docker
systemctl restart docker
  • 运行即可
docker-compose up -d 

往私人镜像仓库上传镜像 | 拉取镜像

  • 重新tag本地镜像,名称前缀为私有仓库的地址
docker tag registry:latest 192.168.22.100:8888/registry:1.11
  • 上传镜像
docker push 192.168.22.100:8888/registry:1.11
  • 拉取镜像
docker pull 192.168.22.100:8888/registry:1.11

image

RabbitMQ 消息队列的使用(基于Docker安装)

同步通讯与异步通讯的各自优缺点

同步通讯

  • 优点

    • 实时性较强,可以立即得到结果
  • 缺点

    • 耦合度高
    • 性能和吞吐能力下降
    • 有额外的资源消耗
    • 有级联失败的问题

异步通讯(流量削峰)

  • 优点

    • 吞吐量提升,无需等待订阅者处理完成,响应速度更快
    • 故障隔离。服务没有直接调用,不存在级联失败问题(疑惑: 那么如果对方失败了,如何保证事物的一致性?)
    • 调用间没有阻塞,不会造成无效的资源占用
    • 耦合度较低,每个服务都可以灵活插拔
    • 流量削峰。不管发生事件的流量波动多大,都由Broker中间商来接收,订阅者可以按照自己的处理速度去处理(需要小心中间商出问题)
  • 缺点

    • 架构复杂了,不好管理
    • 需要依赖于Broker的可靠、安全、性能。

常见的集中MQ对比

几种常见MQ的对比:

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

追求可用性:Kafka、 RocketMQ 、RabbitMQ

追求可靠性:RabbitMQ、RocketMQ

追求吞吐能力:RocketMQ、Kafka

追求消息低延迟:RabbitMQ、Kafka

在Docker环境下载与安装RabbitMQ

  • 获取RabbitMQ的docker镜像(mangement版本是包含web管理页面的)
docker pull rabbitmq:3-management
  • 创建并运行容器
# 进行了端口的映射,5672是服务地址,15672是图形化管理界面
# 添加了一个数据卷映射,后期可以通过docker volumn inspect rabbitMqData查看信息
# 添加了虚拟主机,之后使用的时候,就写该虚拟主机,当然也可以在图形化界面再加一个
# 定义了管理界面的用户名和密码
# 以后台方式运行 -d
docker run  --name rabbitmq3 \
      -p 5672:5672 -p 15672:15672 \
      -v rabbitMqData:/var/lib/rabbitmq \
      --hostname myRabbit \
      -e RABBITMQ_DEFAULT_VHOST=my_vhost  \
      -e RABBITMQ_DEFAULT_USER=admin \
      -e RABBITMQ_DEFAULT_PASS=admin\
      -d\
      rabbitmq:3-management

RabbitMQ中的一些专业名词

  • publisher 生产者

  • consumer 消费者

  • exchange 交换机,负责消息的路由,可能会涉及到路由key

  • queue 队列,存储消息

  • virtualHost 虚拟主机,隔离不同用户使用的exchange、queue,使消息隔离

RabbitMQ的快速入门(使用原生API)

创建一个父工程,2个子工程,引入rabbitMQ的客户端依赖

  • 引入依赖
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.6.0</version>
    </dependency>

创建子工程Publisher生产者,编写一个类

public class MQPublisher {
    public static void main(String[] args) throws Exception {
        // 1、创建连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1 设置链接参数
        factory.setHost("192.168.22.100");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");
        factory.setVirtualHost("/");
        // 1.2 建立连接
        Connection connection = factory.newConnection();

        // 2、创建通道
        Channel channel = connection.createChannel();

        // 3、创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4、发送消息
        String message = "你好呀";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("消息发送成功:" + message);

        // 5、关闭通道与连接
        channel.close();
        connection.close();
    }
}

创建子工程Publisher消费者,编写一个类

public class MQConsumer {
    public static void main(String[] args) throws Exception{
        // 1、获取连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1 设置连接参数
        factory.setHost("192.168.22.100");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");
        factory.setVirtualHost("/");
        // 1.2 建立连接
        Connection connection = factory.newConnection();

        // 2、 创建通道
        Channel channel = connection.createChannel();

        // 3、 创建队列
        String queue = "simple.queue";
        channel.queueDeclare(queue,false, false, false, null);

        // 4、订阅消息
        channel.basicConsume(queue, true ,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("获取到生产者发送的消息,开始消费:" + new String(body));
            }
        });

        // 5、等待接收消息
        System.out.println("主线程去忙别的了。~~");
    }
}

使用SpringCloud中的AMQP

创建SpringBoot项目,引入SpringBoot及amqp启动器

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

配置application.yml(五步走)

spring:
  rabbitmq:
    host: 192.168.22.100
    port: 5672
    username: admin
    password: admin
    virtual-host: /

简单队列模型

  • 通过一个单独的队列来完成消息的订阅与发送称为简单队列模型

  • 消息发布者

    @Test
    public void publish() {
        // 1、 准备消息
        String message = "hello rabbitMQ";

        // 2、 定义队列,routeKey好像就是队列
        String queueName = "simple.queue";

        // 3、发送消息
        rabbitTemplate.convertAndSend(queueName, message);
        System.out.println("发布者已经发布了消息:" + message);

    }
  • 消息发布者,编写如下代码监听队列,然后启动一下SpringBoot项目
@Component
public class SpringRabbitListener {

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

WrokQueue任务模型

  • 该模型是多个消费者监听同一个队列,他们之间将会默认会很平均的分摊消费者发送的消息
    image

  • 发布者模块的代码可以稍微改动下,循环发送50条消息

    @Test
    public void publish() {
        // 1、 准备消息
        String message = "hello rabbitMQ";

        // 2、 定义队列,routeKey好像就是队列
        String queueName = "simple.queue";

        // 3、发送消息
        for (int i = 1; i <= 50; i++) {
            rabbitTemplate.convertAndSend(queueName, message + i);
            System.out.println("发布者已经发布了消息:" + message);
        }

    }
  • 消费者模块需要再添加一个消费者
@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueConsumer1(String msg) {
        System.out.println("消费者1号接收到消息:" + msg);
    }


    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueConsumer2(String msg) {
        System.out.println("消费者2号接收到消息:" + msg + "-------------");
    }
}

任务模型能者多劳的配置

  • 从运行结果上看,发现2个任务所消费的消息数量均是一致的。

    • 那是因为消费者在获取队列任务时,不管能不能执行完,先获取到再说
  • 修改配置实现能者多劳

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

发布订阅模型概念解释

  • publisher 发布者,作用依然是发布消息,但是消息不再直接发送给queue了,而是发送给exchange

  • exchange 交换机,用于进行消息的路由,接收到publisher的消息后路由到指定的queue中

    • fanout:广播,将消息路由给每所有绑定到了该交换机上的队列
    • direct:定向,将消息路由给指定routeKey的队列
    • Topic:通配符,将消息路由给指定routeKey的队列(但是可以使用通配符,#代表1个单词,*代表1个或多个单词)
  • queue 队列,用于进行数据的接收与缓存

  • consumer 消费者,作用一样,还是订阅队列

image

Fanout 广播发布订阅模型

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

  • 1) 可以有多个队列
  • 2) 每个队列都要绑定到Exchange(交换机)
  • 3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
  • 4) 交换机把消息发送给绑定过的所有队列
  • 5) 订阅队列的消费者都能拿到消息

声明队列和交换机,使队列与交换机进行绑定

@Configuration
public class FanoutConfig {
    /**
     * 定义一个fanout交换机
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExChange");
    }

    // 定义第一个队列
    @Bean
    public Queue queue1() {
        return new Queue("fanout.queue1");
    }

    // 定义第一个绑定器,将队列与交换机绑定
    @Bean
    public Binding binding1 (FanoutExchange fanoutExchange, Queue queue1) {
        return BindingBuilder.bind(queue1).to(fanoutExchange);
    }

    // 定义第二个队列
    @Bean
    public Queue queue2() {
        return new Queue("fanout.queue2");
    }

    // 定义第二个绑定器,将队列与交换机绑定
    @Bean
    public Binding binding2 (FanoutExchange fanoutExchange, Queue queue2) {
        return BindingBuilder.bind(queue2).to(fanoutExchange);
    }
}

发布者发布消息给交换机

@Test
public void publishFanout() {
    // 1、 准备消息
    String message = "hello direct exchange";

    // 2、定义交换机名称
    String exChangeName = "fanoutExchange";

    // 3、发送消息。中间那个没填的参数是 routingkey
    rabbitTemplate.convertAndSend(exChangeName, "", message);
}

消费者监听队列消费消息

@Component
public class FanoutConsumer {
    @RabbitListener(queues = "fanout.queue1")
    public void receiveMessageFromQueue1(String message) {
        System.out.println("Received message from Queue 1: " + message);
    }

    @RabbitListener(queues = "fanout.queue2")
    public void receiveMessageFromQueue2(String message) {
        System.out.println("Received message from Queue 2: " + message);
    }
}

FanOut广播方式的总结(交换机的作用,会使用到的Bean)

交换机的作用是什么?

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

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

  • Queue
  • FanoutExchange
  • Binding

Direct定向发布订阅模型

  • Direct模型中,发布者依然是发送消息给ExChange,但不同的是,ExChange只会将消息路由给指定routingkey的队列(前提也得是与交换机进行了绑定)。

发布者发布消息,需要指定routingkey和交换机

    @Test
    public void publishDirect() {
        // 1、 准备消息
        String message = "hello direct exchange";

        // 2、定义交换机名称
        String exChangeName = "directExChange";

        // 3、发送消息。中间那个没填的参数是 routingkey
        rabbitTemplate.convertAndSend(exChangeName, "red", message);
    }

消费者消费消息,这里采用注解的方式完成队列与交换机的绑定(别忘了交换机的类型)

@Component
public class SpringRabbitListenerDirect {
    // 消费者1号,监听direct.queue1 队列,与此同时接收routingkey为blue和red的消息
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue("direct.queue1"),
                    exchange = @Exchange(value = "directExChange", type = ExchangeTypes.DIRECT),
                    key = {"blue", "red"}
            )
    )
    public void listenDirectConsumer1(String msg) {
        System.out.println("消费者1号接收到消息:" + msg);
    }

    // 消费者2号,监听direct.queue2 队列,与此同时接收routingkey为blue消息
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue("direct.queue2"),
                    exchange = @Exchange(value = "directExChange", type = ExchangeTypes.DIRECT),
                    key = {"blue"}
            )
    )
    public void listenDirectConsumer2(String msg) {
        System.out.println("消费者2号接收到消息:" + msg);
    }
}

Topic消费订阅模型

  • 与direct唯一的区别就是,定义routingkey的时候可以使用通配符了。

配置队列与交换机的关系

@Configuration
public class TopicConfig {
    /**
     * 定义一个topic交换机
     * @return
     */
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange("topicExChange");
    }

    // 定义第一个队列
    @Bean
    public Queue queue1() {
        return new Queue("topic.queue1");
    }

    // 定义第一个绑定器,将队列与交换机绑定
    // 为queue1配置china.#这个routingkey,能匹配chain.开头的routingkey,但是一个#仅能代表一个单词
    @Bean
    public Binding binding1 (TopicExchange topicExchange, Queue queue1) {
        return BindingBuilder.bind(queue1).to(topicExchange).with("china.#");
    }

    // 定义第二个队列
    @Bean
    public Queue queue2() {
        return new Queue("topic.queue2");
    }

    // 定义第二个绑定器,将队列与交换机绑定,
    // 为queue2配置china.news这个routingkey,能匹配chain.开头的任意routingkey
    @Bean
    public Binding binding2 (TopicExchange topicExchange, Queue queue2) {
        return BindingBuilder.bind(queue2).to(topicExchange).with("china.*");
    }
}

发布者发布消息,与Direct一致

    @Test
    public void publishTopic() {
        // 2、定义交换机名称
        String exChangeName = "topicExChange";

        // 3、发送消息。中间那个没填的参数是 routingkey,china.# 和 china.* 都能接收到
        rabbitTemplate.convertAndSend(exChangeName, "china.study", "chind.study 消息");

        // 3、发送消息。中间那个没填的参数是 routingkey,china.# 能接收到
        rabbitTemplate.convertAndSend(exChangeName, "china.study.news", "chind.study.news 消息");
    }

消费者消费消息

@Component
public class SpringRabbitListenerTopic {
    // 消费者1号,监听topic.queue1 队列
    @RabbitListener(queues = "topic.queue1")
    public void listenTopicConsumer1(String msg) {
        System.out.println("消费者1号接收到消息:" + msg);
    }

    // 消费者2号,监听topic.queue2 队列
    @RabbitListener(queues = "topic.queue2")
    public void listenTopicConsumer2(String msg) {
        System.out.println("消费者2号接收到消息:" + msg);
    }
}

使用JackSon的消息转换器

  • 为什么需要使用到消息转换器?

    • 因为通过rabbitMQ的图形化界面工具来查看发布者发送的消息,可以看到是使用application/x-java-serialized-object形式来发送与接收的

    • 因此将其转换成了一个很长的字符串, 非常的耗费内存

    • 因此可以通过自定义转换器的方式,使其以 application/json的格式来发送与接收

  • 引入jackson的依赖,如下2个都可以

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>
		
<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();
    }

ElasticSearch分布式搜索

ElasticSearch的常见概念(索引、映射、文档、字段、DSL、ELK、倒排索引等、分片及副本)

  • ElasticSearch的概念与Mysql中的概念对比
MYSQL ElasticSearch
table index
记录 文档
cloumn field
check mapping
SQL DSL
  • ELK 表示辅助ElasticSearch使用的三件套

    • E,ElasticSearch
    • L,Logstash和Beats 完成分布式日志管理
    • K,Kibana 日志统计展示与数据可视化
  • 倒排索引

    • 说到倒排索引就不得不说到我们的正向索引,向MYSQL使用的就是正向索引,以ID或二级字段作为关键字而构建的索引。
  • 倒排索引

    • 以文档数据的字或词来作为索引,以文档的ID作为该索引对应的值,一个索引可以对应多个文档ID
    • 以ik分词器: “今天天气真好”,“昨天天气也真好”,2个文档为例,假设他们的id为1跟2
索引列 对应的id
今天 1
天气 1,2
真好 1,2
昨天 2
2
  • 分片
    • 一般在集群当中才会有分片的概念,分片相当于存储数据的节点
    • 而分片一般都会对应着副本
    • 集群环境下,一定需要保证正在运行的服务器可以获取到所有分片的数据。

ElasticSearch的下载与安装(采用docker,注意与ES同一个网络)

  • 使用docker命令获取相对应的镜像
docker pull elasticsearch:7.12.1
  • 创建一个自定义的docker网络,使其网络内的容器可以相互连接
docker network create es-net
  • 创建容器并启动,启动后访问http://192.168.22.100:9200/即可看到响应
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:端口映射配置

安装Kibana可视化界面工具

  • 使用docker获取镜像
docker pull kibana:7.12.1
  • 创建容器并运行
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启动一般比较慢,需要多等待一会,可以通过命令:

docker logs -f kibana
  • 访问如下网址打开图形化界面
http://192.168.22.100:5601/

# 如下地址可以打开 开发者工具,可以向elasticsearch发送请求
http://192.168.22.100:5601/app/dev_tools#/console

安装IK分词器

  • IK分词器有2种

    • ik_smart:最大粒度分次,能4个字分成一个此就不会分成3个
    • ik_max_word:最细切分,与ik_smart相反
  • 在线安装ik插件(较慢)

# 进入容器内部
docker exec -it elasticsearch /bin/bash

# 在线下载并安装
./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

#退出
exit
#重启容器
docker restart elasticsearch
  • 离线安装ik插件
    • 先通过先前的设置查看当前数据卷所在的地址
    docker volume ls
    docker volume inspect es-plugins
    
    • 进入到该插件目录,将下载好的IK插件解压后放进去
    • 重启es即可生效

配置IK分词器忽略词 以及 添加一些词语

  • 修改ik分词器config目录下的IKAnalyzer.cfg.xml
# 注意 ext.dic 和 stopword.dic
<?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>
	<!--用户可以在这里配置远程扩展字典 -->
	<!-- <entry key="remote_ext_dict">words_location</entry> -->
	<!--用户可以在这里配置远程扩展停止词字典-->
	<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
  • 就在当前config目录下创建ext.dic文件,一个词语按一个回车即可,最后不用留空行
白嫖
奥利给
疾风剑豪
亚索
  • 重启es即可docker restart es

部署ES集群

  • 编写一个docker-compose.yml文件
version: '2.2'
services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elastic
  es02:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es02
    environment:
      - node.name=es02
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data02:/usr/share/elasticsearch/data
    networks:
      - elastic
  es03:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es03
    environment:
      - node.name=es03
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data03:/usr/share/elasticsearch/data
    networks:
      - elastic

volumes:
  data01:
    driver: local
  data02:
    driver: local
  data03:
    driver: local

networks:
  elastic:
    driver: bridge
  • 直接运行docker-compose up

索引库操作

mapping映射属性

  • mapping映射是对索引库内文档的约束,常见的mapping映射属性有:

    • type 字段类型
      • 字符型: text(全文检索),keyword(精确值,例如品牌、国家、ip地址等不可分割)
      • 数值型: integer、long、short、byte、double、float
      • 布尔: boolean
      • 日期: date
      • 对象: object
    • index 是否创建索引
    • analyzr:使用哪种分词器,默认为standard
    • properties: 字段的子字段
  • mapping映射的使用语法

PUT /myindex
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword",
        "index": true
      },
      "age": {
        "type": "short",
        "index": false
      }
    }
  }
}

创建索引库和映射(索引别写大写)

  • 使用示例
PUT /myindex
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword",
        "index": true
      },
      "age": {
        "type": "short",
        "index": false
      }
    }
  }
}

查询索引库

GET /myindex

修改索引库(添加mapping字段,注意请求的josn是否要携带mappings)

  • 索引库一旦建立之后,无法对其中的字段进行修改

  • 但是可以添加新的字段,示例如下

PUT /myindex/_mapping
{
    "properties": {
      "info" : {
         "type": "text",
         "index": true,
         "analyzer": "ik_max_word"
      }
    }
}

删除索引库

DELETE /myindex

总结

  • 索引库的增删改查均使用RestFul风格的请求

  • 新增和修改使用: PUT /indexName {请求体}

    • 修改时,需要 put /indexName_mapping,并且请求体不再需要mappings包裹
  • 删除使用: DELETE /indexName

  • 查询:GET /indexName

文档操作

  • 测试之前,先创建如下索引库
PUT /myindex
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword",
        "index": true
      },
      "age": {
        "type": "short",
        "index": false
      },
      "info": {
        "type": "text",
        "analyzer": "ik_smart"
      }
    }
  }
}

新增文档(POST与PUT)

POST /myindex/_doc/1
{
  "name": "张三",
  "age": 18,
  "info": "这是一个非常可爱的孩子,法外狂徒~~"
}

# 若文档ID存在,则覆盖掉原本ID为1的文档(字段只会剩下覆盖后的)。
# 若文档ID不存在,则相当于新增
PUT /myindex/_doc/1
{
  "name": "张三",
  "age": 88
}

删除文档

DELETE /myindex/_doc/1

修改文档

全量修改

# 文档ID存在则为修改
PUT /myindex/_doc/1
{
  "name": "张三",
  "age": 88
}

增量修改

POST /myindex/_update/1
{
  "doc": {
    "name": "张三",
    "age": 11
  }
}

查询文档

GET /myindex/_doc/1

RestAPI 使用Java来完成对索引库的增删改查、文档的增删改查

创建一个SpringBoot工程,引入如下依赖,并解决依赖仲裁问题

<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

初始化RestClient完成对索引库的操作

  • 初始化RestClient
    private RestHighLevelClient restHighLevelClient;

    @BeforeEach
    public void before() {
        restHighLevelClient = new RestHighLevelClient(
                RestClient.builder(HttpHost.create("http://192.168.22.100:9200"))
        );
    }
	@AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

新增索引库

    String MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"name\": {\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_smart\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";

    @Test
    void addIndex() throws IOException {
        // 1、准备request对象
        CreateIndexRequest request = new CreateIndexRequest("hotel");

        // 2、封装DSL
        request.source(MAPPING_TEMPLATE, XContentType.JSON);

        // 3、发送请求
        restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
    }

删除索引库

    @Test
    void deleteIndex() throws IOException {
        // 1、准备request对象
        DeleteIndexRequest request = new DeleteIndexRequest("hotel");

        // 2、发送请求
        restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
    }

判断索引是否存在

    @Test
    void testExistsIndex() throws IOException {
        // 1、准备request对象
        GetIndexRequest request = new GetIndexRequest("hotel");

        // 2、发送请求
        boolean isExists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);

        // 3、处理请求
        System.out.println(isExists ? "索引存在" : "索引不存在");
    }

查询索引库

    @Test
    void testExistsIndex() throws IOException {
        // 1、准备request对象
        GetIndexRequest request = new GetIndexRequest("hotel");

        // 2、发送请求
        GetIndexResponse response = restHighLevelClient.indices().get(request, RequestOptions.DEFAULT);

        // 3、处理请求,这里可以通过response获取到索引的信息
        System.out.println(response);
    }

完成对文档的操作

新增文档

    @Test
    void testInsertDocument() throws IOException {
        // 1、从数据库中查询一条数据
        Hotel hotel = hotelService.getById(36934);
        // 2、转换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 3.将HotelDoc转json
        String json = JSON.toJSONString(hotelDoc);
        
        
        // 1、准备request对象
        IndexRequest request = new IndexRequest("hotel").id("1");

        // 2、封装DSL
        request.source(json,XContentType.JSON);

        // 3、发送请求
        restHighLevelClient.index(request, RequestOptions.DEFAULT);
    }

删除文档

    @Test
    void testDeleteDocument() throws IOException {
        // 1、准备request对象
        DeleteRequest request = new DeleteRequest("hotel").id("1");

        // 2、发送请求
        restHighLevelClient.delete(request, RequestOptions.DEFAULT);
    }

修改文档(增量,并且如果ID不存在则为新增)

    @Test
    void testUpdateDocument() throws IOException {
        // 1、准备request对象
        UpdateRequest request = new UpdateRequest("hotel", "2");

        // 2、封装DSL,这个是增量修改
        request.doc(
                "price",1999,
                "startName","四钻"
        );

        // 2、发送请求
        restHighLevelClient.update(request, RequestOptions.DEFAULT);
    }

查询文档

    @Test
    void testGetDocument() throws IOException {
        // 1、准备request对象
        GetRequest request = new GetRequest("hotel").id("2");

        // 2、发送请求
        GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);

        // 3、处理结果
        String json = response.getSourceAsString();

        // 3.1、将结果封装成实体类对象
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println(hotelDoc);
    }

批量插入文档

    @Test
    void testBulkDocument() throws IOException {
        // 1、准备数据
        List<Hotel> hotelList = hotelService.list();
        // 2、将其封装成HotelDoc对象
        List<HotelDoc> hotelDocList = hotelList.stream().map(HotelDoc::new).collect(Collectors.toList());
        
        // 3、准备request对象
        BulkRequest request = new BulkRequest("hotel");

        // 4、封装DSL
        hotelDocList.forEach(hotelDoc -> {
            request.add(new IndexRequest()
                       .source(JSON.toJSONString(hotelDoc), XContentType.JSON)
                       .id(hotelDoc.getId().toString()));
        });

        // 5、发送请求
        restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
    }

查询文档

SQL查询的分类

  • 查询所有: match all

  • 全文检索: match_query、multi_match_query

  • 精确查询: trem_queryrange_query、ids

  • 地理查询: geo_distance、geo_bounding_box

  • 复合查询: boolfunction score

全文检索查询

  • 适用范围: 需要根据用户输入的词条查询倒排索引时的情况

  • 一般常见场景: 京东的搜索框、百度的搜索框

  • match_all示例

GET /hotel/_search
{
  "query": {
    "match_all": {}
  }
}

match查询示例

  • 查询语法
GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}
  • 查询示例
GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家外滩"
    }
  }
}

multi_match查询示例

  • 查询语法
GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT",
      "fields": ["Field1","Field2","Field3"]
    }
  }
}
  • 查询示例
GET /hotel/_search
{
  "query": {
    "multi_match": {
      "query": "如家",
      "fields": ["city","brand","name"]
    }
  }
}

精确查询

trem关键字查询

GET /hotel/_search
{
  "query": {
    "term": {
      "city": "北京"
    }
  }
}

range范围查询

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 100,
        "lte": 300
      }
    }
  }
}

地理坐标查询

矩形范围查询

GET /hotel/_search
{
  "query": {
    "geo_bounding_box": {
      "location": {
        "top_left": {
           "lat": 31.5,
          "lon": 121.48
        },
        "bottom_right": {
           "lat": 31.4,
           "lon": 121.24
        }
      }
    }
  }
}

附近查询

GET /hotel/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km",
      "location": "31.2, 121.48"
    }
  }
}

复合查询

相关性算分

image

  • 这个例子中,相当于把城市为上海,倒排索引中有如家|外滩的,分数都乘以了10倍
GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "all": "如家外滩"
        }
      }, 
      "functions": [
        {
          "filter": {"term": {"city": "上海"}},
          "weight": 10
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

布尔查询(复合,注意畸形查询问题)

  • must: 必须要满足的条件(参与算分)

  • should:选择性匹配,(参与算分,相当于或)

  • must_not: 必须不匹配不参与算分,类似于非

  • filter: 必须匹配不参与算分

  • 使用示例

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"city": "上海" }}
      ],
      "should": [
        {"term": {"brand": "皇冠假日" }},
        {"term": {"brand": "华美达" }}
      ],
      "must_not": [
        { "range": { "price": { "lte": 500 } }}
      ],
      "filter": [
        { "range": {"score": { "gte": 45 } }}
      ]
    }
  }
}

搜索结果处理

排序(与query同级)

普通字段排序

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "starName": {
        "order": "desc"
      }
    },
    {
      "price": {
        "order": "asc"
      }
    }
  ]
}

地理坐标排序

  • 根据经纬度升序排序
    • unit:最后能看到一个sort字段,可以看到距离有多少km
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 22.533753,
          "lon": 114.122491
        },
        "order": "asc",
        "unit": "km"
      }
    }
  ]
}

分页

基本分页

  • form 相当于mysql中limit的偏移量

  • size 需要显示多少条记录

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 5,
  "size": 5
}

深度分页

  • 当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求
# 超过1万,直接报错
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 9991,
  "size": 10
}
  • 官方提供了2种解决方案
    • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。(无法往上翻页
    • scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。

高亮

高亮原理及演示(搜索条件必须带关键字)

  • 原理: 将匹配到的倒排索引单独领出来放在响应体的一个字段当中,默认会为其添加上em这个标签。当然标签也可以自定义。

  • 注意事项

    • 高亮是对关键字高亮,因此搜索条件必须带有关键字(比如range),而不能是范围这样的查询。
    • 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
    • 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
  • 使用示例

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "上海"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "require_field_match": "false"
      }
    }
  }
}

RestClinet 查询文档(默认只能查询10条记录,注意size)

全文检索查询match

  • 封装一个方法,用来处理响应结果
    @Test
    void testMatchAll() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");
        // 2、封装DSL
        MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
        searchRequest.source().query(matchAllQueryBuilder).size(100);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }
  • match_all查询
    @Test
    void testMatchAll() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");
        // 2、封装DSL
        MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
        // 2.2 这里添加了size是希望查询出100条数据,不然默认只能查询出一条
        searchRequest.source().query(matchAllQueryBuilder).size(100);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }
  • match 查询
    @Test
    void testMatch() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");
        // 2、封装DSL
        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", "上海");
        searchRequest.source().query(matchQueryBuilder);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

精确查询(term、range)

  • term 精确查询QueryBuilders.termQuery()
    @Test
    void testTerm() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");
        
        // 2、封装DSL
        searchRequest.source().query(QueryBuilders.termQuery("city","北京")).size(100);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }
  • range范围查询
    @Test
    void testRange() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        searchRequest.source().query(QueryBuilders.rangeQuery("price").lte(300));

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

地理查询(geo_bounding_box、geodistance)

  • 矩形范围查询geoBoundingBoxQuery
    @Test
    void testGeoBoundingBox() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        GeoBoundingBoxQueryBuilder geoBoundingBoxQueryBuilder = QueryBuilders.geoBoundingBoxQuery("location");
        geoBoundingBoxQueryBuilder.topLeft().reset(31.5, 121.48);
        geoBoundingBoxQueryBuilder.bottomRight().reset(31.4, 121.24);
        searchRequest.source().query(geoBoundingBoxQueryBuilder);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }
  • 中心原点距离查询geoDistanceQuery
    @Test
    void testGeoDistance() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders
                // 经纬度字段
                .geoDistanceQuery("location")
                // 设置维度、 经度
                .point(31.4, 121.24)
                // 设置查询距离中心原点 100km 范围内的
                .distance("100", DistanceUnit.KILOMETERS);
        searchRequest.source().query(geoDistanceQueryBuilder);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

复合查询(score_function,bool),布尔的must和should如果存在相同匹配,则根据must的来

  • 算分查询
    @Test
    void testFunctionScore() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
                // 原始查询
                QueryBuilders.matchQuery("all", "如家外滩"),
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                // 过滤条件,只对满足过滤条件的进行算分
                                QueryBuilders.termQuery("city","北京"),
                                // 定义算分规则,这里采用weight指定算分结果
                                ScoreFunctionBuilders.weightFactorFunction(10)
                        )
                }
        );
        searchRequest.source().query(functionScoreQueryBuilder);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }
  • bool布尔查询
    @Test
    void testBool() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        BoolQueryBuilder boolQueryBuilder = QueryBuilders
                .boolQuery()
                .must(QueryBuilders.termQuery("city", "北京"))
                .should(QueryBuilders.termQuery("brand", "君悦"))
                .filter(QueryBuilders.matchQuery("name", "如家"))
                .mustNot(QueryBuilders.rangeQuery("price").gte(1000));

        searchRequest.source().query(boolQueryBuilder);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);

RestClient 搜索结果处理

排序

    @Test
    void testSort() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        searchRequest.source().sort("price", SortOrder.DESC);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

分页

    @Test
    void testPage() throws IOException {
        // 0、准备分页数据
        int pageNo = 2;
        int pageSize = 5;

        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        searchRequest.source()
                .from((pageNo - 1) * pageSize)
                .size(pageSize);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

高亮

    @Test
    void testhignLigter() throws IOException {
        // 0、准备分页数据
        int pageNo = 2;
        int pageSize = 5;

        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        // 2.1 先根据关键字查询
        searchRequest.source().query(QueryBuilders.matchQuery("all", "如家"));
        // 2.2 设置高亮
        searchRequest.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

    /**
     * 处理响应结果
     *
     * @param response
     */
    private void handlerResponse(SearchResponse response) {
        SearchHit[] hits = response.getHits().getHits();
        List<HotelDoc> list = new ArrayList<>();
        for (SearchHit hit : hits) {
            // 获取到每一条记录
            String record = hit.getSourceAsString();
            // 封装成HotelDoc对象
            HotelDoc hotelDoc = JSON.parseObject(record, HotelDoc.class);
            // 处理高亮字段
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            // 1、健壮性判断,判断Map集合是否为空
            if(! CollectionUtils.isEmpty(highlightFields)) {
                // 2、获取到高亮字段结果
                HighlightField nameFiled = highlightFields.get("name");
                // 3、判断结果是否为空
                if(ObjectUtils.isNotEmpty(nameFiled)) {
                    // 获取到高亮的具体结果:Text
                    Text[] fragments = nameFiled.getFragments();
                    // 由于只需要获取到数组中第一个元素赋值,因此判断下
                    if (fragments.length > 0) {
                        hotelDoc.setName(fragments[0].string());
                    }
                }
            }
            // 存储
            list.add(hotelDoc);
        }
        list.forEach(System.out::println);
    }

数据聚合aggregations

聚合的种类(3类,参加聚合的字段必须是keyword、日期、数值、布尔类型)

  • 桶聚合(Bucket)

    • TermAggregation : 按照文档字段进行聚合,类似于MYSQL的分组
    • DateAggregation: 按照日期进行聚合,例如一周、一个月为一组
  • 度量聚合(Metric)

    • max
    • min
    • avg
    • stats:同时求 最大、最小、平均
  • 管道聚合(pipeline):以其他聚合的结果为基础进行聚合

使用DSL实现聚合(聚合名称、聚合字段、排序字段、显示个数)

Bucket桶聚合(有点像分组统计Count)

GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10
      }
    }
  }
}

// 解释
GET /hotel/_search
{
  "size": 0,  // 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": { // 定义聚合
    "brandAgg": { //给聚合起个名字
      "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
        "field": "brand", // 参与聚合的字段
        "size": 20 // 希望获取的聚合结果数量
      }
    }
  }
}
  • 结果解析
    image
统计结果按照统计数量升序排序
  • 默认是按照统计数量降序排的,可以进行修改
GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": {
          "_count": "asc"
        }
      }
    }
  }
}

限定聚合范围查询

GET /hotel/_search
{
  "size": 0,
  "query": {
    "term": {
      "city": "北京"
    }
  }, 
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": {
          "_count": "desc"
        }
      }
    }
  }
}

Metric聚合

  • 按照评分,统计其 最大值、最小值、平均值、个数、总和
GET /hotel/_search
{
  "size": 0,
  "query": {
    "term": {
      "city": "北京"
    }
  }, 
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": {
          "_count": "desc"
        }
      }
    }
  }
}

桶排序嵌套聚合分组,桶排序甚至可以使用嵌套的聚合分组的字段来进行排序

GET /hotel/_search
{
  "size": 0,
  "query": {
    "term": {
      "city": "北京"
    }
  }, 
  "aggs": {
    "bucketAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": {
          "scoreAgg.avg": "desc"
        }
      },
      "aggs": {
        "scoreAgg": {
          "stats": {
            "field": "score"
          }
        }
      }
    }
  }
}

使用RestAPI实现聚合

桶Bucket聚合之Term

    @Test
    void testBucketTermAgg() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        searchRequest.source()
                .aggregation(AggregationBuilders
                        .terms("aggBucket")
                        .field("brand")
                        .size(10)
                )
                .size(0);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
}

度量Metric聚合

    @Test
    void testMetricStatsAgg() throws IOException {
        // 1、准备requets
        SearchRequest searchRequest = new SearchRequest("hotel");

        // 2、封装DSL
        searchRequest.source()
                .aggregation(AggregationBuilders
                        .stats("aggScore")
                        .field("score")
                )
                .size(0);

        // 3、发送请求
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 4、处理响应结果
        handlerResponse(response);
    }

自动补全功能

安装拼音分词器

https://github.com/medcl/elasticsearch-analysis-pinyin

  • 安装的步骤与ik分词器一致

  • 测试用法,

POST /_analyze
{
  "text": "如家酒店还不错",
  "analyzer": "pinyin"
}

分词器的三个组成部分

  • 从pinyin分词器上直接来看,完全不符合我们的预期,因此需要自定义分词器

  • 我们还需要考虑到我们需要中文分词的。因此可以先用中文分词器分词之后,再保留拼音分词器。

elasticsearch中分词器(analyzer)的组成包含三部分:

  • character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
  • tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
  • tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

自定义分词器(只是设置映射时需要使用拼音分词器,查询依然用ik)

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": {  
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          "type": "pinyin", 
    		  "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

自动补全

  • 需要自动补全的字段必须为 completion类型

  • 自动补全的功能为suggest建议

  • 使用示例

// 创建索引库
PUT test
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}

// 示例数据
POST test/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
  "title": ["SK-II", "PITERA"]
}
POST test/_doc
{
  "title": ["Nintendo", "switch"]
}


// 自动补全查询
GET /test/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", // 关键字
      "completion": {
        "field": "title", // 补全查询的字段
        "skip_duplicates": true, // 跳过重复的
        "size": 10 // 获取前10条结果
      }
    }
  }
}

酒店自动补全功能

  • 前台: 监听键盘输入,输入后发送一个请求,向后台获取一个List<String>

  • 后台实现思路

    • 重建ES中的索引库,增加一个字段suggestion,类型为completion
    • 与ES进行交互的POJO类也需要增加一个字段suggestion,类型为List<String>
      • 从MYSQL向ES导入数据时,会用到该POJO类,将需要自动补全的字段的String内容add到suggesion这个List变量当中
    • 重新为ES填充数据
    • 直接查询就好,后台通过RestAPI进行处理

新建索引库(核心在于自定义分词器,还有一个自动补全的字段)

PUT /hotel
{
  "settings": {
    "analysis": {
      "analyzer": {
        "text_anlyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
          "tokenizer": "keyword",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id":{
        "type": "keyword"
      },
      "name":{
        "type": "text",
        "analyzer": "text_anlyzer",
        "search_analyzer": "ik_smart",
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "score":{
        "type": "integer"
      },
      "brand":{
        "type": "keyword",
        "copy_to": "all"
      },
      "city":{
        "type": "keyword"
      },
      "starName":{
        "type": "keyword"
      },
      "business":{
        "type": "keyword",
        "copy_to": "all"
      },
      "location":{
        "type": "geo_point"
      },
      "pic":{
        "type": "keyword",
        "index": false
      },
      "all":{
        "type": "text",
        "analyzer": "text_anlyzer",
        "search_analyzer": "ik_smart"
      },
      "suggestion":{
          "type": "completion",
          "analyzer": "completion_analyzer"
      }
    }
  }
}

添加POJO类字段

 // 自动补全功能
    private List<String> suggestion;

if (this.business.contains("/")) {
            String[] arr = this.business.split("/");
            this.suggestion = new ArrayList<>();
            this.suggestion.add(this.brand);
            Collections.addAll(this.suggestion, arr);

        }else {
            this.suggestion = Arrays.asList(this.brand, this.business);
        }

后台处理请求的代码

    @Override
    public List<String> suggestion(String key) {
        try {
            // 1.准备Request
            SearchRequest request = new SearchRequest("hotel");
            // 2.准备DSL
            request.source().suggest(new SuggestBuilder().addSuggestion(
                    "suggestions",
                    SuggestBuilders.completionSuggestion("suggestion")
                            .prefix(key)
                            .skipDuplicates(true)
                            .size(10)
            ));
            // 3.发起请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            // 4.解析结果
            Suggest suggest = response.getSuggest();
            // 4.1.根据补全查询名称,获取补全结果
            CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
            // 4.2.获取options
            List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
            // 4.3.遍历
            List<String> list = new ArrayList<>(options.size());
            for (CompletionSuggestion.Entry.Option option : options) {
                String text = option.getText().toString();
                list.add(text);
            }
            return list;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

数据同步问题

  • 我们的ES数据都是从MYSQL来的,那么一旦MYSQL的数据发生了增删改,就需要同步到ES中

  • 这个时候可以考虑使用消息中间件

在MYSQL发生数据的增删改时,发送一个消息(采用Topic模型)

    @PostMapping
    public void saveHotel(@RequestBody Hotel hotel) {
        hotelService.save(hotel);
        // 通知更新ES
        rabbitTemplate.convertAndSend("ElasticSearchExChange", "insertOrUpdate.es",  hotel.getId());
    }

    @PutMapping()
    public void updateById(@RequestBody Hotel hotel) {
        if (hotel.getId() == null) {
            throw new InvalidParameterException("id不能为空");
        }
        hotelService.updateById(hotel);
        // 通知更新ES
        rabbitTemplate.convertAndSend("ElasticSearchExChange", "insertOrUpdate.es",  hotel.getId());
    }

    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable("id") Long id) {
        hotelService.removeById(id);
        // 通知更新ES
        rabbitTemplate.convertAndSend("ElasticSearchExChange", "delete.es",  id);
    }

同步ES数据,监听

@Component
public class EsSyncListener {
    @Autowired
    IHotelService iHotelService;

    @Autowired
    RestHighLevelClient client;

    /**
     * 由于ES的增量更新,如果没有id,则为新建,因此一个足矣
     *
     * @param id
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "insertESQueue"),
            exchange = @Exchange(value = "ElasticSearchExChange",type = ExchangeTypes.TOPIC),
            key = "insertOrUpdate.es"
    ))
    public void insertOrUpdateEs(Long id) {
        try {
            Hotel hotel = iHotelService.getById(id);
            HotelDoc hotelDoc = new HotelDoc(hotel);

            IndexRequest indexRequest = new IndexRequest("hotel");
            indexRequest.source(JSON.toJSONString(hotelDoc), XContentType.JSON);

            client.index(indexRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 用于同步删除ES中的数据
     *
     * @param id
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "deleteESQueue"),
            exchange = @Exchange(value = "ElasticSearchExChange",type = ExchangeTypes.TOPIC),
            key = "delete.es"
    ))
    public void deleteEs(Long id) {
        try {
            DeleteRequest request = new DeleteRequest("hotel", String.valueOf(id));
            client.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

感兴趣的话也可以定义个常量类


    public class MqConstants {
    /**
     * 交换机
     */
    public final static String HOTEL_EXCHANGE = "hotel.topic";
    /**
     * 监听新增和修改的队列
     */
    public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
    /**
     * 监听删除的队列
     */
    public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
    /**
     * 新增或修改的RoutingKey
     */
    public final static String HOTEL_INSERT_KEY = "hotel.insert";
    /**
     * 删除的RoutingKey
     */
    public final static String HOTEL_DELETE_KEY = "hotel.delete";
}

业务处理实战

封装查询条件

封装多个查询结果Bool

处理全文检索

处理关键字过滤

处理价格范围

设置经纬度范围排序

设置分页参数

设置排序

设置算分查询

封装处理高亮结果

处理响应结果

处理高亮字段

封装地理位置的距离

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
    @Autowired
    private RestHighLevelClient client;

    @Override
    public PageResult search(RequestParams requestParams) {
        try {
            // 1、获取request
            SearchRequest searchRequest = new SearchRequest("hotel");

            // 2、封装DSL、封装查询条件
            handlerBasicQuery(searchRequest, requestParams);

            // 3、发送请求
            SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

            // 4、处理结果 并 返回
            return handlerResponse(response);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 封装请求条件
     * @param searchRequest
     * @param requestParams
     */
    private void handlerBasicQuery(SearchRequest searchRequest, RequestParams requestParams) {
        String key = requestParams.getKey();
        String brand = requestParams.getBrand();
        String city = requestParams.getCity();
        Integer minPrice = requestParams.getMinPrice();
        Integer maxPrice = requestParams.getMaxPrice();
        String starName = requestParams.getStarName();
        Integer page = requestParams.getPage();
        Integer size = requestParams.getSize();
        String sortBy = requestParams.getSortBy();
        String location = requestParams.getLocation();

        // 1、构建一个Bool查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 2、处理全文检索
        if (StringUtils.isNotBlank(key)) {
            boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));
        }else {
            // 没有关键字则查询所有
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());
        }
        // 3、处理品牌brand关键字过滤
        if(StringUtils.isNotBlank(brand)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("brand", brand));
        }
        // 4、处理城市city关键字过滤
        if(StringUtils.isNotBlank(city)){
            boolQueryBuilder.filter(QueryBuilders.termQuery("city",city));
        }
        // 5、处理价格范围
        if(minPrice != null && maxPrice != null) {
            boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
        }
        // 6、处理星级酒店过滤
        if (StringUtils.isNotBlank(starName)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("starName", starName));
        }

        // 7、设置经纬度过滤
        if (StringUtils.isNotBlank(location)) {
            searchRequest
                    .source()
                    .sort(SortBuilders
                            .geoDistanceSort("location",new GeoPoint(location))
                            .unit(DistanceUnit.KILOMETERS)
                            .order(SortOrder.ASC));
        }
        // 8、设置分页参数
        if (page != null || page > 0) {
            // 设置分页参数
            searchRequest.source().from((page - 1) * size).size(size);
        }

        // 9、设置排序
        if (StringUtils.isNotBlank(sortBy) && !"default".equals(sortBy)) {
            searchRequest.source().sort(sortBy);
        }

        // 10、算分查询,为投了广告的人搞一搞、将查询条件交给searchRequest
        searchRequest.source().query(
                QueryBuilders.functionScoreQuery(
                        // 10.1 算分查询的基础查询条件
                        boolQueryBuilder,
                        // 10.2 判断字段的isAd是否为true,如果为true,则为其 分数 * 10倍
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                            new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                    QueryBuilders.termQuery("isAd", true),
                                    ScoreFunctionBuilders.weightFactorFunction(10)
                            )
                }).boostMode(CombineFunction.MULTIPLY)
        );
    }


    private PageResult handlerResponse(SearchResponse response) {
        SearchHits hits = response.getHits();

        // 获取总条目数
        long total = hits.getTotalHits().value;

        // 封装数据
        SearchHit[] hitList = hits.getHits();
        List<HotelDoc> list = new ArrayList<>();
        for (SearchHit hit : hitList) {
            // 获取到每一条记录
            String record = hit.getSourceAsString();
            // 封装成HotelDoc对象
            HotelDoc hotelDoc = JSON.parseObject(record, HotelDoc.class);
            // 处理高亮字段
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            // 1、健壮性判断,判断Map集合是否为空
            if (!CollectionUtils.isEmpty(highlightFields)) {
                // 2、获取到高亮字段结果
                HighlightField nameFiled = highlightFields.get("name");
                // 3、判断结果是否为空
                if (ObjectUtils.isNotEmpty(nameFiled)) {
                    // 获取到高亮的具体结果:Text
                    Text[] fragments = nameFiled.getFragments();
                    // 由于只需要获取到数组中第一个元素赋值,因此判断下
                    if (fragments.length > 0) {
                        hotelDoc.setName(fragments[0].string());
                    }
                }
            }
            // 处理距离当前多远
            Object[] sortValues = hit.getSortValues();
            if(sortValues != null && sortValues.length > 0) {
                hotelDoc.setDistance(sortValues[0]);
            }

            // 存储
            list.add(hotelDoc);
        }

        // 返回结果
        return new PageResult(list, total);
    }
}

posted @ 2022-09-20 15:45  CodeStars  阅读(145)  评论(0编辑  收藏  举报