微服务

微服务就是一种经过良好架构设计的分布式架构方案,微服务架构的特征:

  • 单一职责:微服务拆分力度更大,每一个服务都对应唯一的业务能力,做到单一职责,避免重复的业务开发
  • 面向服务:微服务只对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

第一天

一、认识微服务

1. 几种技术的比较

Dubbo SpringCloud SpringCloudAlibaba
注册中心 zookeeper、redis Eureka、Consul Nacos、Eureka
服务远程调用 Dubbo协议 Feign(http协议) Dubbo、Feign
配置中心 SpringCloudConfig SpringCloudConfig、Nacos
服务网关 SpringCloudGateway、Zuul SpringCloudGateway、Zuul
服务监控和保护 dubbo-admin,功能弱 Hystrix Sentinel

2. SpringCloud

  • SpringCloud是目前国内使用最广泛的微服务框架

    • 官网地址:https://spring.io/projects/spring-cloud
    • 版本对应信息查询地址:https://start.spring.io/actuator/info
  • SpringCloud集成了各种微服务功能组件,并给予SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验

    • 微服务
  • SpringCloud与SpringBoot的版本兼容情况如下:

    image-20240108102144858
    SpringCloud SpringBoot
    2023.0.x aka Leyton 3.2.x
    2022.0.x aka Kilburn 3.0.x,3.1.x(Starting with 2022.0.3)
    2021.0.x aka Jubilee 2.6.x,2.7.x(Starting with 2021.0.3)
    2020.0.x aka Ilford 2.4.x,2.5.x(Starting with 2020.0.3)
    Hoxton 2.2.x,2.3.x(Starting with SR5)
    Greenwich 2.1.x
    Finchley 2.0.x
    Edgware 1.5.x
    Dalston 1.5.x

3. 服务拆分及远程调用

3.1 服务拆分注意事项

  1. 微服务要根据业务模块拆分,做到单一职责,不同的微服务中不要重复开发相同的业务
  2. 微服务可以将业务暴露为接口,供其他微服务使用
  3. 微服务数据独立,不要访问其他微服务的数据库

3.2 微服务的远程调用

  • 基于RestTemplate发起的http请求实现远程调用

4. 提供者与消费者

  • 服务提供者:在一次业务中,被其他微服务调用的服务
  • 服务消费者:在一次业务中,调用其他微服务的服务

二、Eureka注册中心

Eureka的作用:将所有微服务集中注册,微服务请求时向注册中心发送消息即可获取服务列表

  • 消费者如何获取服务提供者的具体信息?
    • 服务提供者启动时会向EurekaServer注册自己的信息
    • 消费者根据服务名称向EurekaServer拉取提供者信息
  • 如果有多个服务提供者,消费者该如何选择?
    • 服务消费者利用负载均衡算法,从服务列表中挑选一个
  • 消费者如何感知服务提供者的健康状态?
    • 服务提供者会每隔30秒向EurekaServer注册中心发送心跳请求,报告健康状态
    • EurekaServer会根据健康状态更新服务列表,消费者就可以获取到最新的服务信息

1. 搭建EurekaServer

搭建EurekaServer服务步骤如下:

  1. 创建项目,引入spring-cloud-starter-netflix-eureka-server依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    
  2. 编写启动类,添加@EnableEurekaServer注解

  3. 添加application.yml文件,书写配置内容如下:

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

2. 注册微服务

步骤如下:

  1. user-service项目中引入spring-cloud-starter-netflix-eureka-client的依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  2. application.yml文件添加如下配置:

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

3. 服务发现

在order-service中完成服务发现

  1. 修改OrderService的代码,修改访问的url路径,用服务名代替IP和端口号

    String url = "http://uservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url,User.class);
    
  2. order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解@LoadBalanced

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
      return new RestTemplate();
    }
    

三、Ribbon负载均衡

1. 负载均衡的流程

  • order-service发起请求http://userservice/user/1
  • LoadBalancerInterceptor负载均衡拦截器拦截请求,交给RibbonLoadBanlancerClient
  • RibbonLoadBanlancerClient获得服务名称交给DynamicServerListLoadBalancer
  • DynamicServerListLoadBalancerIRule中选取负载均衡规则去决定选择使用的服务并将服务地址返还给RibbonLoadBalancerClient
  • RibbonLoadBalancerClient将带有服务名称的url修改为指定服务真实地址的url并发送请求

2. IRule负载均衡的策略

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

3. 修改负载均衡规则的方式

3.1 代码方式

  • 在order-service中的Order Application类中,定义一个新的IRule:

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

3.2 配置文件方式

  • 在order-service的application.yml文件中,添加新的配置也可以修改规则:

    userservice:
    	ribbon:
    		NFLoadBanlancerRuleClassName: com.netflix.loadbalancer.RandomRule
    

4. 饥饿加载

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

ribbon
	eager-load:
		enabled: true
		clients: 
			- userservice

四、Nacos注册中心

GitHub: https://github.com/alibaba/nacos

版本对应说明:地址

前往Github下载安装包,解压到指定位置即可,使用以下命令启动:

startup.cmd -m standalone
启动成功

1. Nacos搭建

步骤如下:

创建项目,引入spring-cloud-alibaba的依赖

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

2. 注册微服务

  1. 对应微服务引入spring-cloud-starter-alibaba-nacos-discovery

com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ```
  1. 修改该服务的application.yml文件,配置如下:

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

3. Nacos服务多级存储模型

一级是服务,二级是集群,三级是实例

3.1 服务集群属性

  1. 修改提供者application.yml,添加配置如下:

    spring:
    	cloud:
    		nacos:
    			server-addr: localhost:8848 #配置nacos服务端地址
    			discovery:
    				cluster-name: HZ #配置集群名称
    
  2. 修改消费者application.yml,添加配置如下:

    servicename:
    	ribbon:
    		NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
    

    NacosRule负载均衡策略

    1. 优先选择同集群服务实例列表
    2. 本地集群找不到提供者,才去其他集群寻找,并且会警告
    3. 确定了可用实例列表后,再采用随机负载均衡挑选实例

注:从spring-cloud-alibaba2020.*版本开始,已经逐渐弃用netflix相关组件,例如ribbon,要想继续使用,应该回退版本到spring-cloud-alibaba2.*,还要注意对应的spring-bootspring-cloud版本的兼容性

版本说明:Github版本说明

  1. 实例的权重控制
    • Nacos控制台可以设置实例的权重值,0-1之间
    • 统计群内的多个实例,权重越高被访问的频率越高
    • 权重设置为0则完全不会被访问

3.2 环境隔离 namespace

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离,未设置namespace的情况下默认为public

image-20220703081115288

实例

  1. 在Nacos控制台创建命名空间dev,并复制namespace的UUID

    image-20220703081956710
  2. 在注册服务的application.yml中添加如下配置文件,即可将该服务分配到此命名空间下,此时若其他服务与它不属于同一namespace下,将无法访问此服务

    namespace: 8f6f25b2-4758-40f7-8445-6c36ae9fb962

spring:
	cloud:
		nacos:
			server-addr: localhost:8848  
			discovery:
				cluster-name: 西安
				namespace: 8f6f25b2-4758-40f7-8445-6c36ae9fb962 #dev环境

Nacos环境隔离

  • namespace用来做环境隔离
  • 每个namespace都有唯一id,由Nacos控制台生成UUID
  • 不同namespace下服务不可见

可用来分别控制开发环境,测试环境,生产环境下的服务之间不可随意调用

4. Nacos和Eureka的区别

image-20220703083516074

4.1 临时实例和非临时实例

服务注册时,在启动配置文件中添加以下配置来设置当前实例的类别

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

ephemeral: 英 [ɪˈfemərəl] 美 [ɪˈfemərəl]

  • adj. 短暂的;(主指植物)短生的,短命的
  • n. 只生存一天的事物;短生植物

4.2 区别与联系

  1. 共同点
    • 都支持服务注册和服务拉取
    • 都支持服务提供者心跳方式做健康检测
  2. 区别
    • Nacos支持服务端主动检测提供者状态
      • 临时实例采用心跳模式
      • 非临时实例采用主动监测模式
    • 在Nacos中,临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    • Nacos支持服务列表变更的消息推送模式,服务列表更新及时
    • Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

第二天

五、 Nacos配置管理

image-20220703085636245

1. 统一配置管理

image-20220703090539366

因为bootstrap.yml的优先级高于application.yml,所以我们可以通过将Nacos服务信息配置到bootstrap.yml中来读取Nacos管理的配置文件,然后与本地配置文件进行合并,然后创建Spring容器,加载Bean启动程序

1.1 在指定服务的pom中引入Nacos配置管理客户端依赖

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

1.2 在指定服务的resources目录下添加一个bootstrap.yml文件,这是一个引导文件,优先级高于application.yml

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

2. 配置热更新

2.1 方式一

通过@Value注解注入,结合@RefreshScope来刷新

2.2 方式二

通过@ConfigurationProperties注解创建对应配置类注入,自动刷新

2.3 注意事项

  • 不是所有的配置都适合放到配置中心,维护起来比较麻烦
  • 建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置

3. 多环境配置共享

微服务启动时会从Nacos中读取多个配置文件:

  • [spring.application.name]-[spring.profiles.active].yaml
  • [spring.application.name].yaml

我们发现无论是开发环境,测试环境,生产环境,[spring.application.name].yaml一定会被加载,因此我们可以将多环境共享的配置内容写入这个文件

3.1 注意事项

当一个环境共享配置文件的值在本地配置文件中也存在时,以环境共享配置文件的值为主

多种配置的优先级:

  • [spring.application.name]-[spring.profiles.active].yaml > [spring.application.name].yaml > application.yaml

4. 搭建Nacos集群

https://nacos.io/img/deployDnsVipMode.jpg

4.1 创建集群数据库

create database if not exists nacos_config default character set utf8mb4 collate utf8mb4_general_ci;

4.2 创建集群数据表

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,
  `encrypted_data_key` text NOT NULL COMMENT '秘钥',
  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';
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='增加租户字段';
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 '租户字段',
  `encrypted_data_key` text NOT NULL 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';
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';
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';
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容量信息表';
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 '租户字段',
  `encrypted_data_key` text NOT NULL 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='多租户改造';
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
);

4.3 向表中插入数据

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

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

4.4 配置Nacos

1. 打开 \Nacos安装目录\conf\
  • 找到cluster.conf.example文件,将后缀.example去掉,编辑cluster.conf文件,配置多个Nacos服务的地址及端口

    127.0.0.1:8845
    127.0.0.1:8846
    127.0.0.1:8847
    
    
  • 找到application.properties文件,编辑文件

    • 将第33行取消注释,表示我们所使用的数据库为mysql
    #*************** Config Module Related Configurations ***************#
    ### If use MySQL as datasource:
    spring.datasource.platform=mysql
    
    • 将36,39,40,41行取消注释,并修改为我们刚才建立的集群数据库信息
    ### Count of DB:
    db.num=1
    
    ### Connect URL of DB:
    db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user.0=root
    db.password.0=root
    
2. 复制Nacos文件夹并分别修改为Nacos1,Nacos2,Nacos3
  • 编辑application.properties文件,修改对应端口

    • nacos1

      server.port = 8840
      
    • nacos2

      server.port = 8845
      
    • nacos3

      server.port = 8847
      
  • 分别启动三个nacos节点 startup.cmd

4.5 Nginx反向代理

upstream nacos-cluster {
		server 192.168.1.7:8840;
		server 192.168.1.7:8845;
		server 192.168.1.7:8847;
	}
	server {
		listen		80;
		server_name	localhost;
		
		location /nacos {
			proxy_pass http://nacos-cluster;
		}
	}

如果访问localhost/nacos提示404,那么就是80端口号被占用,请更改端口号重启nginx

image-20220703115917500

六、 基于Feign的远程调用

MindMap overview

1. 添加依赖

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

2. 代码书写

  • 在服务启动类上添加@EnableFeignClients注解

    @SpringBootApplication
    @EnableFeignClients
    public class OrderServiceApplication {
    
      public static void main(String[] args) {
          SpringApplication.run(OrderServiceApplication.class, args);
      }
    }
    
  • 创建远程调用服务接口,格式如下

    @FeignClient("user-service")
    public interface UserClients {
    
        @GetMapping("/user/info/{id}")
        Response getUserInfoById(@PathVariable("id") Long id);
    }
    
  • Feign调用远程服务接口实例

    public Response getOrderInfoById(Long id){
      Response response = new Response();
      OrderInfo orderInfo = orderMapper.selectById(id);
      if(orderInfo!=null){
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(orderInfo,orderVO);
        orderVO.setPrice(Double.valueOf((orderInfo.getPrice()/100)));
        Response userInfoById = userClients.getUserInfoById(orderInfo.getUserId());
        orderVO.setUserInfo(userInfoById.getData());
        response.code(ResponseCode.SUCCESS).message("获取成功").count(1).data(orderVO);
      }
      return response;
    }
    
  • 返回结果

    {
    "code": 200,
    "message": "获取成功",
    "count": 1,
    "data": {
    "id": "1543151273902505986",
    "price": 3902.0,
    "name": "OculusQuest2",
    "num": 2,
    "userId": "1543062156246155265",
    "userInfo": {
    "id": "1543062156246155265",
    "username": "zhiyuan121",
    "email": "5168154488@gmail.com",
    "introduction": "个人简介",
    "phoneNumber": "97938192868",
    "nickname": "絷缘",
    "status": "正常",
    "registerTime": "2022-07-02"
    },
    "createTime": "2022-07-02"
    }
    }

3. Feign的自定义配置

类型 作用 说明
feign.Logger.Level 日志级别 包含四种不同级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果解析器 远程调用返回结果解析
feign.codec.Encoder 请求参数编码 将请求参数编码
feign.Contract 支持的注解格式 默认SpringMVC注解
feign.Retryer 失败重试机制 请求失败重试,默认没有,但是会使用Ribbon重试

3.1 日志配置方式

  1. 配置文件方式

    • 全局生效

      feign:
      	client:
      		config:	
      			default: #default表示全局配置
      				loggerLevel: FULL #日志级别
      
    • 局部生效

      feign:
      	client:
      		config:
      			user-service: #对应服务名表示局部配置
      				loggerLevel: FULL #日志级别
      
  2. Java代码方式

    • 声明一个Bean

      public class FeignClientConfiguration{
      	@Bean
        public Logger.Level feignLogLevel(){
          return Logger.Level.BASIC;
        }
      }
      
    • 全局生效:使用加在启动类上的@EnableFeignClients

      @EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
      
    • 局部生效:使用加在指定服务上的@FeignClient

      @FeignClient(value = "user-service",configuration = FeignClientsConfiguration.class)
      

3. Feign性能优化

3.1 Feign底层客户端的实现

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

因此优化Feign的性能主要包括:

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

3.2 为Feign添加HttpClient支持

  1. 引入依赖

    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-httpclient</artifactId>
    </dependency>
    
  2. 配置连接池

    feign:
    	client:
    		config:
    			default:
    				loggerLevel: BASIC
    	httpclient:
    		enabled: true #开启httpclient支持
    		max-connections: 200 #最大连接数
    		max-connections-per-route: 50 #每个路径最大连接数
    

4. Feign的最佳实践

4.1 方式一(继承)

给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准

4.2 方式二(抽取)

将FeignClient抽取为独立模块,并且把接口有关的Entity、默认的Feign配置都放到这个模块中,提供给所有消费者使用

image-20220703214647319

步骤:

  1. 首先创建一个module,命名为feign-api,然后引入feign依赖
  2. 将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
  3. 在order-service中引入feign-api依赖
  4. 修改order-service中所有与上述三个组件有关的import部分,改成导入feign-api中的包

当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用,解决方式有两种:

  1. 指定FeignClient所在包

    @EnableFeignClients(basePackages = "com.zhiyuan.feign.clients")

  2. 指定FeignClient字节码

    @EnableFeignClients(clients = {UserClient.class})

抽取为feign-api后会导致日志配置失效,通过添加 logging.level.com.zhiyuan.clients:DEBUG 可以使日志配置生效

七、统一网关Gateway

1. 网关的功能

1.1 身份认证和权限校验

1.2 服务路由、负载均衡

1.3 请求限流

2. SpringCloud中网关的实现

2.1 Gateway

2.2 zuul

Zuul是基于Servlet实现的,属于阻塞式编程,而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能

3. 搭建网关

3.1 创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖

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

3.2 编写路由配置及nacos地址

server:
	port: 10010
spring:
	application:
		name: gateway
	cloud:
		nacos:
			server-addr: localhost:8848
		gateway:
			routes: #网关路由配置
				- id: user-service #路由ID,自定义唯一即可
					uri: lb://user-service # 路由目标地址 
					predicates: #路由断言,即用来判断请求是否符合路由规则的配置
						- Path=/user/** 

3.3 路由断言工厂Route Predicate Factory

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件

org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory

名称 说明 示例
After 某个时间点后的请求 - After=2022-06-20T17:00:00.369+8:00[Asia/Shanghai]
Before 某个时间点前的请求 - Before2022-06-20T17:00:00.369+8:00[Asia/Shanghai]
Between 某两个时间点之间的请求 - Between=2022-06-20T17:00:00.369+8:00[Asia/Shanghai],2022-07-01T17:00:00.369+8:00[Asia/Shanghai]
Cookie 请求必须包含某些Cookie - Cookie=
Header 请求必须包含某些Header - Header=
Host 请求必须访问某个host - Host=**
Method 请求必须以指定方式发起 - Method=GET,POST
Path 请求路径必须符合指定规则 - Path=
Query 请求参数必须包含指定参数 - Query=
RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr=192.168.1.1/24
Weight 权重处理

3.4 路由过滤器GatewayFilter

  • 局部过滤器
    过滤器工厂 作用 参数
    AddRequestHeader 给原始请求添加Header Header名称,Header值
    AddRequestParameter 给原始请求添加请求参数 参数名称,参数值
    AddResponseHeader 给原始响应添加Header Header名称,Header值
    DedupeResponseHeader 剔除响应头中的重复值 Header名称,去重策略
    Hystrix 引入Hystrix的断路器保护 HystrixCommand名称
    FallbackHeaders 给FallbackURL请求头中添加具体的异常信息 Header名称
    PrefixPath 给原始请求路径添加前缀 前缀路径
    PreserveHostHeader 给请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性来决定是否要发送原始的Host
    RequestRateLimiter 对请求限流,限流算法为令牌桶 keyResolver、rateLimiter、status Code、denyEmptyKey、emptyKeyStatus
    RedirectTo 将原始请求重定向到指定URL HTTP状态码,重定向URL
    RemoveHopByHopHeadersFilter 将原始请求中IETF组织规定的Header信息全部移除 默认启用,可以通过配置指定删除的Header
    RemoveRequestHeader 将原始请求中的某个Header移除 Header名称
    RemoveResponseHeader 将原始响应中的某个Header移除 Header名称
    RewritePath 重写原始请求路径 原始路径正则表达式,重写后路径的正则表达式
    RewriteResponseHeader 重写原始响应中的某个Header Header名称,值的正则表达式,重写后的值
    SaveSession 在转发请求之前强制执行WebSession::save操作
    SecureHeaders 给原始响应添加有安全作用的Header
    SetPath 修改原始请求路径 修改后的路径
    SetResponseHeader 修改原始响应中某个Header的值 Header名称,修改后的值
    SetStatus 修改原始响应中的状态码 HTTP状态码,字符串或数字均可
    StripPrefix 截断原始请求路径 使用数字表示要截断路径的数量
    Retry 针对不同的响应进行重试 retries、statuses、methods、series
    RequestSize 设置允许接收的最大请求包,如果请求大小超过设置的值,返回413 Payload Too Large 请求包大小,单位Byte,默认5M
    ModifyRequestBody 在转发请求之前修改原始请求体内容 修改后的请求体内容
    ModifyResponseBody 修改原始响应体的内容 修改后的响应体内容
  • 全局过滤器
img
  • 使用示例

    • 局部过滤器
    #局部过滤器,只对定义该过滤器的服务有效
    server:
      port: 10010
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          server-addr: localhost:8848 #nacos服务端地址
          discovery:
            cluster-name: 西安
        gateway:
          routes:
            - id: user-service
              uri: lb://user-service
              predicates:
                - Path=/user/**
              filters:
              	- AddRequestHeader=Author,ZhiYuanXie
            - id: order-service
              uri: lb://order-service
              predicates:
                - Path=/order/**
    
    #默认过滤器,对配置的所有的服务有效
    server:
      port: 10010
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          server-addr: localhost:8848 #nacos服务端地址
          discovery:
            cluster-name: 西安
        gateway:
          routes:
            - id: user-service
              uri: lb://user-service
              predicates:
                - Path=/user/**
            - id: order-service
              uri: lb://order-service
              predicates:
                - Path=/order/**
         default-filters:
            - AddRequestHeader=Author,ZhiYuanXie
    
    • 全局过滤器
    @Order(-1)
    @Component
    public class AuthorizeFilter implements GlobalFilter,Ordered {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            //获取请求参数
            MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
            //获取响应对象
            ServerHttpResponse response = exchange.getResponse();
            //获取authorization参数  
            String auth = queryParams.getFirst("authorization");
            if ("admin".equals(auth)){
                //放行
                return chain.filter(exchange);
            }
            //拦截请求,响应对象设置HTTP状态码
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        
        //设置过滤器优先级
        public int getOrder(){
            return -1;
        }
    }
    

    @Order注解

    路由过滤器、默认过滤器、全局过滤器的执行顺序

    image-20220704132511162
    • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高
    • 全局过滤器GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己实现
    • 路由过滤器Filters和默认过滤器defaultFilter由Spring指定,默认是按声明顺序从1开始递增
    • 当过滤器的order值都一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter 的顺序执行

    参考以下方法查看优先级:

    image-20220704133728495

3.5 网关的跨域请求配置

跨域:域名不一致就是跨域,主要包括:

  • 域名不同
  • 域名相同,端口不同

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

解决方案:CORS

Gateway的跨域只需要简单配置即可实现

server:
  port: 10010
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos服务端地址
      discovery: 
        cluster-name: 西安
    gateway:
      globalcors: #全局跨域处理
        add-to-simple-url-handler-mapping: true #解决options请求被拦截的问题
        cors-configurations:
          '[/**]':
            allowedOrigins: #跨域允许的网址
              - "http://localhost:8080/"
            allowedMethods: #跨域允许的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" #跨域是否允许携带Header信息
            allowedCredentials: true #跨域是否允许携带Cookie信息
            maxAge: 360000 #跨域检测有效期
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
      default-filters:
        - AddRequestHeader=Author,ZhiYuanXie

第三天

八、容器化部署Docker

Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?

  • Docker允许开发汇总将应用、依赖、函数库、配置一起打包,形成可以直接安装的镜像
  • Docker应用运行在容器中,使用沙箱机制,相互隔离

Docker如何解决开发、测试、生产环境有差异的问题?

Docker镜像中包含完整的运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行

Docker与虚拟机的区别:

虚拟机是使用Hypervisor技术在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在Windows系统里面运行Ubuntu系统。

特性 Docker 虚拟机
性能 接近原生 性能较差
硬盘占用 一般为MB 一般为GB
启动 秒级 分钟级

1. 概念

1.1 镜像(Image)

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

1.2 容器(Container)

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

1.3 DockerHub

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

1.4 Docker

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

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

2. 安装Docker

2.1 CentOS7安装Docker

CentOS7系统下载地址:

http://mirrors.163.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso

迅雷下载镜像文件有奇效,IDM平时下载很快,下载镜像开8线程只能跑到1M/s,迅雷直接10M/s

  • 若之前安装过其他版本Docker,通过以下方式完成卸载:
    yum remove docker \
                    docker-client \
                    docker-client-latest \
                    docker-common \
                    docker-lastest-logrotate \
                    docker-logrotate \
                    docker-selinux \
                    docker-engine-selinux \
                    docker-engine \
                    docker-ce
    
  • 安装Docker
    yum install -y yum-utils device-mapper-persistent-data lvm2
    
    • 更新yum本地镜像源

      # 删除原有配置源目录
      cd /etc
      rm -rf yum.repos.d
      # 新建配置源目录
      mkdir yum.repos.d
      # 下载阿里镜像源
      cd yum.repos.d
      wget https://mirrors.aliyun.com/repo/Centos-7.repo
      # 更新yum缓存
      yum makecache
      
    • 更新软件源信息

      参考:https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.40491b11PUGxwo

      # step 1: 安装必要的一些系统工具
      yum install -y yum-utils device-mapper-persistent-data lvm2
      # Step 2: 添加软件源信息
      yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
      # Step 3
      # Step 4: 更新并安装Docker-CE
      yum makecache fast
      yum -y install docker-ce
      # Step 4: 开启Docker服务
      service docker start
      
    • 关闭防火墙(为了学习Docker,开发中应该开启指定端口)

      # 关闭防火墙应用
      systemctl stop firewalld
      # 禁止开机启动防火墙
      systemctl disable firewalld
      # 查看防火墙状态
      systemctl status firewalld
      
    • 配置Docker镜像源

      mkdir -p /etc/docker
      tee /etc/docker/daemon.json <<-'EOF'
      {
        "registry-mirrors": ["https://xm9ypajm.mirror.aliyuncs.com"]
      }
      EOF
      systemctl daemon-reload
      systemctl restart docker
      

2.2 Docker基本操作

镜像的命名规范:

  • 镜像名称一般由两部分组成:[respository]:[tag]
    • respository:镜像名称
    • tag:版本
  • 如果没有指定tag时,默认是latest,代表最新版本的镜像
DOCKER
  • 镜像相关指令
    • docker images:查看当前所有镜像
    • docker pull [respository]:[tag]:从镜像仓库中拉取指定镜像
    • docker save -o [Path/FileName.tar] [respository]:[tag]:将指定镜像打包
    • docker load -i [Path/FileName.tar]:将打包好的镜像加载到Docker中
    • docker rmi [respository]:[tag]:移除指定镜像

示例:

  • 从DockerHub中拉取一个nginx镜像并查看

    docker pull nginx

    docker images

  • 将nginx镜像打包到本地

    docker save -o ~/nginx.tar nginx:latest

  • 将镜像文件从本地tar包加载

    docker load -i ~/nginx.tar

    docker images

  • 容器相关命令
    • docker run:创建并运行一个容器
    • docker pause:暂停运行
    • docker unpause:继续运行
    • docker stop:停止运行
    • docker start:运行
    • docker ps:查看所有运行容器及状态
    • docker logs:查看容器运行日志
    • docker exec:进入容器执行命令
    • docker rm:删除指定容器

示例:

  • 运行一个nginx容器

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

    • --name:指定容器名称
    • -p:指定端口映射
    • -d:后台运行

    docker ps

  • 查看指定容器运行日志

    docker logs mn

    跟踪查看运行日志:docker logs -f mn

  • 进入容器执行命令

    docker exec -it mv bash

    exit:退出容器

  • 停止运行容器

    docker stop mn

  • 查看所有容器包括未运行的

    docker ps -a

  • 运行容器

    docker start mn

  • 删除容器

    docker rm:只能删除未运行的容器

    docker rm -f mn:强制删除容器,无论是否运行

示例:运行一个持久化存储的redis容器,并通过redis-cli设置num=666

  • 运行容器

    docker run --name my-redis -p 6379:6379 -d redis redis-server --appendonly yes

  • 进入容器

    docker exec -it my-redis bash

  • 启动redis-cli

    redis-cli

  • 设置num=666

    set num 666

  • 退出redis-cli,退出容器

    exit

    docker exec -it my-redis redis-cli:直接进入容器中启动redis-cli

2.3 数据卷操作

数据卷的作用:将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全

image-20220705192332218
  • 基本语法docker volume [COMMAND]

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

    • 创建并运行容器时指定数据卷的挂载目录,若数据卷不存在,则自动创建数据卷

      docker run \
      --name mn \
      -p 80:80 \
      -v html:/usr/share/nginx/html \
      -d nginx
      
  • 挂载目录

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

    数据卷挂载与目录挂载

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

3. 镜像结构

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

  • 基础镜像(BaseImage):应用依赖的系统函数库、环境变量、配置、文件系统等
  • 入口(Entrypoint):镜像运行入口,一般是程序启动的脚本和参数
  • 层(Layer):在BaseImage基础上添加安装包、依赖、配置等,每次操作形成新的一层

镜像是分层结构,每一层称一个Layer

3.1 自定义镜像

Dockerfile:一个文本文件,指令的合集,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层Layer

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

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

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

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

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

  • docker-demo.jar jdk8.tar.gz Dockerfile上传至/tmp/docker-demo

  • docker build -t javaweb:1.0 .

  • docker images

  • docker run --name web -p 8090:8090 -d javaweb:1.0

  • 访问ip:8090/hello/count

我们发现在Dockerfile中构建jdk环境的操作是可复用的,我们应该把构建jdk环境的部分构建一个镜像,这样以后就可以直接使用了,而java:8-alpine帮我们做了这件事

3.2 DockerCompose

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

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

version: "3.9"
services:
	mysql: #指定服务名称
		image: mysql:8.0.21 # 指定镜像文件
		environment: #设置环境变量
			MYSQL_ROOT_PASSWORD: 123456
		volumes: #数据卷挂载
			- /tmp/mysql/data:/var/lib/mysql
			- /tmp/mysql/conf/hym.cnf:/etc/mysql/conf.d/hym.cnf
	web: #指定服务名称
		build: . #从当前目录中构建镜像
		ports:   #设置端口号
			- "8090:8090"

书写格式参考规范:

  • https://docs.docker.com/compose/compose-file/compose-file-v3/
  • https://docs.docker.com/compose/compose-file/compose-file-v2/
3.2.1 安装DockerCompose

参考:https://docs.docker.com/compose/install/compose-plugin/#installing-compose-on-linux-systems

 curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
  • docker-compose所在目录/usr/local/bin/docker-compose

  • docker-compose添加可执行权限chmod +x docker-compose

  • Base自动补全命令

    curl \
        -L https://raw.githubusercontent.com/docker/compose/v2.6.1/contrib/completion/bash/docker-compose \
        -o /etc/bash_completion.d/docker-compose
    

    如果无法访问该地址,则修改本机hosts文件

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

3.2.2 部署微服务集群
docker run \
--name my-mysql8 \
-e MYSQL_ROOT_PASSWORD=root \
-p 3306:3306 \
-d mysql:latest \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci

FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar
# docker-comspose配置文件语法版本
version: 3.8
services:
	nacos:
		images: nacos/nacos-server
		environment: 
			MODE: standalone
		ports:
			- "8848:8848"
	mysql:
		images: mysql:8.0.31
		environment: 
			MYSQL_ROOT_PASSWORD: 996748
		volumes:
			- "$PWD/mysql/data:/var/lib/mysql"
			- "$PWD/mysql/conf:/etc/mysql/conf.d"
	user-service: 
		build: ./user-service
	order-service:
		build: ./order-service
	gateway:
		build: ./gateway
		ports: 
			- "10010:10010"
		
image-20230505123711539

4. Docker镜像仓库

4.1 配置Docker信任地址

我们的私服采用的是http协议,默认不被Docker信任

# 编辑Docker服务守护进程配置文件
vi /etc/docker/daemon.json
# 添加内容
"insecure-registries":["http://192.168.96.130:8080"]
# 重新加载Docker服务守护进程
systemctl daemon-reload
# 重启Docker
systemctl restart docker  

4.2 使用Docker部署带有图形界面的DockerRegistry

version: '3.0'
services:
	registry:
		image: registry
		volumes:
			- ./registry-data:/var/lib/registry
	ui:
		image: joxit/docker-registry-ui:1.5-static
		ports:
			- 8080:80
		environment:
			- REGISTRY_TITLE=絷缘私有仓库
			- REGISTRY_URL=http://registry:5000
		depends_on:
			- registry
mkdir /tmp/docker-registry-ui
cd /tmp/docker-registry-ui
touch docker-compose.yml
vim docker-compose.yml
docker-compose up -d 

4.3 在私有镜像仓库推送/拉取镜像

# 将现有镜像打包成为私有镜像
docker tag nginx:latest 192.168.96.130:8080/nginx:latest
# 将私有镜像推送到私有仓库
docker push 192.168.96.130:8080/nginx:latest
# 将私有镜像拉取到当前环境
docker pull 192.168.96.130:8080/nginx:latest
image-20220706180422059

第四天

九、消息队列RabbitMQ

同步调用的问题

  • 微服务之间基于Feign的调用就属于同步方式,存在一些问题
    • 耦合度高,每次加入新的需求,就需要修改原来的代码
    • 阻塞调用,调用者需要等待提供者响应,调用链过长时等待时间相当于业务执行时间总和
    • 资源浪费,调用者在等待过程中,不会释放请求占用的资源
    • 级联失败:当调用链中有一个服务出现问题,name就会导致依赖于此服务的所有微服务发生故障

异步调用的方案

  • 实现方式是事件驱动模式
    • 优势
      • 服务解耦
      • 性能提升,吞吐量提高
      • 故障隔离,服务之间没有强依赖,不担心级联失败
      • 流量削峰
    • 缺点
      • 依赖于Broker的可靠性、安全性、吞吐能力
      • 架构复杂了,业务之间没有明显的流程线,不好追踪管理

1. MQ(MessageQueue)

消息队列,字面来看就是存放消息的队列,也就是事件驱动架构中的Broker

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

2. RabbitMQ快速入门

RabbitMQ是基于Erlang语言开发的开源消息通信中间件

官网地址:https://rabbitmq.com/

2.1 安装RabbitMQ

2.1.1 下载镜像
docker pull rabbitmq:3-management
2.1.2 安装MQ
docker run \
-e RABBITMQ_DEFAULT_USER=zhiyuan \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name my-mq \
--hostname mql \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management

若浏览器无法访问ip:15672,则按照下方操作即可:

docker exec -it my-mq bash
rabbitmq_plugins enable rabbitmq_management

2.2 概念解析

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

2.3 常见消息模型

2.3.1 基本消息队列(BasicQueue)
image-20230505173449633
2.3.2 工作消息队列(Work Queue)
  • 可以提高消息处理速度,避免队列消息堆积

    image-20230506114816183
2.3.3 发布订阅(Publish Subscribe)
  • 发布订阅模式允许将同一消息发送给多个消费者,实现方式就是加入交换机exchange
image-20230506121857146
  • Fanout Exchange:广播
  • Direct Exchange:路由
  • Topic Exchange:主题

3. SpringAMQP

image-20230505173640193

3.1 入门案例:消息的发送

  • 引入AMQP依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 在publisher服务中编写application.yml,添加mq连接信息:
spring:
	rabbitmq:
		host: 192.168.174.130
		port: 5672
		virtual-host: /
		username: zhiyuan
		password: 123456
  • 在publisher服务中新建一个测试类,编写测试方法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testSimpleQueue(){
        String queueName = "simple.queue";
        String message = "Hello,SpringAMQP";
        rabbitTemplate.covertAndSend(queueName,message);
    }
}

3.2 入门案例:消息的接收

  • 在consumer服务中编写application.yml,添加mq连接信息:
spring:
	rabbitmq:
		host: 192.168.174.130
		port: 5672
		virtual-host: /
		username: zhiyuan
		password: 123456
  • 在consumer服务中新建一个类,编写消费逻辑:
@Component
public class SpringRabbitListener{
    @RabbitListener(queues = {"simple.queue"})
    public void listenSimpleQueueMessage(String msg) throws InterruptedException{
        System.out.println("SpringConsumer接受到消息:【" + msg + "】");
    }
}

注意事项:消息一旦被消费就会从队列中移除,RabbitMQ没有消息回溯功能

3.3 工作队列案例

  • 基本思路

    • 在publisher服务中定义测试方法,每秒产生50条消息发送到simple.queue
    • 在consumer服务中定义两个消息监听者,都监听simple.queue
    • 消费者1每秒钟处理50条消息,消费者2每秒钟处理10条消息
  • @Test
    public void testSendMessageToWorkQueue() throws InterruptedException {
        String queueName = "simple.queue";
        String message = "Hello,SpringAMQP-MESSAGE_";
        for (int i = 0; i < 50; i++) {
            rabbitTemplate.convertAndSend(queueName,message + i);
            Thread.sleep(20);
        }
    }
    
  • @RabbitListener(queues = {"simple.queue"})
    public void listenWorkQueueMessage1(String msg) throws InterruptedException {
        System.out.println("SpringConsumer1接收到消息“【" + msg + "】" + LocalTime.now());
        Thread.sleep(20);
    }
    
  • @RabbitListener(queues = {"simple.queue"})
    public void listenWorkQueueMessage2(String msg) throws InterruptedException {
        System.err.println("SpringConsumer2接收到消息“【" + msg + "】" + LocalTime.now());
        Thread.sleep(200);
    }
    

    我们发现一个现象,消费者1很快处理完消息后就停止处理了,而把所有的消息都交由速度较慢的消费者2,这是由于预取消息导致的,我们可以通过修改application.yml文件来限制预取消息的上限

    spring:
      rabbitmq:
        host: 192.168.174.130
        port: 5672
        virtual-host: /
        username: zhiyuan
        password: 123456
        listener:
          simple:
            prefetch: 1 # 每次只能获取一条消息,处理完成才能发获取下一条消息
    

3.4 发布订阅案例

3.4.1 Fanout Exchange:将接受到的消息路由到每一个与其绑定的queue
image-20230506122404242
  • 实现思路

    • 在consumer服务中利用代码声明队列,交换机,并将二者绑定
    • 在consumer服务中编写两个消费者方法,分别监听fanout.queue1和fanoput.queue2
    • 在publisher服务中编写测试方法,向zhiyuan.fanout发送消息
  • @Configuration
    public class FanoutConfig {
        @Bean
        public FanoutExchange fanoutExchange(){
            return new FanoutExchange("zhiyuan.fanout");
        }
    
        @Bean
        public Queue fanoutQueue1(){
            return new Queue("fanout.queue1");
        }
    
        @Bean
        public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
            return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
        }
    
        @Bean
        public Queue fanoutQueue2(){
            return new Queue("fanout.queue2");
        }
    
        @Bean
        public Binding bindingQueue2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
            return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
        }
    
    }
    
  • @RabbitListener(queues = {"fanout.queue1"})
    public void listenFanoutQueue1(String msg) throws InterruptedException {
        System.err.println("SpringConsumer1接收到FanoutQueue1消息“【" + msg + "】" + LocalTime.now());
    }
    @RabbitListener(queues = {"fanout.queue2"})
    public void listenFanoutQueue2(String msg) throws InterruptedException {
        System.err.println("SpringConsumer2接收到FanoutQueue2消息“【" + msg + "】" + LocalTime.now());
    }
    
  • @Test
    public void testSendMessageToFanoutQueue() throws InterruptedException {
        String exchangeName = "zhiyuan.fanout";
        String message = "Hello,Fanout EveryOne!";
        rabbitTemplate.convertAndSend(exchangeName,"",message);
    }
    
3.4.2 Direct Exchange:将接受到的消息根据规则路由到指定的queue
image-20230506125801143
  • 实现思路

    • 每一个queue都与Exchange设置一个BindingKey
    • 发布者发送消息时,指定消息的RoutingKey
    • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
    1. 利用@RabbitListener声明Exchange、Queue、RoutingKey
    2. 在consumer服务中编写两个消费者方法,分别监听direct.queue1和direct.queue2
    3. 在publisher中编写测试方法,向zhiyuan.direct发送消息
  • @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1"),
        exchange = @Exchange(name = "zhiyuan.direct",type = ExchangeTypes.DIRECT),
        key = {"red","blue"}
    ))
    public void listenDirectQueue1(String msg){
        System.err.println("SpringConsumer接收到DirectQueue1消息“【" + msg + "】" + LocalTime.now());
    }
    
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue2"),
        exchange = @Exchange(name = "zhiyuan.direct",type = ExchangeTypes.DIRECT),
        key = {"red","yellow"}
    ))
    public void listenDirectQueue2(String msg){
        System.err.println("SpringConsumer接收到DirectQueue2消息“【" + msg + "】" + LocalTime.now());
    }
    
  • @Test
    public void testSendMessageToDirectQueue1() throws InterruptedException {
        String exchangeName = "zhiyuan.direct";
        String message = "Hello,Direct key is blue";
        rabbitTemplate.convertAndSend(exchangeName,"blue",message);
    }
    @Test
    public void testSendMessageToDirectQueue2() throws InterruptedException {
        String exchangeName = "zhiyuan.direct";
        String message = "Hello,Direct key is yellow";
        rabbitTemplate.convertAndSend(exchangeName,"yellow",message);
    }
    
    @Test
    public void testSendMessageToDirectQueue() throws InterruptedException {
        String exchangeName = "zhiyuan.direct";
        String message = "Hello,Direct key is red";
        rabbitTemplate.convertAndSend(exchangeName,"red",message);
    }
    
3.4.3 TopicExchange:与DirectExchange类似,区别在于routingKey必须是多个单词列表,并且以.分割
  • Queue和Exchange指定BindingKey时可以使用通配符
    • #:代指0个或多个单词
    • *:代指一个单词
image-20230506131941723
  • 实现思路

    • 利用@RabbitListener声明Exchange、Queue、RoutingKey
    • 在consumer服务中编写两个消费者方法,分别监听topic.queue1和topic.queue2
    • 在publisher中编写测试方法,向zhiyuan.topic发送消息
  • @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue1"),
        exchange = @Exchange(name = "zhiyuan.topic",type = ExchangeTypes.TOPIC),
        key = "china.#"
    ))
    public void listenTopicQueue1(String msg){
        System.err.println("SpringConsumer接收到TopicQueue1消息“【" + msg + "】" + LocalTime.now());
    }
    
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue2"),
        exchange = @Exchange(name = "zhiyuan.topic",type = ExchangeTypes.TOPIC),
        key = "#.news"
    ))
    public void listenTopicQueue2(String msg){
        System.err.println("SpringConsumer接收到TopicQueue2消息“【" + msg + "】" + LocalTime.now());
    }
    
  • @Test
    public void testSendMessageToTopicQueue() throws InterruptedException {
        String exchangeName = "zhiyuan.topic";
        String message = "Hello,Topic key is china.news";
        rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
    }
    

4.SpringAMQP消息转换器

在SpringAMQP的发送方法中,接收消息的类型时Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP最终会帮我们序列化为字节后发送,content-typeapplication/x-java-serialized-object ,默认直接以对象方式传输是很不安全的而且消息很长

  • SpringAMQP对消息对象的处理是由 org.springframework.amqp.support.converter.MessageConverter完成的

    • 默认实现:SimpleMessageConverter

      • 基于JDK的 ObjectOutputStream 完成序列化

      • 如果要修改只需要定义一个MessageConverter类型的Bean即可

        • 创建 object.queue 队列

          @Bean
          public Queue objectQueue(){
              return new Queue("object.queue");
          }
          
        • 引入依赖

          <dependency>
              <groupId>com.fasterxml.jackson.core</groupId>
              <artifactId>jackson-databind</artifactId>
          </dependency>
          <dependency>
              <groupId>com.fasterxml.jackson.dataformat</groupId>
              <artifactId>jackson-dataformat-xml</artifactId>
              <version>2.9.10</version>
          </dependency>
          
        • 在publisher、consumer服务声明MessageConverter

          @Bean
          public MessageConverter jsonMessageConverter(){
              return new Jackson2JsonMessageConverter();
          }
          
        • 测试发送HashMap类型消息

          @Test
          public void testSendObjectMessageToSimpleQueue(){
              String queueName = "object.queue";
              HashMap<String, String> map = new HashMap<>();
              map.put("name","zhiyuan");
              map.put("gender","male");
              rabbitTemplate.convertAndSend(queueName,map);
          }
          
        • 测试接收HashMap类型消息

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

十、分布式搜索Elasticsearch

1. 初识elasticsearch

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

  • elasticsearch结合kibana、Logstash、Beats,也就是说elastic stack(ELK)被广泛应用于日志数据分析、实时监控等领域
  • elasticsearch是elastic stack的核心,负责存储、搜索、分析数据
image-20230506142641009
  • elasticsearch的发展
    • Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发
      • 官网地址:https://lucene.apache.org
      • 优势:
        • 易扩展
        • 高性能(基于倒排索引)
      • 缺点:
        • 只限于java语言开发
        • 学习曲线陡峭
        • 不支持水平扩展
    • 相比于lucene,elasticsearch具有以下优势
      • 支持分布式,可水平扩展
      • 提供Restful接口,可被任何语言调用
    • 搜索引擎技术排名
      1. Elasticsearch:开源的分布式搜索引擎
      2. Splunk:商业项目收费
      3. Solr: Apache的开源搜索引擎

1.1. 正向索引和倒排索引

传统数据库采用正向索引

  • elaticsearch采用倒排索引
    • 文档(document):每条数据就是一个文档
    • 词条(term):文档按照语义分成的词语
image-20230506173552402
image-20230506173822385

1.2 文档

  • elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息
  • 文档数据会被序列化为json格式存储在elasticsearch中

1.3 索引

  • 索引(index):相同类型文档的集合
  • 映射(mapping):索引中文档的字段约束信息,类似表的结构约束
image-20230506175935509

1.4 概念对比

image-20230506183639661
  • MySQL擅长事务类型操作,可以确保数据的安全和一致性
  • Elasticsearch擅长海量数据的搜索、分析、计算
image-20230506184113064

2. 安装Elasticsearch

2.1 创建网络

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

docker network create es-net

2.2 加载镜像

docker pull elasticsearch:7.12.1

2.3 运行

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 "ES_JAVA_OPTS=-Xms512m -Xmx512m":设置JAVA程序运行内存
  • -e "discovery.type=single-node":设置es为单例模式
  • --privileged:授予数据卷访问权
  • --network es-net:加入名为es-net的网络

2.4 部署kibana

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
image-20230506190829754

3. 分词器

es在创建倒排索引时需要对文档分词,在搜索时,需要对用户输入内容分词,但默认的分词规则对中文处理并不友好

POST /_analyze
{
    "analyzer":"standard",
    "text":"神探狄仁杰"
}
  • 语法说明
    • POST:请求方式
    • /_analyze:请求路径,省略了http://IP:9200,kibana会帮我们补充
    • 请求参数:JSON风格
      • analyzer:分词器类型,这里默认时standard分词器
      • text:要分词的内容
  • elasticsearch 的分词器对中文支持不好,一般我们会采用IK分词器

    • 测试默认分词器效果

      • POST /_analyze
        { 
          "text":"神探狄仁杰", 
          "analyzer":"standard"
        }
        
      • {
          "tokens" : [
            {
              "token" : "神",
              "start_offset" : 0,
              "end_offset" : 1,
              "type" : "<IDEOGRAPHIC>",
              "position" : 0
            },
            {
              "token" : "探",
              "start_offset" : 1,
              "end_offset" : 2,
              "type" : "<IDEOGRAPHIC>",
              "position" : 1
            },
            {
              "token" : "狄",
              "start_offset" : 2,
              "end_offset" : 3,
              "type" : "<IDEOGRAPHIC>",
              "position" : 2
            },
            {
              "token" : "仁",
              "start_offset" : 3,
              "end_offset" : 4,
              "type" : "<IDEOGRAPHIC>",
              "position" : 3
            },
            {
              "token" : "杰",
              "start_offset" : 4,
              "end_offset" : 5,
              "type" : "<IDEOGRAPHIC>",
              "position" : 4
            }
          ]
        }
        
    • IK分词器:https://github.com/medcl/elasticsearch-analysis-ik

3.1 安装分词器

# 进入容器内部
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

在线安装可能会失败,解决办法离线安装

docker volume inspect es-plugins
cd /var/lib/docker/volume/es-plugins/_data
# 上传插件压缩包解压后目录
# 重启容器
docker restart es
# 查看es日志
docker logs -f es

3.2 IK分词器

IK分词器包含两种模式

  • ik_smart:最少切分
  • ik_max_word:最细切分

测试请求:

POST /_analyze
{
    "text":"神探狄仁杰",
    "analyzer":"ik_smart"
}

返回结果

{
  "tokens" : [
    {
      "token" : "神",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "探",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "狄仁杰",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 2
    }
  ]
}

3.3 IK分词器的拓展和停用字典

要拓展IK分词器的词库,只需要修改一个IK分词器目录中config目录下的IKAnalyzer.cfg.xml文件

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer扩展配置</comment>
    <!-- 用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">ext.dic</entry>
    <!-- 用户可以在这里配置自己的扩展停止词典 -->
    <entry key="ext_stopwords">stopword.dic</entry>
</properties>

在config目录下对应的字典文件中添加扩展词语,或创建自己的扩展字典加入扩展词语,词语以换行符分隔

4. 索引库操作

4.1 mapping属性

  • mapping是对索引库中文档的约束
    • type:字段数据类型,常见的简单类型有:
      • 字符串:text(可分词的文本)关键字:keyword(精确值)
      • 数值:long、integer、short、byte、double、float
      • 布尔:boolean
      • 日期:date
      • 对象:object
    • index:是否创建索引,默认为true
    • analyzer:分词器
    • properties:某字段的子字段

4.2 创建索引库

ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示

PUT /索引库名称
{
    "mappings":{
        "properties":{
            "字段名1":{
                "type":"text",
                "analzyer":"ik_smart"
            },
            "字段名2":{
                "type":"keyword",
                "index":false
            },
            "字段名3":{
                "properties":{
                    "子字段": {
                       "type":"keyword" 
                    }  
                }             
            } 
        } 
    } 
}

4.3 查看、删除索引库

  • 查看索引库
GET /索引库名
  • 删除索引库
DELETE /索引库名

4.4 修改索引库

索引库和mapping一旦创建无法修改,但是可以加入新的字段

PUT /索引库名/_mapping
{
    "properties":{
        "新字段名":{
            "type":"integer"
        }
    }
}

5. 文档操作

5.1 新增文档

POST /索引库名/_doc/文档ID
{
    "字段1":"值1",
    "字段2":"值2",
    "字段3":{
        "子属性1":"值3",
        "子属性2":"值4"
    }
}

5.2 查询文档

GET /索引库名/_doc/文档ID

5.3 删除文档

DELETE /索引库名/_doc/文档ID

5.4 修改文档

5.4.1 全量修改
  • 会删除旧文档,添加新文档
PUT /索引库名/_doc/文档ID
{
	"字段1":"值1",
	"字段2":"值2"
}
5.4.2 增量修改
  • 只修改指定字段值
POST /索引库名/_update/文档ID
{
    "doc":{
        "字段名":"新值"
    }
}

6. RestClient

  1. ES中支持两种地理坐标类型:
  • geo_point:由纬度(latitude)和经度(longitude)确定的一个点
  • geo_shape:由多个geo_point组成的复杂几何图形
  1. ES中字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段
"all":{
    "type":"text",
    "analyzer":"ik_max_word"
},
"brand":{
    "type":"keyword",
    "copy_to":"all"
}
PUT /hotel
{
  "mappings":{
    "properties": {
      "id":{
        "type":"keyword"
      },
      "name":{
        "type":"text",
        "analyzer":"ik_max_word",
        "copy_to": "all"
      },
      "address":{
        "type":"keyword",
        "index":false
      },
      "price":{
        "type":"integer"
      },
      "score":{
        "type":"integer"
      },
      "brand":{
        "type":"keyword"
      },
      "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": "ik_max-word"
      }
    }
  }
}
  • 参不参与分词

    • "type":"keyword":不参与分词
    • "type":"text":参与分词
      • "analyzer":指定分词器
  • 参不参与搜索:

    • "index":true:参与搜索
    • "index":false:不参与搜索
  • 多个字段均参与搜索:

    • 添加一个新的字段,并将要参与搜索的字段copy_to新的字段

      "新字段":{
          "type":"text",
          "analyzer":"ik_max_word"
      },
      "city":{
          "type":"keyword",
          "copy_to":"新字段"
      }
      

6.1 初始化 JavaRestClient

  • 引入依赖

    <properties>
    	<elasticsearch.version>7.12.1</elasticsearch.version>
    </properties>
    
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>${elasticsearch.version}</version>
    </dependency>
    
  • 书写测试类

    public class HotelIndexTest {
        private RestHighLevelClient client;
    
        @BeforeEach
        void setUp(){
            this.client = new RestHighLevelClient(RestClient.builder(
                    HttpHost.create("http://192.168.174.130:9200")
            ));
        }
    
        @AfterEach
        void tearDown() throws IOException{
            this.client.close();
        }
    
        @Test
        public void testInit(){
            System.out.println(client);
        }
    }
    

6.2 RestClient实现索引库的CRUD

6.2.1 创建索引库
  • 将DSL语句创建为常量使用,内容如下,需要去掉请求方式和请求地址,变成正常的JSON格式
{
  "mappings":{
    "properties": {
      "id":{
        "type":"keyword"
      },
      "name":{
        "type":"text",
        "analyzer":"ik_max_word",
        "copy_to": "all"
      },
      "address":{
        "type":"keyword",
        "index":false
      },
      "price":{
        "type":"integer"
      },
      "score":{
        "type":"integer"
      },
      "brand":{
        "type":"keyword"
      },
      "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": "ik_max_word"
      }
    }
  }
}
@Test
public void createHotelIndex() throws IOException {
    //创建提交创建索引库请求的对象
    CreateIndexRequest hotelRequest = new CreateIndexRequest("hotel");
    //为请求对象设置请求DSL语句及请求格式
    hotelRequest.source(HotelConstants.HOTEL_MAPPING_TEMPLATE, XContentType.JSON);
    //创建索引库
    client.indices().create(hotelRequest, RequestOptions.DEFAULT);
}
  • indices():此方法返回的对象中包含所有有关索引库的操作方法
6.2.2 删除索引库
@Test
public void deleteHotelIndex() throws IOException {
     //创建提交删除索引库请求的对象
    DeleteIndexRequest hotelRequest = new DeleteIndexRequest("hotel");
    client.indices().delete(hotelRequest,RequestOptions.DEFAULT);
}
6.2.3 判断索引库是否存在
@Test
public void existsHotelIndex() throws IOException {
     //创建提交查询索引库请求的对象
    GetIndexRequest hotelRequest = new GetIndexRequest("hotel");
    boolean exists = client.indices().exists(hotelRequest, RequestOptions.DEFAULT);
    System.out.println(exists?"索引库已存在":"索引库不存在");
}
6.2.4 查询索引库
@Test
public void getHotelIndex() throws IOException{
    //创建提交查询索引库请求的对象
    GetIndexRequest hotelRequest = new GetIndexRequest("hotel");
    GetIndexResponse hotelResponse = client.indices().get(hotelRequest, RequestOptions.DEFAULT);
    Map<String, MappingMetadata> mappings = hotelResponse.getMappings();
    ObjectMapper objectMapper = new ObjectMapper();
    String mappingsJSON = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mappings);
    System.out.println(mappingsJSON);
}

6.3 RestClient实现文档的CRUD

6.3.1 创建文档
@Test
public void createDoc() throws IOException{
    Hotel hotel = hotelService.getById(39106L);
    IndexRequest indexRequest = new IndexRequest("hotel").id(hotel.getId().toString());
    HotelDoc hotelDoc = new HotelDoc(hotel);
    indexRequest.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
    client.index(indexRequest, RequestOptions.DEFAULT);
}
6.3.2 查询文档
@Test
public void getDoc() throws IOException{
    GetRequest getRequest = new GetRequest("hotel").id("39106");
    GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
    System.out.println(JSON.toJSONString(response.getSource(),SerializerFeature.PrettyFormat));
}
6.3.3 删除文档
@Test
public void deleteDoc() throws IOException{
    DeleteRequest deleteRequest = new DeleteRequest("hotel").id("39106");
    client.delete(deleteRequest,RequestOptions.DEFAULT);
}
6.3.4 修改文档
  • 全量更新与创建文档操作并无差别
  • 增量更新
@Test
public void modifyDoc() throws IOException{
    UpdateRequest updateRequest = new UpdateRequest("hotel","39106");
    updateRequest.doc(
        "price","350",
        "starName","五钻"
    ).upsert(
        "area","120"
    );
    client.update(updateRequest,RequestOptions.DEFAULT);
}
6.3.5 批量新增文档
@Test
public void testBulk() throws IOException{
    BulkRequest bulkRequest = new BulkRequest();
    List<Hotel> hotelList = hotelService.list();
    for (Hotel hotel : hotelList) {
        HotelDoc hotelDoc = new HotelDoc(hotel);
        bulkRequest.add(new IndexRequest("hotel")
                        .id(hotelDoc.getId().toString())
                        .source(JSON.toJSONString(hotelDoc),XContentType.JSON));

    }
    client.bulk(bulkRequest,RequestOptions.DEFAULT);
}
GET /hotel/_search

7. DSL语法

7.1 查询所有

match_all
  • 示例
GET /indexName/_search
{
    "query":{
        "[queryType]":{
            "[queryCondition|field]":"[conditionValue|value]"
        }
    }
}
  • 实例
//查询所有
GET /hotel/_search
{
    "query":{
        "match_all":{}
    }
}

7.2 全文检索

match_query
multi_match_query
  • 示例
GET /indexName/_search
{
    "query":{
        "match":{
            "[fieldName]":"[textValue]"
        }
    }
}

GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "[textValue]",
      "fields": ["field1","field2","field3"...]
    }
  }
}
  • 实例
GET /hotel/_search
{
  "query": {
    "match": {
      "all": "外滩"
    }
  }
}

GET /hotel/_search
{
  "query": {
    "multi_match": {
      "query": "如家",
      "fields": ["brand","name","business"]
    }
  }
}

7.3 精确查询

ids
range:根据值的范围查询 gte:大于等于 lte:小于等于 gt:大于 lt:小于
term:根据词条精确值查询
  • 示例
GET /indexName/_search
{
    "query":{
        "term":{
            "[fieldName]":{
                "value":"[value]"
            }
        }
    }
}

GET /indexName/_search
{
	"query":{
        "range":{
            "[fieldName]":{
                "gte":[numValue],
                "lte":[numValue]
            }
        }
    }    
}
  • 实例
GET /hotel/_search
{
  "query":{
    "term": {
      "city": {
        "value": "上海"
      }
    }
  }
}

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

7.4 地理查询

geo_distance
geo_bounding_box
  • 示例
GET /indexName/_search
{
    "query":{
        "geo_distance":{
            "distance":"15km",
            "[fieldName]":"31.21,121.5"
        }
    }
}
  • 实例
GET /hotel/_search
{
  "query": {
    "geo_distance":{
      "distance":"15km",
      "location":"31.21,121.5"
    }
  }
}

7.5 复合查询

bool
function_score:算分函数查询,可以控制文档相关性算分,控制文档排名
7.5.1 相关性算分查询
image-20230510113953995
  • TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大

  • BM25:在elasticsearch5.0之后,会随着词频增加而增大,蛋增长曲线趋于水平

  • 示例

GET /indexName/_search
{
    "query":{
        "function_score":{
            "query":{
                "match":{
                    "all":"外滩"
                }
            },
            "functions":[
                {
                    "filter":{
                        "term":{
                            "id":"1"
                        }
                    },
                    "weight":10
                }
            ],
            "boost_mode":"multiply"
        }
    }
}
image-20230510114817294
  • 实例
//让如家品牌酒店排名更靠前一些
GET /hotel/_search
{
    "query":{
        "function_score":{
            "query":{
                "match":{
                    "all":"外滩"
                }
            },
            "functions":[
                {
                    "filter":{
                        "term":{
                            "brand":"如家"
                        }
                    },
                    "weight":2
                }
            ],
            "boost_mode":"sum"        
        }
    }
}
7.5.2 布尔查询
  • 布尔查询是一个或多个查询字句的组合,子查询的组合方式有如下:
    • 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}}}
            ]
        }
    }
}
GET /hotel/_search
{
    "query":{
        "bool":{
            "must":[
                {"match":{"name":"如家"}}
            ],
            "must_not":[
                {"range":{"price":{"gt":400}}}
            ],
            "filter":[
                {"geo_distance":{"distance":"10km","location":{"lat":"31.21","lon":"121.5"}}
            ]
        }
    }
}

8. 排序

elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序,可排序的字段类型有:

  • keyword
  • 数值
  • 地理坐标
  • 日期类型
GET /indexName/_search
{
    "query":{
        "match_all":{}
    },
    "sort":[
        {
            "[fieldName]":"[desc|asc]"
        }
    ]
}
GET /indexName/_search
{
    "query":{
        "match_all":{}
    },
    "sort":[
        {
            "_geo_distance":{
                "location":"lat,lau",
                "order":"[desc|asc]",
                "unit":"km"
            }
        }
    ]
}

9. 分页

elasticsearch默认情况下只返回top10的数据,而如果要查询更多的数据就需要修改分页参数

  • from:分页开始的位置,默认为0
  • size:每页文档条数
GET /indexName/_search
{
    "query":{
        "match_all":{}
    },
    "from":100,
    "size":20,
    "sort":[
        {"price":"asc"}
    ]
}

深度分页问题

image-20230510134615469

ES是分布式的,所以会面临深度分页问题

  1. 首先在每个数据分片上都排序并查询前1000条文档
  2. 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档

如果搜索页数过深,或者结果集(from+size)越大,对内存和CPU的消耗也越高,所以ES设定结果集查询上限是10000

解决方案
  • search_after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据【官方推荐】
  • scroll:原理是将排序数据形成快照,保存在内存【官方不推荐】

10. 高亮

高亮就是在搜索结果中吧搜索关键字突出显示

高亮显示的原理

  • 后端将搜索结果中的关键字用指定标签标记出来
  • 前端对指定标签添加高亮样式

默认情况下,ES搜索字段必须与高亮字段一致,才会正常返回高亮字段内容,如果要改变默认情况,应该将require_field_match改为false

GET /hotel/_search
{
    "query":{
        "match":{
            "[fieldName]":"[textValue]"
        }
    },
    "highlight":{
        "fields":{
            "[fieldName]":{
            	"pre_tags":"<em>",
                "post_tags":"</em>",
                "require_field_match":false
        	}
        }
    }
}

11. RestClient查询文档

11.1 查询所有

  • 代码

    @Test
    void testMatchAll() throws IOException {
        SearchRequest hotelRequest = new SearchRequest("hotel");
        hotelRequest.source().query(QueryBuilders.matchAllQuery());
        SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel/_search
    {
        "query":{
            "match_all":{}
        }
    }
    
  • RestAPI中构建DSL都是通过HighLevelRestClient中的source()方法实现的

11.2 全文检索

  • 代码

    @Test
    void testMatch() throws IOException {
        SearchRequest hotelRequest = new SearchRequest("hotel");
        hotelRequest.source().query(QueryBuilders.matchQuery("all","如家"));
        SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
    @Test
    void testMultiMatch() throws IOException{
        SearchRequest hotelRequest = new SearchRequest("hotel");
        hotelRequest.source().query(QueryBuilders.multiMatchQuery("如家","business","name"));
        SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel/_search
    {
        "query":{
            "match":{
                "all":"如家"
            }
        }
    }
    
    GET /hotel/_search
    {
        "query":{
            "match":{
                "query":"如家",
                "fields":["brand","name"]
            }
        }
    }
    

11.3 精确查询

  • 代码

    @Test
    void testTerm() throws IOException{
        SearchRequest hotelRequest = new SearchRequest("hotel");
        hotelRequest.source().query(QueryBuilders.termQuery("city","上海"));
        SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
    @Test
    void testRange() throws IOException{
        SearchRequest hotelRequest = new SearchRequest("hotel");
        hotelRequest.source().query(QueryBuilders.rangeQuery("price").gte(150).lte(200));
        SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel._search
    {
        "query":{
            "term":{
                "city":"上海"
            }
        }
    }
    
    GET /hotel/_search
    {
        "query":{
            "range":{
                "price":{
                    "gte":100,
                    "lte":150
                }
            }
        }
    }
    

11.4 地理查询

  • 代码

    @Test
    void testGeoDistance() throws IOException{
        SearchRequest hotelRequest = new SearchRequest("hotel");
        hotelRequest.source().query(QueryBuilders.geoDistanceQuery("location").distance("15km").point(31.21,121.5));
        SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel/_search
    {
      "query": {
        "geo_distance":{
          "distance":"15km",
          "location":"31.21,121.5"
        }
      }
    }
    

11.5 复合查询

  • 代码

    @Test
    void testBool() throws IOException{
        SearchRequest searchRequest = new SearchRequest("hotel");
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        boolQueryBuilder
            .must(QueryBuilders.termQuery("city","上海"))
            .filter(QueryBuilders.rangeQuery("price").gte(150).lte(200));
        searchRequest.source().query(boolQueryBuilder);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel/_search
    {
        "query":{
            "bool":{
                "must":[
                    {"term":{"city":"上海"}}
                ],
                "should":[],
                "must_not":[],
                "filter":[
                    {
                        "range":{
                            "price":{
                                "gte":150,
                                "lte":200
                            }
                        }
                    }
                ]
            }
        }
    }
    

11.6 排序分页

  • 代码

    @Test
    void testPageAndSort() throws IOException{
        SearchRequest searchRequest = new SearchRequest("hotel");
        searchRequest.source()
            .query(QueryBuilders.matchAllQuery())
            .from(0)
            .size(20)
            .sort("price", SortOrder.DESC);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel/_search
    {
        "query":{
            "match_all":{}
        },
        "from":0,
        "size":20,
        "sort":[
            {
                "price":"desc"
            }
        ]
    }
    

11.7 高亮文档

  • 代码

    @Test
    void testHighlight() throws IOException{
        SearchRequest searchRequest = new SearchRequest("hotel");
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("name")
            .requireFieldMatch(false)
            .preTags("<keyword-highlight>")
            .postTags("</keyword-highlight>");
        searchRequest.source()
            .query(QueryBuilders.matchQuery("all","如家"))
            .highlighter(highlightBuilder);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        System.out.println("共检索到" + total + "条数据");
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if(highlightFields!=null && highlightFields.size()>0){
                HighlightField highlightField = highlightFields.get("name");
                System.out.println(highlightField.getFragments()[0].toString());
            }
            System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat));
        }
    }
    
  • DSL

    GET /hotel/_search
    {
        "query":{
            "match":{
                "all":"如家"
            }
        },
        "highlight":{
            "fields":{
                "name":{
                    "pre_tags":"<em>",
                    "post_tags":"</em>",
                    "require_field_match":false
                }
            }
        }
    }
    

12. 数据聚合

12.1 聚合的分类

12.2 DSL实现聚合

12.3 RestAPI实现聚合

13. 自动补全

原文作者:絷缘
作者邮箱:zhiyuanworkemail@163.com
原文地址:https://zhiyuandnc.github.io/DZ1f24lyN/
版权声明:本文为博主原创文章,转载请注明原文链接作者信息