• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

黑马程序员---微服务笔记实用篇

武飞扬头像
睡不着还睡不醒
帮助1

微服务技术栈导学

微服务实现流程:

学新通

 所有要学的技术:

学新通

 分层次教学:

学新通

 具体分层:

学新通

实用篇---第一天

学新通

 一、认识微服务

 单体架构

将业务所有功能集中在一个项目中开发,打成一个包部署

优点:架构简单、部署成本低

缺点:耦合度高

学新通

分布式架构

根据业务功能对系统进行查分,每个业务模块作为独立项目开发,称为一个服务

优点:降低服务耦合、有利于服务升级拓展

学新通

服务治理

分布式架构需要考虑的问题:

服务拆分粒度如何?

服务集群地址如何维护?

服务之间如何实现远程调用?

服务健康状态如何感知?

微服务

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

单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发

面向服务:微服务对外暴露业务接口

自治:团队独立、技术独立、数据独立、部署独立

隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

学新通

微服务技术对比

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

学新通

企业场景

学新通

SpringCloud

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

SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现饿了这些组件的自动装配,从而提供了良好的开箱即用的体验

学新通

SpringBoot和SpringCloud版本兼容如下:

学新通

二、服务拆分及远程调用

服务拆分

1.不同微服务,不要重复开发相同的业务

2.微服务数据独立,不要访问其他微服务的数据库

3.微服务可以将自己的业务暴露为接口,供其他微服务调用

学新通

远程调用

要想实现跨服务的远程调用,其实就是发送一次http请求

1.注册RestTemplate

学新通

2.调用api

学新通

三、Eureka

提供者与消费者

服务提供者:一次业务中,被其他微服务调用的服务(提供接口给其他服务)

服务消费者:一次业务中,调用其他微服务的服务(调用其他微服务提供的接口)

服务A调用服务B,服务B调用服务C,那么服务B是什么角色呢?

        既是服务提供者,有时候服务消费者

Eureka原理分析

消费者该如何获取服务提供者的具体信息呢?

        服务提供者启动时向eureka注册自己的信息

        eureka保存这些信息

        消费者根据服务名向eureka拉取提供者信息

如果有多个服务提供者,消费者该如何选择?

        服务消费者利用负载均衡算法,从服务列表中挑选一个

消费者如何感知服务提供者健康状态?

        服务提供者会每隔30s向EurekaServer发送心跳请求,报告健康状态

        eureka会更新记录服务列表信息,心跳不正常会被踢除

        消费者就可以拉取到最新的信息 

学新通

搭建eureka服务

任务:

学新通

搭建EurekaServer:

学新通

注册user-service

服务注册两步:

        1.引入eureka-client依赖

        2.在application.yml中配置eureka地址

学新通

为了能够更方便的模拟多个服务请求,可以编辑配置

-D代表参数

server.port就是配置端口

学新通

此时可以看到USERSERVICE有两个

学新通

服务拉取

服务拉取是基于服务名称获取服务列表,然后对服务列表做负载均衡

学新通

四、 Ribbon

负载均衡原理

学新通

负载均衡策略

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则

学新通

策略详解:

学新通

通过定义IRule实现可以修改负载均衡规则:

方式①:在order-service中的OrderApplication类中,定义一个新的IRule

注意:这是全局配置

  1.  
    @Bean
  2.  
    public IRule randomRule(){
  3.  
    return new RandomRule();
  4.  
    }

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

注意:这是局部配置

  1.  
    userservice:
  2.  
    ribbon:
  3.  
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

饥饿加载

Ribbon默认采用懒加载,即第一次访问时才会创建LoadBalanceClient,请求时间会很长。

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面的配置开启饥饿加载:

  1.  
    ribbon:
  2.  
    eager-load:
  3.  
    enabled: true
  4.  
    clients: userservice

总结:

学新通

五、Nacos

认识并安装Nacos

Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件,相比Eureka功能更加丰富,在国内受欢迎程度更高。

在nacos解压目录的bin目录下进入命令窗口,输入

学新通

进入nacos页面,初始用户名和密码都是nacos

学新通

nacos快速入门

服务注册到nacos:

学新通

学新通

nacos服务分级存储模型

①一级是服务,例如userservice

②二级是集群,例如杭州或上海

③三级是实例,例如杭州机房的某台部署了userservice的服务器

学新通

服务跨集群调用问题:

服务调用尽可能选择本地集群的服务,跨集群调用延迟较高

本地集群不可访问时,再去访问其他集群

服务集群属性配置:

修改application.yml文件,添加spring.cloud.nacos.discovery

学新通

NacosRule 负载均衡

根据集群负载均衡:

学新通

NacosRule负载均衡策略:

①优先选择同集群服务实例列表

②本地集群找不到提供者,才会去其它集群寻找,并且会报警告

③确定了可用实例列表后,再采用随机负载均衡挑选实例

根据权重负载均衡

在实际部署中会出现这样的场景:

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求

Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高

学新通

环境隔离---namespace

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离

学新通

新建命名空间:

学新通

 创建好命名空间以后,添加namespace配置

学新通

 Nacos环境隔离:

①namespace用来做环境隔离

②每个namespace都有唯一的id

③不同的namespace下的服务不可见

nacos注册中心细节分析

学新通

 临时实例和非临时实例:

服务器注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:

学新通

Nacos与eureka的共同点:

①都支持服务注册和服务拉取

②都支持服务提供者心跳方式做健康检测

Nacos和Eureka的区别:

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

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

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

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

实用篇---第二天

学新通

 一、Nacos配置管理

Nacos实现配置管理

 学新通

 配置的统一管理:

        Data ID命名:服务名称-profile.后缀名

        Group:分组,默认即可

        配置格式:目前支持yaml和properties

        配置内容:填写将来可能会改变的配置

学新通

微服务配置拉取

学新通

 进行微服务配置拉取可以实现:

学新通

步骤:

学新通

注意:如果你的userservice配置在dev命名空间下,请标明

  1.  
    spring:
  2.  
    application:
  3.  
    name: userservice
  4.  
    profiles:
  5.  
    active: dev
  6.  
    cloud:
  7.  
    nacos:
  8.  
    server-addr: localhost:8848
  9.  
    config:
  10.  
    file-extension: yaml #文件后缀名
  11.  
    namespace: 0482f978-fc0d-4030-948c-5a91488ff1d5

总结:

将配置交给Nacos管理的步骤

①在Nacos中添加配置文件

②在微服务中引入nacos的config依赖

③在为服务中添加bootstrap.yml,配置nacos地址、当前环境、服务名称、文件后缀名。这些决定了程序启动时去nacos读取哪个文件

配置热更新

Nacos中的配置文件变更后,微服务无需重启就可以感知,不过需要通过下面两种配置实现:

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

学新通

方式二:使用@ConfigurationProperties注解

  1.  
    @Data
  2.  
    @ConfigurationProperties(prefix="pattern")
  3.  
    @Component
  4.  
    public class PatternProperties {
  5.  
     
  6.  
    private String dateformat;
  7.  
    }

 在Controller中不再需要@Value注解,直接自动注入PatternProperties类即可

  1.  
    @Slf4j
  2.  
    @RestController
  3.  
    @RequestMapping("/user")
  4.  
    public class UserController {
  5.  
    @Autowired
  6.  
    private PatternProperties properties;
  7.  
     
  8.  
    @GetMapping("now")
  9.  
    public String now(){
  10.  
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
  11.  
    }
  12.  
    }

总结:

学新通

 多环境配置共享

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

[spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml

[spring.application.name].yaml,例如:userservice.yaml

无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件

学新通

多种配置的优先级:

服务名-profile.yaml > 服务名称.yaml > 本地配置

学新通

nacos集群搭建

学新通

学新通

搭建集群基本步骤:

        搭建数据库,初始化数据库表结构

        下载nacos安装包

        配置nacos

        启动nacos集群

        nginx反向代理

mysql脚本:

  1.  
    /******************************************/
  2.  
    /* 数据库全名 = nacos_config */
  3.  
    /* 表名称 = config_info */
  4.  
    /******************************************/
  5.  
    CREATE TABLE `config_info` (
  6.  
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  7.  
    `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  8.  
    `group_id` varchar(255) DEFAULT NULL,
  9.  
    `content` longtext NOT NULL COMMENT 'content',
  10.  
    `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  11.  
    `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  12.  
    `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  13.  
    `src_user` text COMMENT 'source user',
  14.  
    `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip',
  15.  
    `app_name` varchar(128) DEFAULT NULL,
  16.  
    `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  17.  
    `c_desc` varchar(256) DEFAULT NULL,
  18.  
    `c_use` varchar(64) DEFAULT NULL,
  19.  
    `effect` varchar(64) DEFAULT NULL,
  20.  
    `type` varchar(64) DEFAULT NULL,
  21.  
    `c_schema` text,
  22.  
    PRIMARY KEY (`id`),
  23.  
    UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
  24.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
  25.  
     
  26.  
    /******************************************/
  27.  
    /* 数据库全名 = nacos_config */
  28.  
    /* 表名称 = config_info_aggr */
  29.  
    /******************************************/
  30.  
    CREATE TABLE `config_info_aggr` (
  31.  
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  32.  
    `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  33.  
    `group_id` varchar(255) NOT NULL COMMENT 'group_id',
  34.  
    `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  35.  
    `content` longtext NOT NULL COMMENT '内容',
  36.  
    `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  37.  
    `app_name` varchar(128) DEFAULT NULL,
  38.  
    `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  39.  
    PRIMARY KEY (`id`),
  40.  
    UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
  41.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
  42.  
     
  43.  
     
  44.  
    /******************************************/
  45.  
    /* 数据库全名 = nacos_config */
  46.  
    /* 表名称 = config_info_beta */
  47.  
    /******************************************/
  48.  
    CREATE TABLE `config_info_beta` (
  49.  
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  50.  
    `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  51.  
    `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  52.  
    `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  53.  
    `content` longtext NOT NULL COMMENT 'content',
  54.  
    `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  55.  
    `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  56.  
    `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  57.  
    `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  58.  
    `src_user` text COMMENT 'source user',
  59.  
    `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip',
  60.  
    `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  61.  
    PRIMARY KEY (`id`),
  62.  
    UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
  63.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
  64.  
     
  65.  
    /******************************************/
  66.  
    /* 数据库全名 = nacos_config */
  67.  
    /* 表名称 = config_info_tag */
  68.  
    /******************************************/
  69.  
    CREATE TABLE `config_info_tag` (
  70.  
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  71.  
    `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  72.  
    `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  73.  
    `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  74.  
    `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  75.  
    `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  76.  
    `content` longtext NOT NULL COMMENT 'content',
  77.  
    `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  78.  
    `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  79.  
    `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  80.  
    `src_user` text COMMENT 'source user',
  81.  
    `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip',
  82.  
    PRIMARY KEY (`id`),
  83.  
    UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
  84.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
  85.  
     
  86.  
    /******************************************/
  87.  
    /* 数据库全名 = nacos_config */
  88.  
    /* 表名称 = config_tags_relation */
  89.  
    /******************************************/
  90.  
    CREATE TABLE `config_tags_relation` (
  91.  
    `id` bigint(20) NOT NULL COMMENT 'id',
  92.  
    `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  93.  
    `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  94.  
    `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  95.  
    `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  96.  
    `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  97.  
    `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  98.  
    PRIMARY KEY (`nid`),
  99.  
    UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  100.  
    KEY `idx_tenant_id` (`tenant_id`)
  101.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
  102.  
     
  103.  
    /******************************************/
  104.  
    /* 数据库全名 = nacos_config */
  105.  
    /* 表名称 = group_capacity */
  106.  
    /******************************************/
  107.  
    CREATE TABLE `group_capacity` (
  108.  
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  109.  
    `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  110.  
    `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  111.  
    `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  112.  
    `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  113.  
    `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  114.  
    `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  115.  
    `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  116.  
    `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  117.  
    `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  118.  
    PRIMARY KEY (`id`),
  119.  
    UNIQUE KEY `uk_group_id` (`group_id`)
  120.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
  121.  
     
  122.  
    /******************************************/
  123.  
    /* 数据库全名 = nacos_config */
  124.  
    /* 表名称 = his_config_info */
  125.  
    /******************************************/
  126.  
    CREATE TABLE `his_config_info` (
  127.  
    `id` bigint(64) unsigned NOT NULL,
  128.  
    `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  129.  
    `data_id` varchar(255) NOT NULL,
  130.  
    `group_id` varchar(128) NOT NULL,
  131.  
    `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  132.  
    `content` longtext NOT NULL,
  133.  
    `md5` varchar(32) DEFAULT NULL,
  134.  
    `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00',
  135.  
    `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00',
  136.  
    `src_user` text,
  137.  
    `src_ip` varchar(20) DEFAULT NULL,
  138.  
    `op_type` char(10) DEFAULT NULL,
  139.  
    `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  140.  
    PRIMARY KEY (`nid`),
  141.  
    KEY `idx_gmt_create` (`gmt_create`),
  142.  
    KEY `idx_gmt_modified` (`gmt_modified`),
  143.  
    KEY `idx_did` (`data_id`)
  144.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
  145.  
     
  146.  
     
  147.  
    /******************************************/
  148.  
    /* 数据库全名 = nacos_config */
  149.  
    /* 表名称 = tenant_capacity */
  150.  
    /******************************************/
  151.  
    CREATE TABLE `tenant_capacity` (
  152.  
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  153.  
    `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  154.  
    `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  155.  
    `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  156.  
    `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  157.  
    `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  158.  
    `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  159.  
    `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  160.  
    `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  161.  
    `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  162.  
    PRIMARY KEY (`id`),
  163.  
    UNIQUE KEY `uk_tenant_id` (`tenant_id`)
  164.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
  165.  
     
  166.  
     
  167.  
    CREATE TABLE `tenant_info` (
  168.  
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  169.  
    `kp` varchar(128) NOT NULL COMMENT 'kp',
  170.  
    `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  171.  
    `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  172.  
    `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  173.  
    `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  174.  
    `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  175.  
    `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  176.  
    PRIMARY KEY (`id`),
  177.  
    UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  178.  
    KEY `idx_tenant_id` (`tenant_id`)
  179.  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
  180.  
     
  181.  
    CREATE TABLE users (
  182.  
    username varchar(50) NOT NULL PRIMARY KEY,
  183.  
    password varchar(500) NOT NULL,
  184.  
    enabled boolean NOT NULL
  185.  
    );
  186.  
     
  187.  
    CREATE TABLE roles (
  188.  
    username varchar(50) NOT NULL,
  189.  
    role varchar(50) NOT NULL,
  190.  
    constraint uk_username_role UNIQUE (username,role)
  191.  
    );
  192.  
     
  193.  
    CREATE TABLE permissions (
  194.  
    role varchar(50) NOT NULL,
  195.  
    resource varchar(512) NOT NULL,
  196.  
    action varchar(8) NOT NULL,
  197.  
    constraint uk_role_permission UNIQUE (role,resource,action)
  198.  
    );
  199.  
     
  200.  
    INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
  201.  
     
  202.  
    INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
学新通

进入nacos的conf目录,修改配置文件cluster.conf.example,重名名为cluster.conf

学新通

配置application.properties

        打开注释,告诉nacos我们使用的是mysql集群

需要打开的注释:

  1.  
    spring.datasource.platform=mysql
  2.  
     
  3.  
    db.num=1
  4.  
     
  5.  
    DB:db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
  6.  
    db.user.0=nacos
  7.  
    db.password.0=nacos

将配置好的文件复制三份:

学新通

将每份文件的server.port修改为之前配好的三个端口之一

比如:server.port=8845

启动:

找到bin目录,打开命令行窗口,输入命令:startup.cmd

修改config/nginx.conf文件,将下面的配置黏贴到http内部:

  1.  
    upstream nacos-cluster{
  2.  
    server 127.0.0.1:8845;
  3.  
    server 127.0.0.1:8846;
  4.  
    server 127.0.0.1:8847;
  5.  
    }
  6.  
     
  7.  
     
  8.  
    server{
  9.  
    listen 80;
  10.  
    server_name localhost;
  11.  
     
  12.  
    location /nacos {
  13.  
    proxy_pass http://nacos-cluster;
  14.  
    }
  15.  
     
  16.  
     
  17.  
    }
学新通

在nginx安装目录下打开命令符提示窗口,输入:

start nginx.exe

配置application.yaml或者bootstrap.yaml

  1.  
    spring:
  2.  
    cloud:
  3.  
    nacos:
  4.  
    server-addr: localhost:80

二、Feign远程调用

初识http客户端Feign

restTemplate方式调用存在的问题

学新通

Feign是一个声明式的http客户端,其作用就是帮助我们优雅地实现http请求的发送,解决上面的问题

使用Feign的步骤:

学新通

 学新通

注:如果Maven导入openfeign失败,可以加上版本号

  1.  
    <dependency>
  2.  
    <groupId>org.springframework.cloud</groupId>
  3.  
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  4.  
    <version>2.1.1.RELEASE</version>
  5.  
    </dependency>

实现远程调用:

  1.  
     
  2.  
     
  3.  
    @RestController
  4.  
    @RequestMapping("order")
  5.  
    public class OrderController {
  6.  
     
  7.  
    @Autowired
  8.  
    private OrderService orderService;
  9.  
     
  10.  
    @Autowired
  11.  
    private UserClient userClient;
  12.  
     
  13.  
    @GetMapping("{orderId}")
  14.  
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
  15.  
    // 根据id查询订单并返回
  16.  
    Order order=orderService.queryOrderById(orderId);
  17.  
    User user = userClient.findById(order.getUserId());
  18.  
    order.setUser(user);
  19.  
    return order;
  20.  
    }
  21.  
    }
学新通

总结:

Feign的使用步骤

        ①引入依赖

        ②添加@EnableFeignClients注解

        ③编写FeignClient接口

        ④使用FeignClient中定义的方法代替RestTemplate

自定义Feign的配置

学新通

配置Feign日志有两种方式:

学新通

 学新通

Feign的日志配置总结:

1.方式一是配置文件,feign.client.config.xxx.loggerLevel

        ①如果xxx是default则代表全局

        ②如果xxx是服务名称,例如userservice,则代表某服务

2.方式二是java代码配置Logger.Level这个Bean

        ①如果在@EnableFeignClients注解声明则代表全局

        ②如果在@FeignClient注解中声明则代表某服务

Feign性能优化

Feign底层的客户端实现:

        URLConnection:默认实现,不支持连接池

        Apache HttpClient:支持连接池

        OKHttp:支持连接池

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

        ①使用连接池代替默认的URLConnection

        ②日志级别,最好用basic或none

Feign添加HttpClient的支持:

学新通

Feign的优化总结:

1.日志级别尽量用basic

2.使用HttpClient或OKHttp代替URLConnection

        ①引入feign-httpClient依赖

        ②配置文件开启httpClient功能,设置连接池参数

Feign的最佳实践

方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准

学新通

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

学新通

Feign的最佳实践总结:

        ①让controller和FeignClient继承同一个接口

        ②将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用

Feign最佳实践的实现

步骤:

1.首先创建一个module,命名为feign-api,然后引入feign的starter依赖

2.将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中

3.在order-service中引入feign-api的依赖

4.修改order-service中所有与上诉三个组件有关的import部分,改成导入feign-api中的包

学新通

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

学新通

总结:

学新通

三、统一网关Gateway

网关作用介绍

为什么需要网关?

学新通

 网关的技术实现:

在SpringCloud中网关的实现有两种:

        gateway

        zuul

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

搭建网关服务

步骤:

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

  1.  
    <dependencies>
  2.  
    <!--nacos服务注册发现依赖-->
  3.  
    <dependency>
  4.  
    <groupId>com.alibaba.cloud</groupId>
  5.  
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  6.  
    </dependency>
  7.  
    <!--网关gateway依赖-->
  8.  
    <dependency>
  9.  
    <groupId>org.springframework.cloud</groupId>
  10.  
    <artifactId>spring-cloud-starter-gateway</artifactId>
  11.  
    </dependency>
  12.  
    </dependencies>

2.编写路由配置及nacos地址

  1.  
    server:
  2.  
    port: 10010 #网关端口
  3.  
    spring:
  4.  
    application:
  5.  
    name: gateway
  6.  
    cloud:
  7.  
    nacos:
  8.  
    server-addr: localhost:8848 #nacos地址
  9.  
    gateway:
  10.  
    routes: #网关路由配置
  11.  
    - id: user-service #路由id,自定义,只要唯一即可
  12.  
    uri: lb://userservice #路由的目标地址lb(loadBalance)就是负载均衡,后面跟服务名称
  13.  
    predicates: #路由断言,也就是判断请求是否符合路由规则的条件
  14.  
    - Path=/user/** #这个是按照路径匹配,只要以/user/开头就符合规则
  15.  
    - id: order-service
  16.  
    uri: lb://orderservice
  17.  
    predicates:
  18.  
    - Path=/order/**
学新通

网关工作流程图:

学新通

总结:

网关搭建步骤:

        1.创建项目,引入nacos服务发现和gateway依赖

        2.配置application.yml,包括服务基本信息、nacos地址、路由

路由配置包括:

        1.路由id:路由的唯一标识

        2.路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡

        3.路由断言(predicates):判断路由的规则

        4.路由过滤器(filters):对请求或响应做处理

路由断言工厂 Route Predicate Factory

网关路由可以配置的内容包括:

路由id:路由唯一标识

uri:路由目的地,支持lb和http两种

predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地

filters:路由过滤器,处理请求或响应

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

例如Path=/user/** 是按照路径匹配,这个规则是有org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory

像这样的断言工厂spring还有十几个:

学新通

以After为例,配置了未来的时间,则访问失败:

- After=2031-04-13T15:14:47.433 08:00[Asia/Shanghai]

学新通

路由过滤器 GatewayFilter

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

学新通

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

学新通

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

实现方式:在gateway中修改application.yml文件,给userservice的路由添加过滤器:

  1.  
    filters:
  2.  
    - AddRequestHeader=Truth,Itcast is freaking awesome!

 验证方式:修改UserController,接收请求头Truth

  1.  
    @GetMapping("/{id}")
  2.  
    public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth",required = false)String truth) {
  3.  
    System.out.println("truth:" truth);
  4.  
    return userService.queryById(id);
  5.  
    }

打印成功:

学新通

默认过滤器:

如果要对所有的路由都生效,则可以将过滤器工厂写到default下

学新通

总结:

过滤器的作用是什么?

        ①对路由的请求或响应做加工处理,比如添加请求头

        ②配置在路由下的过滤器只对当前路由的请求生效

defaultFilters的作用是什么?

        ①对所有路由都生效的过滤器

全局过滤器 GlobalFilter

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样

区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现

定义方式是实现GlobalFilter接口:

学新通

定义全局过滤器,拦截并判断用户身份:

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

                参数中是否有authorization

                authorization参数值是否有admin

        如果同时满足则放行,否则拦截

  1.  
    @Component
  2.  
    @Order(-1) //指定过滤器的顺序
  3.  
    public class AuthorizeFilter implements GlobalFilter {
  4.  
    @Override
  5.  
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
  6.  
    //1.获取请求参数
  7.  
    ServerHttpRequest request = exchange.getRequest();
  8.  
    MultiValueMap<String, String> params = request.getQueryParams();
  9.  
    //2.获取参数中的authorization参数
  10.  
    String auth=params.getFirst("authorization");
  11.  
    //3.判断参数值是否等于admin
  12.  
    if("admin".equals(auth)){
  13.  
    //4.是,放行
  14.  
    return chain.filter(exchange);
  15.  
    }
  16.  
    //5.否,拦截
  17.  
    //5.1 设置状态码
  18.  
    exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //401代表未登录
  19.  
    //5.2 拦截请求
  20.  
    return exchange.getResponse().setComplete();
  21.  
     
  22.  
    }
  23.  
    }
学新通

总结:

全局过滤器的作用是什么?

        对所有路由都生效的过滤器,并且可以自定义处理逻辑

实现全局过滤器的步骤是什么?

        ①实现GlobalFilter接口

        ②添加@Order注解或实现Ordered接口

        ③编写处理逻辑

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter合并到一个过滤器链(集合)中,排序后依次执行每个过滤器

学新通

 默认过滤器和路由过滤器很相似,他们都是GatewayFilter

而对于全局过滤器,虽然他是GlobalFilter,但是通过适配器模式,可以将他转为GatewayFilter学新通

每一个过滤器必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前

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

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

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

学新通

总结:

学新通

 网关的cors跨域配置

跨域:域名不一致就是跨域

主要包括:

学新通

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

 解决方案:CORS

网关处理跨域采用的同样是CORS方案,并且只需要简单的配置即可实现:

学新通

实用篇---第三天

学新通

一、初识Docker

什么是Docker

项目部署的问题:

学新通

Docker是如何解决依赖的兼容问题呢?

学新通

linux操作系统结构:

学新通

 Ubuntu和CentOS都是基于Linux内核,只是系统应用不同,提供的函数库有差异,所以程序不能跨系统运行

Docker如何解决不同系统环境的问题?

==>Docker打包好的程序可以运行在任何linux系统上

学新通

总结:

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

        Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像

        Docker应用运行在容器中,使用沙箱机制,相互隔离

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

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

Docker是一个快速交付应用、运行应用的技术

学新通

Docker与虚拟机的差别

学新通

Docker与虚拟机性能的差别:

学新通

Docker和虚拟机的差异:

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

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

Docker架构

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

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

学新通

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

docker架构:

学新通

总结:

学新通

 安装Docker

Docker分为CE和EE两大版本,CE是社区版免费,EE是企业版收费

Docker CE支持64为版本CentOS 7,并且要求内核版本不低于3.10,CentOS 7满足最低内核要求,所以我们在CentOS 7 上安装Docker

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

学新通

开始正式安装Docker

第一步:虚拟机联网,安装yum工具

学新通

第二步:更新本地镜像源

学新通

 上面的命令要分开敲,如下:

学新通

第三步:安装docker-ce(社区免费版)

学新通

至此,docker安装完毕

启动docker

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

        启动docker前,一定要关闭防火墙!!!

学新通

启动docker:

命令:systemctl start docker

启动后,可以用docker -v来查看docker的版本,如果能够查到,则启动docker成功

学新通

 配置镜像:

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

学新通

具体命令分步可以看我的代码:

学新通

 二、Docker的基本操作

镜像操作

镜像名称一般分为两部分:

        [repository]:[tag]

tag是版本号,在没有指定tag时,默认是latest,代表最新版本的镜像

学新通

镜像操作命令:

学新通

dock

案例一:从DockerHub中拉去一个nginx镜像并查看

学新通

拉取镜像成功后,可以通过docker images查看

学新通

案例二:利用docker save将nginx镜像导出磁盘,然后再通过load加载回来

步骤一:利用dockr xx --help命令查看docker save和docker load的语法

学新通

步骤二:使用docker tag 创建新镜像mynginx 1.0

步骤三:使用docker save导出镜像到磁盘

学新通

通过load加载回来:

学新通

总结:

学新通

镜像命令练习

学新通

 我的练习代码如下:

[root@Soft soft]# docker pull redis:alpine3.17

[root@Soft soft]# docker save -o redis.tar redis:alpine3.17

[root@Soft soft]# docker rmi redis:alpine3.17

[root@Soft soft]# docker load -i redis.tar

最终操作结果:

学新通

容器相关命令

学新通

 向浏览器

 容器命令案例1

创建并运行一个nginx容器

        去docker hub查看nginx的容器运行命令

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

命令解读:

docker run:创建并运行一个容器

--name:给容器起一个名字,比如叫做mn

-p:将宿主端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口

        注意:宿主机的端口只要没被占用,可以任意指定;但是容器内的端口一般取决于运行的程序本身,一般是固定的,比如nginx一般监听的都是80端口

-d:后台运行容器

nginx:镜像名称,例如nginx,不写tag代表最新nginx

学新通

容器创建成功后,会返回一个字符串。这是容器的全局唯一标识,代表容器创建成功

学新通

 查看容器运行状态:

学新通

浏览器访问:

注明,可以用ifconfig查看虚拟机的ip地址

学新通

向浏览器输入虚拟机的ip地址,查看到下面的页面则说明nginx部署成功:

学新通

查看日志:-f选项可以持续跟踪日志,而无需每次手动查询日志

[root@Soft soft]# docker logs -f mn

总结:

学新通

容器命令案例2

进入nginx容器,修改HTML文件内容,添加“传智教育欢迎您”

步骤一:进入容器。

        进入我们刚刚创建的nginx容器的命令为:

        docker exec -it mn bash

命令解读:

docker exec:进入容器命令,执行一个命令

-it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互

mn:要进入的容器的名称

bash:进入容器后执行的命令,bash是一个linux终端交互命令

进入nginx容器内部,其实就是一个阉割版的linux,可以使用linux的命令

学新通

步骤二:进入nginx的HTML所在目录 /usr/share/nginx/html

步骤三:修改index.html的内容

学新通

退出容器的命令:exit

docker ps默认只能查看运行中的容器,可以加-a参数查看所有容器

学新通

当容器处于运行状态时,不能直接通过docker rm containerName删除,需要先停止容器运行才行。为了方便,可以直接使用-f参数,强制删除运行时的容器

学新通

总结:

查看容器状态:

        docker ps

        添加-a参数查看所有状态的容器

删除容器:

        docker rm

        不能删除运行中的容器,除非添加-f参数

进入容器:

        命令是docker exec -it [容器名] [要执行的命令]

        exec命令可以进入容器修改文件,但是在容器内修改文件是不推荐的

容器命令练习

创建并运行一个redis容器,并且支持数据持久化

学新通

 进入redis容器,并执行redis-cli客户端命令,存入num=555

学新通

 数据卷命令

容器与数据耦合的问题:

学新通

数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录

学新通

 操作数据卷:

数据卷操作的基本语法如下:

docker volume [COMMAND]

 docker volume 命令是数据卷操作,但根据命令后跟随的command来确定下一步的操作:

create        创建一个volume

inspect        显示一个或多个volume的信息

ls                列出所有的volume

prune        删除未使用的volume

rm                删除一个或多个指定的volume

官方释义:

学新通

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

①创建数据卷

学新通

②查看所有数据

学新通

③查看数据卷详细信息卷

 学新通

 删除未使用的数据卷:

学新通

移除特定的数据卷

学新通

 总结:

数据卷的作用:

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

数据卷操作:

docker volume create

docker volume ls

docker volume inspect

docker volume rm

docker volume prune

挂载数据卷

我们在创建容器时,可以通过-v参数来挂载一个数据卷到某个容器目录

学新通

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

需求说明:上个案例中,我们进入nginx容器内部,已经知道nginx的html目录所在位置是 /usr/share/nginx/html,我们需要把这个目录挂载到html这个数据卷上,方便操作其中的内容

提示:运行容器时使用-v参数挂载数据卷

步骤:

①创建容器并挂载数据卷到容器内的HTML目录

学新通

②进入html数据卷所在位置,并修改HTML内容

学新通

运行结果:

学新通

 如果做数据卷挂载,数据卷不存在,docker会自动创建需要的数据卷

学新通

总结:

数据卷挂载方式:

        -v volumeName:/targetContainerPath

        如果容器运行时volume不存在,会被自动创建出来

数据卷挂载案例

创建并运行一个MySQL容器,将宿主机目录直接挂载到容器上

目录挂载与数据卷挂载语法相似:

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

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

学新通

步骤1:

学新通

步骤2/3:

学新通

步骤4:

学新通

两种数据卷挂载方式的对比:

学新通

 总结:

学新通

三、Dockerfile自定义镜像

镜像结构

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

学新通

总结:

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

        BaseImage层:包含基本的系统函数库、环境变量、文件系统

        Entrypoint:入口,是镜像中应用启动的命令

        其他:在BaseImage基础上添加依赖、安装程序、完成整个应用的安装和配置

Dockerfile

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

学新通

 简单的Dockerfile为例:

  1.  
    # 指定基础镜像
  2.  
    FROM ubuntu:16.04
  3.  
    # 配置环境变量,JDK的安装目录
  4.  
    ENV JAVA_DIR=/usr/local
  5.  
     
  6.  
    # 拷贝jdk和java项目的包
  7.  
    COPY ./jdk8.tar.gz $JAVA_DIR/
  8.  
    COPY ./docker-demo.jar /tmp/app.jar
  9.  
     
  10.  
    # 安装JDK
  11.  
    RUN cd $JAVA_DIR \
  12.  
    && tar -xf ./jdk8.tar.gz \
  13.  
    && mv ./jdk1.8.0_144 ./java8
  14.  
     
  15.  
    # 配置环境变量
  16.  
    ENV JAVA_HOME=$JAVA_DIR/java8
  17.  
    ENV PATH=$PATH:$JAVA_HOME/bin
  18.  
     
  19.  
    # 暴露端口
  20.  
    EXPOSE 8090
  21.  
    # 入口,java项目的启动命令
  22.  
    ENTRYPOINT java -jar /tmp/app.jar
学新通

案例1:

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

步骤1:新建一个空文件夹docker-demo

步骤2:拷贝课前资料的docker-demo.jar文件到docker-demo这个目录

步骤3:拷贝课前资料的jdk8.tar.gz文件到docker-demo这个目录

步骤4:拷贝课前资料提供的Dockerfile到docker-demo这个目录

步骤5:进入docker-demo

步骤6:运行命令

docker build -t javaweb:1.0 .

        记得命令末尾 空格 一个点,这个点代表Dockerfile所在的目录(构建的时候要告诉docker Dockerfile在哪)

学新通

检查并运行此镜像:

学新通

 访问页面,出现下面的页面即大功告成:

学新通

案例2:

基于java:8-alpine镜像,将一个Java项目构建为镜像

实现思路:

①新建一个空的目录,然后目录中新建一个文件,命名为Dockerfile

②拷贝课前资料提供的docker-demo.jar到这个目录中

③编写Dockerfile文件:

        a)基于java:8-alpine作为基础镜像

        b)将app.jar拷贝到镜像中

        c)暴露端口

        d)编写入口ENTRYPOINT

④使用docker build命令构建镜像

⑤使用docker run创建容器并运行

Dockerfile文件:

  1.  
    # 指定基础镜像
  2.  
    FROM java:8-alpine
  3.  
    # 拷贝
  4.  
    COPY ./docker-demo.jar /tmp/app.jar
  5.  
     
  6.  
    # 暴露端口
  7.  
    EXPOSE 8090
  8.  
    # 入口,java项目的启动命令
  9.  
    ENTRYPOINT java -jar /tmp/app.jar

注意不要随便更改暴露出来的端口,运行成功

学新通

初识DockerCompose

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

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

学新通

安装docker compose:

第一步:下载

学新通

第二步:修改文件权限

x代表给docker-compose执行权(绿色代表可以执行)

学新通

第三步让tionBash自动补全命令

学新通

总结:

Docker compose有什么作用?

        帮助我们快速部署分布式应用,无需一个个微服务去构建镜像和部署

DockerCompose部署微服务

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

①查看docker-compose:

学新通

②修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名

(用docker compose部署,所有的服务之间都可以用服务名访问)

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

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

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

因为阿里代码有问题,nacos应该第一时间部署,而后再部署其他微服务,所以需要重启gateway、orderservice、userservice

学新通

访问页面成功则配置成功:

学新通

Docker镜像仓库

镜像仓库(Docker Registry)有共有和私有两种形式:

公共仓库:例如Docker官方的Docker Hub

私有仓库:用户自己搭建

简化版镜像仓库:

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

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

学新通

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

带有图形化界面

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

学新通

第一步:配置Docker信任地址

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

学新通

第二步:配置docker-compose

学新通

配置好后执行命令:

docker-compose up -d

加载完毕后,可以看到ui界面

学新通

在私有镜像仓库推送或拉取镜像:

学新通

 推送镜像成功可以在私服看到:

学新通

 总结:

1.推送本地镜像到仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀

2.镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json文件,被docker信任

3.推送使用docker push命令

4.拉取使用docker pull命令

实用篇---第四天

学新通

一、初识MQ

同步通讯和异步通讯

同步通信相当于打视频,一次只能和一个人打

异步通信相当于聊天,一次可以和很多个人聊

学新通

同步通信的优缺点

缺点:

微服务间基于Feign的调用就属于同步方式,存在一些问题

①代码耦合严重,比如支付服务需要在加业务的时候不断修改

②耗时太长,性能下降

......

学新通

 学新通

 总结:

同步调用的优点:

        时效性强,可以立即得到结果

同步调用的问题:

        耦合度高

        性能和吞吐能力下降

        有额外的资源消耗

        有级联失败问题

异步通信的优缺点

异步调用常见实现是事件驱动模式

学新通

优点:

①服务解耦

②性能提升,吞吐量提高

③服务没有强依赖,不担心级联失败问题(故障隔离)

④流量削峰

缺点:

①依赖于Broker的可靠性、安全性、吞吐力量

②架构复杂,业务没有明显的流程线,不好追踪管理

mq常见技术介绍

MQ:Message Queue,中文是消息队列,也就是事件驱动架构中的Broker

学新通

二、RabbitMQ入门

RabbitMQ介绍和安装

RabbitMQ是基于Erlang计开发的开源消息中间件

单机部署:

第一步:下载镜像

方式一:在线拉取

docker pull rabbitmq:3-management

方式二:从本地加载

将mq.tar上传到虚拟机中,使用命令加载镜像即可:

docker load -i mq.tar

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

学新通

然后就可以进入rabbitMQ了

学新通

RabbitMQ的结构和概念:

学新通

RabbitMQ中的几个概念:

channel:操作MQ的工具

exchange:路由消息到队列中

queue:缓存消息

virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

消息模型

学新通

HelloWorld案例

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

publisher:消息发布者,将消息发送到队列queue

queue:消息队列,负责接受并缓存消息

consumer:订阅消息,处理队列中的消息

学新通

完成官方Demo中的hello world案例

①导入课前资料的mq-demo工程

②运行publisher服务中的测试类PublisherTest中的测试方法testSendMessage()

③查看RabbitMQ控制台的消息

④启动consumer服务,查看是否能接收消息

总结:

基本消息队列的消息发送流程:

1.建立connection

2.创建channel

3.利用channel声明队列

4.利用channel向队列发送消息

基本消息队列的消息接收流程:

1.建立connection

2.建立channel

3.利用channel声明队列

4.利用consumer的消费行为handleDelivery()

5.利用channel将消费者与队列绑定

三、SpringAMQP

SpringAMQP基本介绍

学新通

 特征:

        侦听器容器,用于异步处理入站消息

        用于发送和接收消息的RabbitTemplate

        RabbitAdmin用于自动声明队列、交换和绑定

基本消息队列功能实现

消息发送:

1.在父工程中引入spring-amqp的依赖

2.在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列

3.在consumer服务中编写消费逻辑,绑定simple.queue这个队列

第一步实现:

  1.  
    <dependency>
  2.  
    <groupId>org.springframework.boot</groupId>
  3.  
    <artifactId>spring-boot-starter-amqp</artifactId>
  4.  
    </dependency>

第二步实现:

①在publisher服务中编写application.yml,添加mq连接信息

  1.  
    spring:
  2.  
    rabbitmq:
  3.  
    host: 192.168.150.101
  4.  
    port: 5672
  5.  
    username: itcast
  6.  
    password: 123321
  7.  
    virtual-host: /

②在publisher服务中新建一个测试类,编写测试方法

  1.  
    @SpringBootTest
  2.  
    public class SpringAmqpTest {
  3.  
    @Autowired
  4.  
    private RabbitTemplate rabbitTemplate;
  5.  
     
  6.  
    @Test
  7.  
    public void testSendMessage2SimpleQueue(){
  8.  
    String queueName="simple.queue";
  9.  
    String message="hello,Spring AMQP!";
  10.  
    rabbitTemplate.convertAndSend(queueName,message);
  11.  
    }
  12.  
    }

总结:

什么是AMQP?

        应用间消息通信的一种协议,与语言和平台无关

SpringAMQP如何发送消息?

        ①引入amqp的starter依赖

        ②配置RabbitMQ地址

        ③利用RabbitTemplate的convertAndSend方法

消息消费:

1.在consumer服务中编写application.yml,添加mq连接信息

  1.  
    spring:
  2.  
    rabbitmq:
  3.  
    host: 192.168.202.128
  4.  
    port: 5672
  5.  
    virtual-host: /
  6.  
    username: itcast
  7.  
    password: 123321

2.在consumer服务中新建一个类,编写消费逻辑

  1.  
    @Component
  2.  
    public class SpringRabbitListener {
  3.  
     
  4.  
    @RabbitListener(queues = "simple.queue")
  5.  
    public void listenSimpleQueue(String msg){
  6.  
    System.out.println("消费者接收到simple.queue的消息【" msg "】");
  7.  
    }
  8.  
     
  9.  
     
  10.  
    }

接收消息成功:

学新通

总结:

SpringAMQP如何接收消息?

引入amqp的starter依赖

配置RabbitMQ地址

定义类,添加@Component注解

类中声明方法,添加@RabbitListener注解,方法参数接收消息

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

Work Queue 工作队列

Work Queue,工作队列,挂载两个消费者,可以提高消息处理的速度,避免队列消息堆积

学新通

 案例:模拟WorkQueue,实现一个队列绑定多个消费者

基本思路如下:

1.在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue

2.在consumer服务中定义两个消息监听者,都监听simple.queue队列

3.消费者1每秒处理50条消息,消费者2每秒处理10条消息

生产者:

  1.  
    @SpringBootTest
  2.  
    public class SpringAmqpTest {
  3.  
    @Autowired
  4.  
    private RabbitTemplate rabbitTemplate;
  5.  
     
  6.  
    @Test
  7.  
    public void testSendMessage2SimpleQueue() throws InterruptedException {
  8.  
    String queueName = "simple.queue";
  9.  
    String message = "hello,message--";
  10.  
    for (int i = 0; i < 50; i ) {
  11.  
    rabbitTemplate.convertAndSend(queueName, message i);
  12.  
    Thread.sleep(20);
  13.  
    }
  14.  
    }
  15.  
    }
学新通

消费者:

  1.  
    @Component
  2.  
    public class SpringRabbitListener {
  3.  
     
  4.  
    @RabbitListener(queues = "simple.queue")
  5.  
    public void listenWorkQueue(String msg) throws InterruptedException {
  6.  
    System.out.println("消费者1接收到simple.queue的消息【" msg "】" LocalTime.now());
  7.  
    Thread.sleep(20);
  8.  
    }
  9.  
     
  10.  
    @RabbitListener(queues = "simple.queue")
  11.  
    public void listenWorkQueue2(String msg) throws InterruptedException {
  12.  
    System.err.println("消费者2接收到simple.queue的消息【" msg "】" LocalTime.now());
  13.  
    Thread.sleep(200);
  14.  
    }
  15.  
     
  16.  
    }
学新通

因为消费预取限制,导致消息预取分配给两个cnsumer各一半。消费者1只接收奇数消息队列,消费者2只接收偶数消息队列,而消费者2的处理消息能力远低于消费者1,会拉低整体处理消息的能力

取消消费预取限制:

        修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限:

  1.  
    spring:
  2.  
    rabbitmq:
  3.  
    host: 192.168.202.128
  4.  
    port: 5672
  5.  
    virtual-host: /
  6.  
    username: itcast
  7.  
    password: 123321
  8.  
    listener:
  9.  
    simple:
  10.  
    prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息

 总结:

Work模型的使用:

        多个消费者绑定到一个队列,同一条消息只会被一个消费者处理

        通过设置prefetch来控制消费者预取的消息数量

发布订阅模型介绍

发布(publish)订阅(subscribe)模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)

常见的exchange类型包括:

Fanout:广播

Direct:路由

Topic:话题

注意:exchange负责消息路由,而不是存储,路由失败则消息丢失

学新通

Fanout Exchange

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue

案例:利用SpringAMQP演示FanoutExchange的使用

实现思路:

1.在consumer服务中,利用代码声明队列、交换机,并将两者绑定

2.在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2

3.在publisher中编写测试方法,向itcast.fanout发送消息

学新通

步骤一:

首先,SpringAMQP提供了声明交换机、队列、绑定关系的API,例如:

学新通

 其次,在consumer服务创建一个类,添加@Configuration注解,并声明FanoutExchange、Queue和绑定关系对象Binding。

代码如下:

  1.  
    @Configuration
  2.  
    public class FanoutConfig {
  3.  
    //itcast.fanout
  4.  
    @Bean
  5.  
    public FanoutExchange fanoutExchange(){
  6.  
    return new FanoutExchange("itcast.fanout");
  7.  
    }
  8.  
     
  9.  
    //fanout.queue1
  10.  
    @Bean
  11.  
    public Queue fanoutQueue1(){
  12.  
    return new Queue("itcast.queue1");
  13.  
    }
  14.  
     
  15.  
    //fanout.queue2
  16.  
    @Bean
  17.  
    public Queue fanoutQueue2(){
  18.  
    return new Queue("itcast.queue2");
  19.  
    }
  20.  
     
  21.  
    @Bean
  22.  
    public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
  23.  
    return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
  24.  
    }
  25.  
     
  26.  
    @Bean
  27.  
    public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
  28.  
    return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
  29.  
    }
  30.  
     
  31.  
    }
学新通

步骤二:

  1.  
    @Component
  2.  
    public class SpringRabbitListener {
  3.  
     
  4.  
    @RabbitListener(queues = "itcast.queue1")
  5.  
    public void listenWorkQueue(String msg) throws InterruptedException {
  6.  
    System.out.println("消费者1接收到Fanout的消息【" msg "】" LocalTime.now());
  7.  
    }
  8.  
     
  9.  
    @RabbitListener(queues = "itcast.queue2")
  10.  
    public void listenWorkQueue2(String msg) throws InterruptedException {
  11.  
    System.err.println("消费者2接收到Fanout的消息【" msg "】" LocalTime.now());
  12.  
    }
  13.  
     
  14.  
    }

步骤三:

  1.  
    @SpringBootTest
  2.  
    public class SpringAmqpTest {
  3.  
    @Autowired
  4.  
    private RabbitTemplate rabbitTemplate;
  5.  
     
  6.  
    @Test
  7.  
    public void testSendFanoutExchange(){
  8.  
    //交换机名称
  9.  
    String exchangeName="itcast.fanout";
  10.  
    //消息
  11.  
    String message="hello,everybody~";
  12.  
    //发送
  13.  
    rabbitTemplate.convertAndSend(exchangeName,"",message);
  14.  
    }
  15.  
    }
学新通

运行成功,nice

学新通

总结:

交换机的作用是什么?

        接收publisher发送的消息

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

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

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

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

        Queue

        FanoutExchange

        Binding

DirectExchange

DirectExchange会将接收到的消息根据规则路由到指定的Queue,因此成为路由模式(routes)

学新通

 案例:利用SpringAMQP演示DirectExchange的使用

实现思路如下:

1.利用@RabbitListener声明Exchange、Queue、RoutingKey

2.在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direcct.queue2

3.在publisher中编写测试方法,向itcast.direct发送消息

步骤一、二:

  1.  
    @Component
  2.  
    public class SpringRabbitListener {
  3.  
     
  4.  
    @RabbitListener(bindings=@QueueBinding(
  5.  
    value=@Queue(name="direct.queue1"),
  6.  
    exchange=@Exchange(name="itcast.direct",type= ExchangeTypes.DIRECT),
  7.  
    key={"red","blue"}
  8.  
    ))
  9.  
    public void listenDirectQueue1(String msg){
  10.  
    System.err.println("消费者接收到direct.queue1的消息【" msg "】" LocalTime.now());
  11.  
    }
  12.  
     
  13.  
    @RabbitListener(bindings = @QueueBinding(
  14.  
    value=@Queue(name="direct.queue2"),
  15.  
    exchange = @Exchange(name="itcast.direct",type="direct"),
  16.  
    key={"red","yellow"}
  17.  
    ))
  18.  
    public void listenDirectQueue2(String msg){
  19.  
    System.err.println("消费者接收到direct.queue2的消息【" msg "】" LocalTime.now());
  20.  
    }
  21.  
     
  22.  
    }
学新通

步骤三:

  1.  
    @Test
  2.  
    public void testSendDirectExchange(){
  3.  
    String exchangeName="itcast.direct";
  4.  
    String message="hello,blue";
  5.  
    rabbitTemplate.convertAndSend(exchangeName,"blue",message);
  6.  
    }

总结:

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

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

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

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

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

        @Queue

        @Exchange

 TopicExchange

TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,且以点.做分割

Queue与Exchange指定BindingKey时可以使用通配符:

#:代指0个或多个单词

*:代指一个单词

学新通

案例:利用SpringAMQP演示TopicExchange的使用

实现思路如下:

1.利用@RabbitListener声明Exchange、Queue、RoutingKey

2.在consumer服务中编写两个消费者方法,分别监听topic.queue1和topic.queue2

3.在publisher中编写测试方法,向itcast.topic发送消息

 学新通

 步骤一、二:

  1.  
     
  2.  
    @RabbitListener(bindings = @QueueBinding(
  3.  
    value=@Queue("topic.queue1"),
  4.  
    exchange = @Exchange(name="itcast.topic",type="topic"),
  5.  
    key = "china.#"
  6.  
    ))
  7.  
    public void listenTopicQueue1(String msg){
  8.  
    System.err.println("消费者接收到topic.queue1的消息【" msg "】");
  9.  
    }
  10.  
     
  11.  
    @RabbitListener(bindings = @QueueBinding(
  12.  
    value=@Queue("topic.queue2"),
  13.  
    exchange = @Exchange(name="itcast.topic",type="topic"),
  14.  
    key = "#.news"
  15.  
    ))
  16.  
    public void listenTopicQueue2(String msg){
  17.  
    System.err.println("消费者接收到topic.queue2的消息【" msg "】");
  18.  
    }
学新通

步骤三:

  1.  
    @Test
  2.  
    public void testSendTopicExchange(){
  3.  
    String exchangeName="itcast.topic";
  4.  
    String message="学it,来黑马";
  5.  
    rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
  6.  
    }

消息转换器

案例:测试发送Object类型消息

说明:在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意类型的消息,SpringAMQP会帮我们序列化为字节后发送

  1.  
    @Bean Queue objectQueue(){
  2.  
    return new Queue("object.queue");
  3.  
    }
  1.  
    public void testSendObjectQueue(){
  2.  
    Map<String,Object> msg=new HashMap<>();
  3.  
    msg.put("name","柳岩");
  4.  
    msg.put("age",21);
  5.  
    rabbitTemplate.convertAndSend("object.queue",msg);
  6.  
    }

队列中收到的消息:

学新通

===>Spring对消息对象的处理是由org.springframework.amqp.support.converter.Messaegonverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream来完成序列化

如果要修改只需要定义一个MessageConverter类型的Bean即可。推荐用JSON方式序列号。步骤如下:

消息发送:

①在publisher服务引入依赖

  1.  
    <dependency>
  2.  
    <groupId>com.fasterxml.jackson.core</groupId>
  3.  
    <artifactId>jackson-databind</artifactId>
  4.  
    </dependency>

②在publisher服务声明MessageConverter

  1.  
    @Bean
  2.  
    public MessageConverter messageConverter(){
  3.  
    return new Jackson2JsonMessageConverter();
  4.  
    }

运行结果:

学新通

消息接收:

①引入jackson依赖

②在consumer服务中定义MessageConverter

③定义一个消费者,监听object.queue队列并消费消息

  1.  
    @RabbitListener(queues="object.queue")
  2.  
    public void listenObejctQueue(Map<String,Object> msg){
  3.  
    System.out.println("收到消息:【" msg "】");
  4.  
    }

总结:

SpringAMQP中的消息序列化和反序列化是怎么实现的?

        利用MessageConverter实现的,默认是JDK的序列化

        注意发送方与接收方必须使用相同的MessageConverter

实用篇---第五天

分布式搜索---elasticsearch

学新通

一、初识elasticsearch

什么是elasticsearch?

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

elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域

elasticsearch是elastic stack的核心,负责存储、搜索和分析数据

学新通

elasticsearch底层使用了Lucene技术

Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发

Lucene的优势:

易扩展

高性能(基于倒排索引)

Lucene的缺点:

只限于Java语言开发

学习曲线陡峭

不支持水平扩展

elasticsearch的发展:

2004年Shay Banon基于Lucene开发了Compass

2010年Shay Banon重写了Compass,取名为Elasticsearch

相比于Lucene,elasticsearch具备下列优势:

        支持分布式,可水平扩展

        提供Restful接口,可悲任何语言调用

学新通

 总结:

学新通

 正向索引和倒排索引

传统数据库(如MySQL)采用正向索引

 elasticsearch采用倒排索引:

        文档(document):每条数据就是一个文档

        词条(term):文档按照语义分成的词语

学新通

正向索引:基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条

倒排索引:对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取文档

es和mysql概念对比

文档:

elasticsearch是面向文档存储的,可以是数据库中的一条商品数据、一个订单信息

文档数据会被序列化为json格式后存储在elasticsearch中

学新通

索引:

索引(index):是相同类型文档的集合

映射(mapping):索引中文档的字段约束信息,类似表的结构约束

学新通

概念对比:

学新通

架构

MySQL:擅长事务类型操作,可以确保数据的安全和一致性

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

学新通

 总结:

文档:一条数据就是一个文档,es中是json格式

字段:json文档中的字段

索引:同类型文档的集合

映射:索引中文档的约束,比如字段名称、类型

elasticsearch与数据库的关系:

数据库负责事务类型操作

elasticsearch负责海量数据的搜索、分析和计算

安装es

第一步:创建网络

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

docker network create es-net

第二步:将es.tar加载:

docker load -i es.tar

kibana的tar包也需要这样做

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

学新通

 只要看到下面的界面,就代表部署es成功:

学新通

安装kibana

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

部署:

运行docker命令,部署kibana

学新通

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

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

注意:kibana和elasticsearch的版本必须严格一致,并在同一网络中

看到下面这个界面代表部署kibana成功:

学新通

安装ik分词器

es在创建倒排索引时需要对文档分词

在搜索时,需要对用户输入内容分词。但默认的分词规则则对中文处理并不友好。

我们可以在kibana的DevTools中测试:

  1.  
    POST /_analyze
  2.  
    {
  3.  
    "analyzer": "standard",
  4.  
    "text": "黑马程序员学习java也太棒了吧!"
  5.  
    }

语法说明:

POST:请求方式

/_analyze:请求路径,这里省略了http://192.168.150.101:9200,有kibana帮我们补充

请求参数,json风格:

        analyzer:分词器类型,这里默认是standard

        text:要分词的内容

分词结果:

学新通

处理中文分词,一般会使用lk分词器

安装ik:

第一步:查看数据卷目录

安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:

学新通

 第二步:把课前资料中的lk分词器解压缩,重命名为ik

学新通

 第三步:上传到es容器的插件数据卷中

也就是/var/lib/docker/volumes/es-plugins/_data

学新通

第四步:重启容器

docker restart es

第五步:测试

ik分词器包含两种模式:

ik_smart:最少切分

ik_max_word:最细切分

  1.  
    POST /_analyze
  2.  
    {
  3.  
    "analyzer": "ik_smart",
  4.  
    "text": "黑马程序员学习java也太棒了吧!"
  5.  
    }

分词效果:

学新通

ik分词器的扩展和停用词典

测试:

  1.  
     
  2.  
    POST /_analyze
  3.  
    {
  4.  
    "analyzer":"ik_smart",
  5.  
    "text":"传智播客的课程可白嫖"
  6.  
    }

可以看到ik无法对所有词语完美分词:

学新通

扩展词库:

学新通

 停用词库:

学新通

 进入文件,可以看见:

学新通

 entry标签里填入文件名,直接进入对应的文件添加内容即可

学新通

重启es:

docker restart es

测试:

  1.  
     
  2.  
    POST /_analyze
  3.  
    {
  4.  
    "analyzer":"ik_smart",
  5.  
    "text":"传智播客的课程可白嫖了"
  6.  
    }

学新通

总结:

分词器的作用是什么?

        创建倒排索引时对文档分词

        用户搜索时,对输入的内容分词

ik分词器有几种模式?

        ik_smart:只能切分,粗粒度

        ik_max_word:最细切分,细粒度

ik分词器如何扩展词条?如何停用词条?

        利用config目录的ikAnalyzer.cfg.xml文件添加扩展词典和停用词典

        在词典中添加扩展词条或者停用词条

二、索引库操作

mapping映射属性

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

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

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

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

        布尔:boolean

        日期:date

        对象:object

注意,在es中没有数组这个类型,但是允许某一种类型的字段有多个值,所以这些都可以作为数组

index:是否创建索引,默认为true。只有index为true,才会创建倒排索引,如果index设为false,则不参与倒排索引

analyzer:使用哪种分词器

properties:该字段的子字段

学新通

总结:

学新通

创建索引库

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

创建索引库和mapping的DSL语法如下:

学新通

 实操定义DSL语句:

  1.  
    PUT /heima
  2.  
    {
  3.  
    "mappings":{
  4.  
    "properties":{
  5.  
    "info":{
  6.  
    "type":"text",
  7.  
    "analyzer":"ik_smart"
  8.  
    },
  9.  
    "email":{
  10.  
    "type":"keyword",
  11.  
    "index":false
  12.  
    },
  13.  
    "name":{
  14.  
    "type":"object",
  15.  
    "properties": {
  16.  
    "firstName":{
  17.  
    "type":"keyword"
  18.  
    },
  19.  
    "lastName":{
  20.  
    "type":"keyword"
  21.  
    }
  22.  
    }
  23.  
    }
  24.  
    }
  25.  
    }
  26.  
    }
学新通

创建成功:

学新通

查看、删除、修改索引库

查看索引库语法:

GET /索引库名

删除索引库语法:

DELETE /索引库名

修改索引库:

索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下:

  1.  
    PUT /heima/_mapping
  2.  
    {
  3.  
    "properties":{
  4.  
    "age":{
  5.  
    "type":"integer"
  6.  
    }
  7.  
    }
  8.  
    }

总结:

索引库操作有哪些?

        创建索引库:PUT/索引库名

        查询索引库:GET/索引库名

        删除索引库:DELETE/索引库名

        添加字段:PUT/索引库名/_mapping

三、文档操作

新增、查询、删除文档

新增文档的DSL语法:

学新通

 插入一个文档:

  1.  
    POST /heima/_doc/1
  2.  
    {
  3.  
    "name":{
  4.  
    "firstName":"信",
  5.  
    "lastName":"李"
  6.  
    },
  7.  
    "info":"人送外号峡谷拆迁队队长",
  8.  
    "email":"123@qq.com"
  9.  
    }

查看文档:

语法:GET /索引库名/_doc/文档id

示例:

GET /heima/_doc/1

删除文档:

语法:DELETE 索引库名/_doc/文档id

示例:

DELETE /heima/_doc/1

修改文档

方式一:全量修改,会删除旧文档,添加新文档

其实这种方式既可以做新增又可以做修改

学新通

 方式二:

增量修改,修改制定的字段值

学新通

 可以修改已经有的字段:

  1.  
    POST /heima/_update/1
  2.  
    {
  3.  
    "doc":{
  4.  
    "email":"lixin@qq.com"
  5.  
    }
  6.  
    }

也可以添加没有的字段:

  1.  
    POST /heima/_update/1
  2.  
    {
  3.  
    "doc":{
  4.  
    "gender":"男"
  5.  
    }
  6.  
    }

此时查看索引结构,可以发现gender字段被创建出来了

学新通

总结:

学新通

四、RestClient操作索引库

导入demo

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

案例:利用JavaRestClient实现创建、删除索引库,判断索引库是否存在

学新通

hotel数据结构分析

mapping要考虑的问题:

字段名、数据类型、是否参与搜索、是否分词、如果分词,分词器是什么?

学新通学新通

 定义hotel:

  1.  
    PUT /hotel
  2.  
    {
  3.  
    "mappings":{
  4.  
    "properties":{
  5.  
    "id":{
  6.  
    "type":"keyword"
  7.  
    },
  8.  
    "name":{
  9.  
    "type":"text",
  10.  
    "analyzer":"ik_max_word",
  11.  
    "copy_to":"all"
  12.  
    },
  13.  
    "address":{
  14.  
    "type":"keyword",
  15.  
    "index":false
  16.  
    },
  17.  
    "price":{
  18.  
    "type":"integer"
  19.  
    },
  20.  
    "score":{
  21.  
    "type":"integer"
  22.  
    },
  23.  
    "brand":{
  24.  
    "type":"keyword",
  25.  
    "copy_to":"all"
  26.  
    },
  27.  
    "city":{
  28.  
    "type":"keyword"
  29.  
    },
  30.  
    "star_name":{
  31.  
    "type":"keyword"
  32.  
    },
  33.  
    "business":{
  34.  
    "type":"keyword",
  35.  
    "copy_to": "all"
  36.  
    },
  37.  
    "location":{
  38.  
    "type":"geo_point"
  39.  
    },
  40.  
    "pic":{
  41.  
    "type":"keyword",
  42.  
    "index":false
  43.  
    },
  44.  
    "all":{
  45.  
    "type":"text",
  46.  
    "analyzer":"ik_max_word"
  47.  
    }
  48.  
     
  49.  
    }
  50.  
    }
学新通

初始化RestClient

1.引入es的RestHighLevelClient依赖

  1.  
    <dependency>
  2.  
    <groupId>org.elasticsearch.client</groupId>
  3.  
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
  4.  
    <version>7.12.1</version>
  5.  
    </dependency>

2.因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本

        <elasticsearch.version>7.12.1</elasticsearch.version>

3.初始化RestHighLevelClient

  1.  
    public class HotelIndexTest {
  2.  
    private RestHighLevelClient client;
  3.  
     
  4.  
    @BeforeEach
  5.  
    void setUp(){
  6.  
    this.client=new RestHighLevelClient(RestClient.builder(
  7.  
    HttpHost.create("http://192.168.202.128:9200")
  8.  
    ));
  9.  
    }
  10.  
     
  11.  
    @AfterEach
  12.  
    void testDown() throws IOException {
  13.  
    this.client.close();
  14.  
    }
  15.  
    }
学新通

创建索引库

学新通

删除和判断数据库

 删除索引库:

  1.  
     
  2.  
    @Test
  3.  
    void testDeleteHotelIndex() throws IOException {
  4.  
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
  5.  
    client.indices().delete(request,RequestOptions.DEFAULT);
  6.  
    }

判断索引库是否存在

  1.  
    @Test
  2.  
    void testExistHotelIndex() throws IOException {
  3.  
    GetIndexRequest request = new GetIndexRequest("hotel");
  4.  
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
  5.  
    System.err.println(exists?"索引库已经存在!":"索引库不存在!");
  6.  
    }

总结:

索引库操作的基本步骤:

①初始化RestHighLevelClient

②创建XxxIndexRequest。Xxx是Create、Get、Delete

③准备DSL(Create时需要)

④发送请求。调用RestHighLevelClient的indices().xxx()方法

xxx是create、exists、delete

五、RestClient操作文档

学新通

新增文档

第一步:初始化JavaRestClient

新建一个测试类,实现文档相关操作,并且完成JavaestClient的初始化

第二步:添加酒店数据到索引库

先查询酒店数据,然后给这条数据创建倒排索引,即可完成添加

  1.  
    public class HotelDocumentTest {
  2.  
     
  3.  
    private RestHighLevelClient client;
  4.  
     
  5.  
    @BeforeEach
  6.  
    void setUp(){
  7.  
    this.client=new RestHighLevelClient(RestClient.builder(
  8.  
    HttpHost.create("192.168.202.128:9200")
  9.  
    ));
  10.  
    }
  11.  
     
  12.  
    @AfterEach
  13.  
    void testDown() throws IOException {
  14.  
    this.client.close();
  15.  
    }
  16.  
    }
学新通

给测试类添加@SpringBootTest注解,可以自动注入

  1.  
    @Autowired
  2.  
    private IHotelService hotelService;
  3.  
    @Test
  4.  
    void testAddDocument() throws IOException {
  5.  
    //根据id查询酒店数据
  6.  
    Hotel hotel = hotelService.getById(61083L);
  7.  
    HotelDoc hotelDoc = new HotelDoc(hotel);
  8.  
    //1.准备Request对象
  9.  
    IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
  10.  
    //2.准备Json文档
  11.  
    request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
  12.  
    //3.发送请求
  13.  
    client.index(request, RequestOptions.DEFAULT);
  14.  
    }

注:因为我们准备的实体类和索引库的实体类有差异,所以需要为索引库数据专门准备一个转变的实体类

  1.  
    @Data
  2.  
    @NoArgsConstructor
  3.  
    public class HotelDoc {
  4.  
    private Long id;
  5.  
    private String name;
  6.  
    private String address;
  7.  
    private Integer price;
  8.  
    private Integer score;
  9.  
    private String brand;
  10.  
    private String city;
  11.  
    private String starName;
  12.  
    private String business;
  13.  
    private String location;
  14.  
    private String pic;
  15.  
     
  16.  
    public HotelDoc(Hotel hotel) {
  17.  
    this.id = hotel.getId();
  18.  
    this.name = hotel.getName();
  19.  
    this.address = hotel.getAddress();
  20.  
    this.price = hotel.getPrice();
  21.  
    this.score = hotel.getScore();
  22.  
    this.brand = hotel.getBrand();
  23.  
    this.city = hotel.getCity();
  24.  
    this.starName = hotel.getStarName();
  25.  
    this.business = hotel.getBusiness();
  26.  
    this.location = hotel.getLatitude() ", " hotel.getLongitude();
  27.  
    this.pic = hotel.getPic();
  28.  
    }
  29.  
    }
学新通

在索引库中查询

GET /hotel/_doc/61083

学新通

查询文档

  1.  
    @Test
  2.  
    void testGetDocumentById() throws IOException {
  3.  
    //1.准备Request
  4.  
    GetRequest request = new GetRequest("hotel", "61083");
  5.  
    //2.发送请求,得到响应
  6.  
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
  7.  
    //3.解析响应结果
  8.  
    String json = response.getSourceAsString();
  9.  
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  10.  
    System.out.println(hotelDoc);
  11.  
    }

运行结果:

学新通

更新文档

修改文档数据有两种方式:

方式一:全量更新。再次写入id一样的文档,就会删除旧文档,添加新文档

方式二:局部更新。只更新部分字段

我们演示方式二:

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

可以看到price和starName修改成功:

学新通

删除文档

  1.  
    @Test
  2.  
    void testDeleteDocument() throws IOException {
  3.  
    DeleteRequest request = new DeleteRequest("hotel", "61083");
  4.  
    client.delete(request,RequestOptions.DEFAULT);
  5.  
    }

总结:

文档操作的基本步骤:

初始化RestHighLevelClient

初始化XxxRequest。Xxx是Index、Get、Update、Delete

准备参数(Index和Update时需要)

发送请求。调用RestHighLevelClient的xxx()方法

        xxx是index、get、update、delete

批量导入文档

需求:批量查询酒店数据,然后批量导入索引库中

思路:

1.利用mybatis-plus查询酒店数据

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

3.利用JavaRestClient中的Bulk批处理,实现批量新增文档

  1.  
    @Test
  2.  
    void testBulkRequest() throws IOException {
  3.  
    //批量查询酒店数据
  4.  
    List<Hotel> hotels = hotelService.list();
  5.  
    //1.创建Request
  6.  
    BulkRequest request = new BulkRequest();
  7.  
    for(Hotel hotel:hotels){
  8.  
    HotelDoc hotelDoc = new HotelDoc(hotel);
  9.  
    //2.准备参数,添加多个新增的Request
  10.  
    request.add(new IndexRequest("hotel").id(hotelDoc.getId().toString()).source(JSON.toJSONString(hotelDoc),XContentType.JSON));
  11.  
    }
  12.  
    //3.发送请求
  13.  
    client.bulk(request,RequestOptions.DEFAULT);
  14.  
    }

可以用DSL语句:GET /hotel/_search        对批量导入功能进行验证

学新通

六、分布式搜索引擎

学新通

 DSL查询文档

Elasticsearch提供了基于JSON的DSL来定义查询。常见的查询类型包括:

查询所有:查询出所有数据,一般测试用。例如:match_all

全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:

        match_query

        multi_match_query

精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段

        ids

        range

        term

地理(geo)查询:根据经纬度查询。例如:

        geo_distance

        geo_bounding_box

复合(compound)查询:复合查询可以将上述各种条件组合起来,合并查询条件。例如:

        bool

        function_score

学新通

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "match_all":{
  5.  
     
  6.  
    }
  7.  
    }
  8.  
    }

查询DSL的基本语法是什么?

        GET  /索引库名/_search

        {

                “query”:{

                        “查询类型”:{

                                “FIELD”:“TEXT”

                        }

                }

        }

全文检索查询

全文检索查询,会对用户输入内容分词,常用于搜索框搜索:

学新通

 match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索。

语法:

学新通

比如查询如家外滩,首先会对“如家外滩”分词,匹配度越高,得分越高,越靠前

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "match":{
  5.  
    "all":"外滩如家"
  6.  
    }
  7.  
    }
  8.  
    }

multi_match:与match查询类似,只不过允许同时查询多个字段

学新通

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query": {
  4.  
    "multi_match": {
  5.  
    "query": "外滩如家",
  6.  
    "fields": ["brand","name","business"]
  7.  
    }
  8.  
    }
  9.  
    }

总结:

match和multi_match的区别是什么?

        match:根据一个字段查询

        multi_match:根据多个字段查询,参与查询的字段越多,性能就越差

精确查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:

        term:根据词条精确值查询

        range:根据值的范围查询

学新通

term查询:

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "term":{
  5.  
    "city":{
  6.  
    "value":"上海"
  7.  
    }
  8.  
    }
  9.  
    }
  10.  
    }

range查询:

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query": {
  4.  
    "range":{
  5.  
    "price":{
  6.  
    "gte":100,
  7.  
    "lte":300
  8.  
    }
  9.  
    }
  10.  
    }
  11.  
    }

地理查询

学新通

geo_bounding_box:查询geo_point值落在某个矩形范围的所有文档

学新通

geo_distance:查询到指定中心点小于某个距离值的所有文档

学新通

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "geo_distance":{
  5.  
    "distance":"15km",
  6.  
    "location":"31.21,121.5"
  7.  
    }
  8.  
    }
  9.  
    }

相关性打分算法

复合(compound)查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑:

例如:

        function score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价

相关性算分:

        当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列

学新通

 总结:

学新通

Function Score Query

 学新通

 案例:给“如家”这个品牌的酒店排名靠前一点

==>翻译来说,function score需要的三要素:

1.哪些文档需要算分加权?

        品牌为如家的酒店

2.算分函数是什么?

        weight

3.加权模式是什么?

        求和

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "function_score":{
  5.  
    "query":{
  6.  
    "match":{
  7.  
    "all":"外滩"
  8.  
    }
  9.  
    },
  10.  
    "functions":[
  11.  
    {
  12.  
    "filter":{
  13.  
    "term":{
  14.  
    "brand":"如家"
  15.  
    }
  16.  
    },
  17.  
    "weight":10
  18.  
    }
  19.  
    ],
  20.  
    "boost_mode":"sum"
  21.  
    }
  22.  
    }
  23.  
    }
学新通

function score query定义的三要素是什么?

        过滤条件:哪些文档要加分

        算分函数:如何计算function score

        加权方式:function score与query score如何运算

Boolean Query

布尔查询是一个或多个查询子句的组合。子查询的组合方式有:

must:必须匹配每个子查询,类似“与”

should:选择性匹配子查询,类似“或”

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

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

案例:利用bool查询实现功能

需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "bool":{
  5.  
    "must": [
  6.  
    {
  7.  
    "match": {
  8.  
    "name":"如家"
  9.  
    }
  10.  
    }
  11.  
    ],
  12.  
    "must_not":[
  13.  
    {
  14.  
    "range":{
  15.  
    "price":{
  16.  
    "gt":400
  17.  
    }
  18.  
    }
  19.  
    }
  20.  
    ],
  21.  
    "filter":[
  22.  
    {
  23.  
    "geo_distance":{
  24.  
    "distance":"10km",
  25.  
    "location":{
  26.  
    "lat": 31.21,
  27.  
    "lon":121.5
  28.  
    }
  29.  
    }
  30.  
    }
  31.  
    ]
  32.  
    }
  33.  
    }
  34.  
    }
学新通

二、搜索结果处理

排序

elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序的字段类型由:keyword类型、数值类型、地理坐标类型、日期类型等

学新通

 案例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "match_all":{}
  5.  
    },
  6.  
    "sort":[
  7.  
    {
  8.  
    "score":"desc"
  9.  
    },
  10.  
    {
  11.  
    "price":"asc"
  12.  
    }
  13.  
    ]
  14.  
    }

案例:实现对酒店数据按照你的位置坐标的距离升序排序

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "match_all":{}
  5.  
    },
  6.  
    "sort":[
  7.  
    {
  8.  
    "_geo_distance": {
  9.  
    "location": {
  10.  
    "lat": 31.034661,
  11.  
    "lon": 121.612282
  12.  
    },
  13.  
    "order": "asc",
  14.  
    "unit":"km"
  15.  
    }
  16.  
    }
  17.  
    ]
  18.  
    }
学新通

分页

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

elasticsearch中通过from、size参数来控制要返回的分页结果:

  1.  
     
  2.  
    GET /hotel/_search
  3.  
    {
  4.  
    "query":{
  5.  
    "match_all": {}
  6.  
    },
  7.  
    "sort":[
  8.  
    {
  9.  
    "price":"asc"
  10.  
    }
  11.  
    ],
  12.  
    "from":2,
  13.  
    "size":10
  14.  
     
  15.  
    }
学新通

学新通

深度分页问题:

ES是分布式的,所以会面临深度分页问题。例如按price排序后,获取from=990,size=10的数据:

学新通

 针对深度分页,ES提供了两种解决方案:

search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐。

scroll:原理是将排序数据形成快照,保存在内存。官方已经不推荐使用。

 总结:

学新通

高亮

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

原理:

        将搜索结果中的关键字用标签标记出来

        在页面中给标签添加css样式

注意:高亮查询,默认情况下ES搜索字段必须和高亮字段一致

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "match":{
  5.  
    "all":"如家"
  6.  
    }
  7.  
    },
  8.  
    "highlight": {
  9.  
    "fields":{
  10.  
    "name":{
  11.  
    "require_field_match":"false"
  12.  
    }
  13.  
    }
  14.  
    }
  15.  
    }
学新通

 学新通

总结:

学新通

三、RestClient查询文档

快速入门

  1.  
    @Test
  2.  
    void testMatchAll() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    request.source().query(QueryBuilders.matchAllQuery());
  5.  
    SearchResponse search = client.search(request, RequestOptions.DEFAULT);
  6.  
    System.out.println(search);
  7.  
    }

解析拿到的结果:

学新通

 总代码:

  1.  
    @Test
  2.  
    void testMatchAll() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    request.source().query(QueryBuilders.matchAllQuery());
  5.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  6.  
    SearchHits searchHits = response.getHits();
  7.  
    long total = searchHits.getTotalHits().value;
  8.  
    System.out.println("共搜索到" total "条数据");
  9.  
    SearchHit[] hits = searchHits.getHits();
  10.  
    for(SearchHit hit:searchHits){
  11.  
    String json = hit.getSourceAsString();
  12.  
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  13.  
    System.out.println(hotelDoc);
  14.  
    }
  15.  
     
  16.  
    }
学新通

总结:

学新通

总结:

查询的基本步骤是:

1.创建SearchRequest对象

2.准备Request.source(),也就是DSL

        ①QueryBuilders来构建查询条件

        ②传入Request.source()的query()方法

3.发送请求,得到结果

4.解析结果(参考JSON结果,从外到内,逐层解析)

match、term、range、bool查询

match语句只需修改QueryBuilders的方法

        request.source().query(QueryBuilders.matchQuery("all","如家"));

term、range、bool查询:

学新通

学新通

  1.  
    @Test
  2.  
    void testBool() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  5.  
    boolQuery.must(QueryBuilders.termQuery("city","上海"));
  6.  
    boolQuery.filter(QueryBuilders.rangeQuery("price").gte(50).lte(250));
  7.  
    request.source().query(boolQuery);
  8.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  9.  
    handleResponse(response);
  10.  
    }

排序和分页

学新通

  1.  
    @Test
  2.  
    void testPageAndSort() throws IOException {
  3.  
    //定义页码、每页大小
  4.  
    int page=1,size=5;
  5.  
    SearchRequest request = new SearchRequest("hotel");
  6.  
    request.source().query(QueryBuilders.matchAllQuery());
  7.  
    //排序 sort
  8.  
    request.source().sort("price", SortOrder.ASC);
  9.  
    //分页 from、size
  10.  
    request.source().from((page-1)*size).size(size);
  11.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  12.  
    handleResponse(response);
  13.  
    }

高亮

学新通

  1.  
    @Test
  2.  
    void testHighLight() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    //准备query
  5.  
    request.source().query(QueryBuilders.matchQuery("name","如家"));
  6.  
    //高亮
  7.  
    request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
  8.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  9.  
    handleResponse(response);
  10.  
    }

学新通

  1.  
    private void handleResponse(SearchResponse response){
  2.  
    SearchHits searchHits = response.getHits();
  3.  
    long total = searchHits.getTotalHits().value;
  4.  
    System.out.println("共搜索到" total "条数据");
  5.  
    SearchHit[] hits = searchHits.getHits();
  6.  
    for(SearchHit hit:hits){
  7.  
    //获取文档source
  8.  
    String json = hit.getSourceAsString();
  9.  
    //反序列化
  10.  
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  11.  
    //获取高亮结果
  12.  
    Map<String, HighlightField> highlightFields = hit.getHighlightFields();
  13.  
    //根据字段名获取高亮结果
  14.  
    if(!CollectionUtils.isEmpty(highlightFields)) {
  15.  
    HighlightField highlightField = highlightFields.get("name");
  16.  
    //获取高亮值
  17.  
    String name = highlightField.getFragments()[0].string();
  18.  
    //覆盖非高亮结果
  19.  
    hotelDoc.setName(name);
  20.  
    }
  21.  
    System.out.println(hotelDoc);
  22.  
    }
  23.  
    }
学新通

总结:

所有搜索DSL的构建,记住一个API:

        SearchRequest的source()方法

高亮结果解析是参考JSON结果,逐层解析

四、黑马旅游案例

酒店搜索和分页

案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页

先实现其中的关键字搜索功能,实现步骤如下:

1.实现实体类、接收前端请求

  1.  
    @Data
  2.  
    public class RequestParams {
  3.  
    private String key;
  4.  
    private Integer page;
  5.  
    private Integer size;
  6.  
    private String sortBy;
  7.  
    }

2.定义controller接口,接受页面请求,调用IHostServicce的search方法

请求方式:Post

请求路径:/hotel/list

请求参数:对象,类型为RequestParam

返回值:PageResult,包含两个属性:

        ①Long total:总条数

        ②List<HotelDoc> hotels:酒店数据

  1.  
    @Data
  2.  
    @NoArgsConstructor
  3.  
    @AllArgsConstructor
  4.  
    public class PageResult {
  5.  
    private Long total;
  6.  
    private List<HotelDoc> hotels;
  7.  
     
  8.  
    }
  1.  
    public interface IHotelService extends IService<Hotel> {
  2.  
    PageResult search(RequestParams params);
  3.  
    }
  1.  
    @MapperScan("cn.itcast.hotel.mapper")
  2.  
    @SpringBootApplication
  3.  
    public class HotelDemoApplication {
  4.  
     
  5.  
    public static void main(String[] args) {
  6.  
    SpringApplication.run(HotelDemoApplication.class, args);
  7.  
    }
  8.  
     
  9.  
    @Bean
  10.  
    public RestHighLevelClient client(){
  11.  
    return new RestHighLevelClient(RestClient.builder(
  12.  
    HttpHost.create("http://192.168.202.128:9200")
  13.  
    ));
  14.  
    }
  15.  
    }
学新通
  1.  
    @Service
  2.  
    public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
  3.  
    @Autowired
  4.  
    RestHighLevelClient client;
  5.  
     
  6.  
    @Override
  7.  
    public PageResult search(RequestParams params) {
  8.  
    try {
  9.  
    SearchRequest request = new SearchRequest("hotel");
  10.  
    String key=params.getKey();
  11.  
    if(key==null || "".equals(key))
  12.  
    request.source().query(QueryBuilders.matchAllQuery());
  13.  
    else
  14.  
    request.source().query(QueryBuilders.matchQuery("all",key));
  15.  
    int page=params.getPage();
  16.  
    int size=params.getSize();
  17.  
    request.source().from((page-1)*size).size(size);
  18.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  19.  
    return handleResponse(response);
  20.  
    } catch (IOException e) {
  21.  
    throw new RuntimeException(e);
  22.  
    }
  23.  
     
  24.  
    }
  25.  
     
  26.  
    private PageResult handleResponse(SearchResponse response){
  27.  
    SearchHits searchHits = response.getHits();
  28.  
    long total = searchHits.getTotalHits().value;
  29.  
    SearchHit[] hits = searchHits.getHits();
  30.  
    List<HotelDoc> hotels=new ArrayList<>();
  31.  
    for(SearchHit hit:hits){
  32.  
    String json = hit.getSourceAsString();
  33.  
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  34.  
    hotels.add(hotelDoc);
  35.  
    }
  36.  
    return new PageResult(total,hotels);
  37.  
    }
  38.  
    }
学新通

3.定义IHotelService中的search方法,利用match查询实现根据关键字搜索酒店的信息

添加品牌、城市、星级、价格等过滤功能

步骤:

1.修改RequestParams类,添加brand、city、starName、minPrice、maxPrice等参数

  1.  
    @Data
  2.  
    public class RequestParams {
  3.  
    private String key;
  4.  
    private Integer page;
  5.  
    private Integer size;
  6.  
    private String sortBy;
  7.  
    private String city;
  8.  
    private String brand;
  9.  
    private String starName;
  10.  
    private Integer minPrice;
  11.  
    private Integer maxPrice;
  12.  
    }

2.修改search方法的实现,在关键字搜索时,如果brand等参数存在,对其做过滤

过滤条件包括:

city精确匹配

brand精确匹配

starName精确匹配

price范围过滤

注意事项:

多个条件之间是AND关系,组合多条件用BooleanQuery

参数存在才需要过滤,做好非空判断

初始代码:

  1.  
    @Override
  2.  
    public PageResult search(RequestParams params) {
  3.  
    try {
  4.  
    SearchRequest request = new SearchRequest("hotel");
  5.  
    // 构建BooleanQuery
  6.  
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  7.  
    //关键字搜索
  8.  
    String key=params.getKey();
  9.  
    if(key==null || "".equals(key))
  10.  
    boolQuery.must(QueryBuilders.matchAllQuery());
  11.  
    else
  12.  
    boolQuery.must(QueryBuilders.matchQuery("all",key));
  13.  
    //条件过滤
  14.  
    //城市条件
  15.  
    if(params.getCity()!=null && !"".equals(params.getCity())){
  16.  
    boolQuery.filter(QueryBuilders.termQuery("city",params.getCity()));
  17.  
    }
  18.  
    //品牌条件
  19.  
    if(params.getBrand()!=null && !params.getBrand().equals("")){
  20.  
    boolQuery.filter(QueryBuilders.termQuery("brand",params.getBrand()));
  21.  
    }
  22.  
    //星级条件
  23.  
    if(params.getStarName()!=null && !params.getStarName().equals("")){
  24.  
    boolQuery.filter(QueryBuilders.termQuery("starName",params.getStarName()));
  25.  
    }
  26.  
    if(params.getMaxPrice()!=null && params.getMinPrice()!=null){
  27.  
    boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
  28.  
    }
  29.  
    request.source().query(boolQuery);
  30.  
     
  31.  
    int page=params.getPage();
  32.  
    int size=params.getSize();
  33.  
    request.source().from((page-1)*size).size(size);
  34.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  35.  
    return handleResponse(response);
  36.  
    } catch (IOException e) {
  37.  
    throw new RuntimeException(e);
  38.  
    }
  39.  
     
  40.  
    }
学新通

可以按快捷键 ctrl alt m,快速重构代码,抽取冗余部分

  1.  
    @Override
  2.  
    public PageResult search(RequestParams params) {
  3.  
    try {
  4.  
    SearchRequest request = new SearchRequest("hotel");
  5.  
    buildBasicQuery(params,request);
  6.  
     
  7.  
    int page=params.getPage();
  8.  
    int size=params.getSize();
  9.  
    request.source().from((page-1)*size).size(size);
  10.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  11.  
    return handleResponse(response);
  12.  
    } catch (IOException e) {
  13.  
    throw new RuntimeException(e);
  14.  
    }
  15.  
     
  16.  
    }
  17.  
     
  18.  
    private void buildBasicQuery(RequestParams params,SearchRequest request) {
  19.  
    // 构建BooleanQuery
  20.  
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  21.  
    //关键字搜索
  22.  
    String key= params.getKey();
  23.  
    if(key==null || "".equals(key))
  24.  
    boolQuery.must(QueryBuilders.matchAllQuery());
  25.  
    else
  26.  
    boolQuery.must(QueryBuilders.matchQuery("all",key));
  27.  
    //条件过滤
  28.  
    //城市条件
  29.  
    if(params.getCity()!=null && !"".equals(params.getCity())){
  30.  
    boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
  31.  
    }
  32.  
    //品牌条件
  33.  
    if(params.getBrand()!=null && !params.getBrand().equals("")){
  34.  
    boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
  35.  
    }
  36.  
    //星级条件
  37.  
    if(params.getStarName()!=null && !params.getStarName().equals("")){
  38.  
    boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
  39.  
    }
  40.  
    if(params.getMaxPrice()!=null && params.getMinPrice()!=null){
  41.  
    boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
  42.  
    }
  43.  
    request.source().query(boolQuery);
  44.  
    }
学新通

附近的酒店

前端页面点击定位后,会将你所在的位置发送给后台:

学新通

 步骤:

学新通

1.修改RequestParams参数,接收location字段

2.修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能

  1.  
    //排序
  2.  
    String location = params.getLocation();
  3.  
    if(location != null && location.equals("")){
  4.  
    request.source().sort(SortBuilders
  5.  
    .geoDistanceSort("location",new GeoPoint(location))
  6.  
    .order(SortOrder.ASC)
  7.  
    .unit(DistanceUnit.KILOMETERS)
  8.  
    );
  9.  
    }

处理定位结果:

1.给HotelDoc添加distance字段

    private Object distance;

2.在handleResponse方法中添加如下代码(给hotelDoc注入distance字段):

  1.  
    for(SearchHit hit:hits){
  2.  
    String json = hit.getSourceAsString();
  3.  
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  4.  
    Object[] sortValues = hit.getSortValues();
  5.  
    if(sortValues.length>0){
  6.  
    Object sortValue = sortValues[0];
  7.  
    hotelDoc.setDistance(sortValue);
  8.  
    }
  9.  
    hotels.add(hotelDoc);

广告置顶

让指定的酒店在搜索结果中排名置顶

我们给需要置顶的酒店文档添加一个标记,然后利用function score给带有标记的文档增加权重

学新通

步骤二:

  1.  
     
  2.  
    POST /hotel/_update/60935
  3.  
    {
  4.  
    "doc":{
  5.  
    "isAD":true
  6.  
    }
  7.  
    }
  8.  
     
  9.  
    POST /hotel/_update/309208
  10.  
    {
  11.  
    "doc":{
  12.  
    "isAD":true
  13.  
    }
  14.  
    }

第三步:

学新通

  1.  
    //算分控制
  2.  
    FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
  3.  
    new FunctionScoreQueryBuilder.FilterFunctionBuilder(
  4.  
    QueryBuilders.termQuery("isAD", true),
  5.  
    ScoreFunctionBuilders.weightFactorFunction(10)
  6.  
    )
  7.  
    });
  8.  
    request.source().query(functionScoreQuery);

广告标识效果:

学新通

实用篇---第七天

学新通

一、数据聚合

聚合的分类

聚合(aggregations)可以实现对文档数据的统计、分析、运算。聚合常见的有三类:

桶聚合:用来对文档做分组

        TermAggregation:按照文档字段值分组

        Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

度量(Metric)聚合:用来计算一些值,比如:最大值、最小值、平均值等

        Avg:求平均值

        Max:求最大值

        Min:求最小值

        Stats:同时求max、min、avg、sum等

管道(pipeline)聚合:其他聚合的结果为聚合做基础

总结:

学新通

DSL实现Bucket聚合

学新通

 桶聚合代码:

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "size":0,
  4.  
    "aggs":{
  5.  
    "brandAgg":{
  6.  
    "terms":{
  7.  
    "field": "brand",
  8.  
    "size":20
  9.  
    }
  10.  
    }
  11.  
    }
  12.  
    }

运行结果:

学新通

 默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序

我们可以修改结果排序方式:

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "size":0,
  4.  
    "aggs":{
  5.  
    "brandAgg":{
  6.  
    "terms":{
  7.  
    "field": "brand",
  8.  
    "size":20,
  9.  
    "order":{
  10.  
    "_count": "asc"
  11.  
    }
  12.  
    }
  13.  
    }
  14.  
    }
  15.  
    }
学新通

学新通

默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "query":{
  4.  
    "range":{
  5.  
    "price":{
  6.  
    "lte":200
  7.  
    }
  8.  
    }
  9.  
    },
  10.  
    "size":0,
  11.  
    "aggs":{
  12.  
    "brandAgg":{
  13.  
    "terms":{
  14.  
    "field": "brand",
  15.  
    "size":10
  16.  
    }
  17.  
    }
  18.  
    }
  19.  
    }
学新通

学新通

总结:

aggs代表聚合,与query同级,此时query的作用是?

        限定聚合的文档范围

聚合必须的三要素:

        聚合名称

        聚合类型

        聚合字段

聚合可配置的属性有:

        size:指定聚合结果数量

        order:指定聚合结果排序方式

        field:指定聚合字段

DSL实现Metrics聚合

例如,我们要求获取每个品牌的用户评分的min、max、avg等值

我们可以利用stats聚合:

学新通

 代码示例:

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "size":0,
  4.  
    "aggs":{
  5.  
    "brandAggs":{
  6.  
    "terms":{
  7.  
    "field": "brand",
  8.  
    "size":20
  9.  
    },
  10.  
    "aggs":{
  11.  
    "scoreAgg":{
  12.  
    "stats":{
  13.  
    "field":"score"
  14.  
    }
  15.  
    }
  16.  
    }
  17.  
    }
  18.  
    }
  19.  
    }
学新通

 学新通

需求:按照分类好的avg降序排序

  1.  
    GET /hotel/_search
  2.  
    {
  3.  
    "size":0,
  4.  
    "aggs":{
  5.  
    "brandAggs":{
  6.  
    "terms":{
  7.  
    "field": "brand",
  8.  
    "size":20,
  9.  
    "order":{
  10.  
    "scoreAgg.avg": "desc"
  11.  
    }
  12.  
    },
  13.  
    "aggs":{
  14.  
    "scoreAgg":{
  15.  
    "stats":{
  16.  
    "field":"score"
  17.  
    }
  18.  
    }
  19.  
    }
  20.  
    }
  21.  
    }
  22.  
    }
学新通

学新通

RestAPI实现聚合

学新通

 发起请求:

  1.  
    @Test
  2.  
    void testAggregation() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    request.source().size(0);
  5.  
    request.source().aggregation(AggregationBuilders
  6.  
    .terms("brandAgg")
  7.  
    .field("brand")
  8.  
    .size(20)
  9.  
    );
  10.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  11.  
    System.out.println(response);
  12.  
    }

学新通

 处理响应:

学新通

  1.  
    @Test
  2.  
    void testAggregation() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    request.source().size(0);
  5.  
    request.source().aggregation(AggregationBuilders
  6.  
    .terms("brandAgg")
  7.  
    .field("brand")
  8.  
    .size(20)
  9.  
    );
  10.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  11.  
    Aggregations aggregations = response.getAggregations();
  12.  
    Terms brandTerms = aggregations.get("brandAgg");
  13.  
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
  14.  
    for(Terms.Bucket bucket:buckets){
  15.  
    String key = bucket.getKeyAsString();
  16.  
    System.out.println(key);
  17.  
    }
  18.  
     
  19.  
    }
学新通

学新通

多条件聚合

案例:在IUserService中定义方法,实现对品牌、城市、星级的聚合

学新通

  1.  
    @Override
  2.  
    public Map<String, List<String>> filters() {
  3.  
    try {
  4.  
    SearchRequest request = new SearchRequest("hotel");
  5.  
    request.source().size(0);
  6.  
    String[] arr = {"brandAgg", "cityAgg", "starAgg"};
  7.  
    String[] fields = {"brand", "city", "starName.keyword"};
  8.  
    for (int i = 0; i < arr.length; i ) {
  9.  
    request.source().aggregation(AggregationBuilders
  10.  
    .terms(arr[i])
  11.  
    .field(fields[i])
  12.  
    .size(100)
  13.  
    );
  14.  
    }
  15.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  16.  
    Aggregations aggregations = response.getAggregations();
  17.  
    Map<String, List<String>> res = new HashMap<>();
  18.  
    for (int i = 0; i < arr.length; i ) {
  19.  
    List<String> brandList = new ArrayList<>();
  20.  
    Terms arrTerms = aggregations.get(arr[i]);
  21.  
    List<? extends Terms.Bucket> buckets = arrTerms.getBuckets();
  22.  
    for (Terms.Bucket bucket : buckets) {
  23.  
    brandList.add(bucket.getKeyAsString());
  24.  
    }
  25.  
    res.put(fields[i], brandList);
  26.  
    }
  27.  
    return res;
  28.  
    }catch (Exception e){
  29.  
    throw new RuntimeException();
  30.  
    }
  31.  
    }
学新通

 学新通

带过滤条件的聚合

学新通

 第一步:

  1.  
    @PostMapping("/filters")
  2.  
    public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
  3.  
    return hotelService.filters(params);
  4.  
    }

第二、三步:

            buildBasicQuery(params,request);

二、自动补全

安装拼音分词器

①解压

②上传至虚拟机的elasticsearch的plugin目录

③重启elasticsearch

④测试

  1.  
    POST /_analyze
  2.  
    {
  3.  
    "text":["如家酒店还不错"],
  4.  
    "analyzer": "pinyin"
  5.  
    }

学新通

自定义分词器

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

character filters:在tokenizer之前对文本进行处理。例如删除字符,替换字符

tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词,还有ik_smart

tokenizer filter:将tokenizer输出的词条做进一步的处理。例如大小写转换、同义词处理、拼音处理等

学新通

  1.  
    PUT /test
  2.  
    {
  3.  
    "settings":{
  4.  
    "analysis": {
  5.  
    "analyzer":{
  6.  
    "my_analyzer":{
  7.  
    "tokenizer":"ik_max_word",
  8.  
    "filter":"py"
  9.  
    }
  10.  
    },
  11.  
    "filter":{
  12.  
    "py":{
  13.  
    "type":"pinyin",
  14.  
    "keep_full_pinyin":false,
  15.  
    "keep_joined_full_pinyin":true,
  16.  
    "keep_original":true,
  17.  
    "limit_first_letter_length":16,
  18.  
    "remove_duplicated_term":true,
  19.  
    "none_chinese_pinyin_tokenizer":false
  20.  
    }
  21.  
    }
  22.  
    }
  23.  
    },
  24.  
    "mappings":{
  25.  
    "properties":{
  26.  
    "name":{
  27.  
    "type":"text",
  28.  
    "analyzer": "my_analyzer"
  29.  
    }
  30.  
    }
  31.  
    }
  32.  
    }
学新通

测试一:

测试代码及结果:

  1.  
    POST /test/_analyze
  2.  
    {
  3.  
    "text":["如家酒店还不错"],
  4.  
    "analyzer": "my_analyzer"
  5.  
    }

学新通

测试二:

  1.  
    POST /test/_doc/1
  2.  
    {
  3.  
    "id":1,
  4.  
    "name":"狮子"
  5.  
    }
  6.  
     
  7.  
    POST /test/_doc/2
  8.  
    {
  9.  
    "id":2,
  10.  
    "name":"虱子"
  11.  
    }
  12.  
     
  13.  
    GET /test/_search
  14.  
    {
  15.  
    "query":{
  16.  
    "match":{
  17.  
    "name":"shizi"
  18.  
    }
  19.  
    }
  20.  
    }
学新通

学新通

拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用

==>因此字段在创建倒排索引时应该用my_analyzer分词器;字段在搜索时应该使用ik_smart分词器

  1.  
    PUT /test
  2.  
    {
  3.  
    "settings":{
  4.  
    "analysis": {
  5.  
    "analyzer":{
  6.  
    "my_analyzer":{
  7.  
    "tokenizer":"ik_max_word",
  8.  
    "filter":"py"
  9.  
    }
  10.  
    },
  11.  
    "filter":{
  12.  
    "py":{
  13.  
    "type":"pinyin",
  14.  
    "keep_full_pinyin":false,
  15.  
    "keep_joined_full_pinyin":true,
  16.  
    "keep_original":true,
  17.  
    "limit_first_letter_length":16,
  18.  
    "remove_duplicated_term":true,
  19.  
    "none_chinese_pinyin_tokenizer":false
  20.  
    }
  21.  
    }
  22.  
    }
  23.  
    },
  24.  
    "mappings":{
  25.  
    "properties":{
  26.  
    "name":{
  27.  
    "type":"text",
  28.  
    "analyzer": "my_analyzer",
  29.  
    "search_analyzer": "ik_smart"
  30.  
    }
  31.  
    }
  32.  
    }
  33.  
    }
学新通

DSL实现自动补全查询

elasticsearch提供了completion suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中的类型有一些约束

        参与补全查询的字段必须是completion类型

        字段的内容一般是用来补全的多个词条形成的数组

学新通

 代码:

  1.  
    # 自动补全的索引库
  2.  
    PUT /auto-test
  3.  
    {
  4.  
    "mappings":{
  5.  
    "properties": {
  6.  
    "title":{
  7.  
    "type":"completion"
  8.  
    }
  9.  
    }
  10.  
    }
  11.  
    }
  12.  
     
  13.  
    # 示例数据
  14.  
    POST /auto-test/_doc
  15.  
    {
  16.  
    "title":["Sonny","WH-1000XM3"]
  17.  
    }
  18.  
    POST /auto-test/_doc
  19.  
    {
  20.  
    "title":["SK-II","PITERA"]
  21.  
    }
  22.  
    POST /auto-test/_doc
  23.  
    {
  24.  
    "title":["Nintendo","switch"]
  25.  
    }
学新通

测试:

  1.  
    GET /auto-test/_search
  2.  
    {
  3.  
    "suggest":{
  4.  
    "titleSuggest":{
  5.  
    "text":"s",
  6.  
    "completion":{
  7.  
    "field":"title",
  8.  
    "skip_duplicates":true,
  9.  
    "size":10
  10.  
    }
  11.  
    }
  12.  
    }
  13.  
    }

学新通

酒店数据自动补全

案例:实现hotel索引库的自动补全、拼音搜索功能

实现思路:

1.修改hotel索引库结构,设置自定义拼音分词器

2.修改索引库的name、all字段,使用自定义分词器

3.索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器

4.给HotelDoc类添加suggestion字段,内容包含brand、business

5.重新导入数据到hotel库

一、查看酒店的数据结构

  1.  
    # 查看酒店数据结构
  2.  
    GET /hotel/_mapping

二、创建hotel索引库

  1.  
    PUT /hotel
  2.  
    {
  3.  
    "settings":{
  4.  
    "analysis": {
  5.  
    "analyzer": {
  6.  
    "text_analyzer":{
  7.  
    "tokenizer":"ik_max_word",
  8.  
    "filter":"py"
  9.  
    },
  10.  
    "completion_analyzer":{
  11.  
    "tokenizer":"keyword",
  12.  
    "filter":"py"
  13.  
    }
  14.  
    },
  15.  
    "filter": {
  16.  
    "py":{
  17.  
    "type":"pinyin",
  18.  
    "keep_full_pinyin":false,
  19.  
    "keep_joined_full_pinyin":true,
  20.  
    "keep_original":true,
  21.  
    "limit_first_letter_length":16,
  22.  
    "remove_duplicated_term":true,
  23.  
    "none_chinese_pinyin_tokenizer":false
  24.  
     
  25.  
    }
  26.  
     
  27.  
    }
  28.  
    }
  29.  
    },
  30.  
    "mappings":{
  31.  
    "properties":{
  32.  
    "id":{
  33.  
    "type":"keyword"
  34.  
    },
  35.  
    "name":{
  36.  
    "type":"text",
  37.  
    "analyzer":"text_analyzer",
  38.  
    "search_analyzer": "ik_smart",
  39.  
    "copy_to":"all"
  40.  
    },
  41.  
    "address":{
  42.  
    "type":"keyword",
  43.  
    "index":false
  44.  
    },
  45.  
    "price":{
  46.  
    "type":"integer"
  47.  
    },
  48.  
    "score":{
  49.  
    "type":"integer"
  50.  
    },
  51.  
    "brand":{
  52.  
    "type":"keyword",
  53.  
    "copy_to":"all"
  54.  
    },
  55.  
    "city":{
  56.  
    "type":"keyword"
  57.  
    },
  58.  
    "star_name":{
  59.  
    "type":"keyword"
  60.  
    },
  61.  
    "business":{
  62.  
    "type":"keyword",
  63.  
    "copy_to": "all"
  64.  
    },
  65.  
    "location":{
  66.  
    "type":"geo_point"
  67.  
    },
  68.  
    "pic":{
  69.  
    "type":"keyword",
  70.  
    "index":false
  71.  
    },
  72.  
    "all":{
  73.  
    "type":"text",
  74.  
    "analyzer":"text_analyzer",
  75.  
    "search_analyzer": "ik_smart"
  76.  
    },
  77.  
    "suggestion":{
  78.  
    "type":"completion",
  79.  
    "analyzer": "completion_analyzer"
  80.  
    }
  81.  
    }
  82.  
    }
  83.  
    }
学新通

三、修改HotelDoc实体类并批处理导入数据

  1.  
    @Data
  2.  
    @NoArgsConstructor
  3.  
    public class HotelDoc {
  4.  
    private Long id;
  5.  
    private String name;
  6.  
    private String address;
  7.  
    private Integer price;
  8.  
    private Integer score;
  9.  
    private String brand;
  10.  
    private String city;
  11.  
    private String starName;
  12.  
    private String business;
  13.  
    private String location;
  14.  
    private String pic;
  15.  
    private Object distance;
  16.  
    private Boolean isAD;
  17.  
    private List<String> suggestion;
  18.  
     
  19.  
    public HotelDoc(Hotel hotel) {
  20.  
    this.id = hotel.getId();
  21.  
    this.name = hotel.getName();
  22.  
    this.address = hotel.getAddress();
  23.  
    this.price = hotel.getPrice();
  24.  
    this.score = hotel.getScore();
  25.  
    this.brand = hotel.getBrand();
  26.  
    this.city = hotel.getCity();
  27.  
    this.starName = hotel.getStarName();
  28.  
    this.business = hotel.getBusiness();
  29.  
    this.location = hotel.getLatitude() ", " hotel.getLongitude();
  30.  
    this.pic = hotel.getPic();
  31.  
    this.suggestion= Arrays.asList(this.brand,this.business);
  32.  
    }
  33.  
    }
学新通

四、优化自动补全结果

因为查出来的结果如果商圈business有多个,会用、分隔,所以为了自动补全更为智能,可以做优化

学新通

  1.  
    if(this.business.contains("、")||this.business.contains("/")){
  2.  
    String[] arr ;
  3.  
    if(this.business.contains("、"))
  4.  
    arr= this.business.split("、");
  5.  
    else
  6.  
    arr=this.business.split("/");
  7.  
    this.suggestion=new ArrayList<>();
  8.  
    this.suggestion.add(this.brand);
  9.  
    Collections.addAll(this.suggestion,arr);
  10.  
    }else
  11.  
    this.suggestion= Arrays.asList(this.brand,this.business);

重新导入数据,优化成功:

学新通

RestAPI实现自动补全查询

学新通

 学新通

  1.  
    @Test
  2.  
    void testSuggest() throws IOException {
  3.  
    SearchRequest request = new SearchRequest("hotel");
  4.  
    request.source().suggest(new SuggestBuilder()
  5.  
    .addSuggestion("suggestions", SuggestBuilders.completionSuggestion("suggestion")
  6.  
    .prefix("h")
  7.  
    .skipDuplicates(true)
  8.  
    .size(10)
  9.  
    ));
  10.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  11.  
    //解析结果
  12.  
    Suggest suggest = response.getSuggest();
  13.  
    //根据补全查询名称,获取补全结果
  14.  
    CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
  15.  
    //获取options
  16.  
    List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
  17.  
    //遍历
  18.  
    for(CompletionSuggestion.Entry.Option option:options){
  19.  
    String s = option.getText().toString();
  20.  
    System.out.println(s);
  21.  
    }
  22.  
    }
学新通

实现搜索框自动补全

第一步:在Controller中添加如下方法

  1.  
    @GetMapping("/suggestion")
  2.  
    public List<String> getSuggestions(@RequestParam("key") String prefix){
  3.  
    return hotelService.getSuggestion(prefix);
  4.  
    }

第二步:在IHotelService接口中添加对应的方法并实现

  1.  
    @Override
  2.  
    public List<String> getSuggestion(String prefix) {
  3.  
    List<String> list=new ArrayList<>();
  4.  
    try {
  5.  
    SearchRequest request = new SearchRequest("hotel");
  6.  
    request.source().suggest(new SuggestBuilder().addSuggestion("suggestions",
  7.  
    SuggestBuilders.completionSuggestion("suggestion")
  8.  
    .prefix(prefix)
  9.  
    .skipDuplicates(true)
  10.  
    .size(10)
  11.  
    ));
  12.  
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  13.  
    Suggest suggest = response.getSuggest();
  14.  
    CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
  15.  
    List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
  16.  
    for (CompletionSuggestion.Entry.Option option : options) {
  17.  
    list.add(option.getText().toString());
  18.  
    }
  19.  
    return list;
  20.  
    }catch(IOException e){
  21.  
    throw new RuntimeException(e);
  22.  
    }
  23.  
    }
学新通

测试效果:

学新通

三、数据同步

同步方案分析

elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步

方案一:同步调用

学新通

 方案二:异步通知

学新通

方案三:监听binlog

学新通

 总结:

方式一:同步调用

        优点:实现简单、粗暴

        缺点:业务耦合度高

方式二:异步通知

        优点:低耦合,实现难度一般

        缺点:依赖mq的可靠性

方式三:监听binlog

        优点:完全解除服务间耦合

        缺点:开启binlog增加数据库负担,实现复杂度高

利用MQ实现mysql与elasticsearch数据同步

案例:利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增、删、改时,要求对elasticsearch中数据也要完成相同操作

步骤:

        导入课前资料提供的hotel-admin项目,启动并测试酒店数据的CRUD

        声明exchange、queue、RoutingKey

        在hotel-admin中的增、删、改业务中完成消息发送

        在hotel-demo中完成消息监听,并更新elasticsearch中数据

        启动并测试数据同步功能

声明队列和交换机

学新通

第一步:导入依赖

  1.  
    <dependency>
  2.  
    <groupId>org.springframework.boot</groupId>
  3.  
    <artifactId>spring-boot-starter-amqp</artifactId>
  4.  
    </dependency>

 第二步:配置rabbitmq

  1.  
    rabbitmq:
  2.  
    host: 192.168.150.101
  3.  
    port: 5672
  4.  
    username: itcast
  5.  
    password: 123321
  6.  
    virtual-host: /

第三步:定义常量

  1.  
    public class MqConstants {
  2.  
    /*
  3.  
    * 交换机
  4.  
    * */
  5.  
    public final static String HOTEL_EXCHANGE="hotel.topic";
  6.  
    /*
  7.  
    * 监听新增和修改的队列
  8.  
    * */
  9.  
    public final static String HOTEL_INSERT_QUEUE="hotel.insert.queue";
  10.  
    /*
  11.  
    * 监听删除的队列
  12.  
    * */
  13.  
    public final static String HOTEL_DELETE_QUEUE="hotel.delete.queue";
  14.  
    /*
  15.  
    * 新增或修改的RoutingKey
  16.  
    * */
  17.  
    public final static String HOTEL_INSERT_KEY="hotel.insert";
  18.  
    /*
  19.  
    * 删除的RoutingKey
  20.  
    * */
  21.  
    public final static String HOTEL_DELETE_KEY="hotel.delete";
  22.  
     
  23.  
    }
学新通

第四步:创建消息队列并绑定到交换机上

  1.  
    @Configuration
  2.  
    public class MqConfig {
  3.  
    @Bean
  4.  
    public TopicExchange topicExchange(){
  5.  
    return new TopicExchange(MqConstants.HOTEL_EXCHANGE,true,false);
  6.  
    }
  7.  
     
  8.  
    @Bean
  9.  
    public Queue insertQueue(){
  10.  
    return new Queue(MqConstants.HOTEL_INSERT_QUEUE,true);
  11.  
    }
  12.  
     
  13.  
    @Bean
  14.  
    public Binding insertQueueBinding(){
  15.  
    return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
  16.  
    }
  17.  
     
  18.  
    @Bean
  19.  
    public Queue deleteQueue(){
  20.  
    return new Queue(MqConstants.HOTEL_DELETE_QUEUE,true);
  21.  
    }
  22.  
     
  23.  
    @Bean
  24.  
    public Binding deleteQueueBinding(){
  25.  
    return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
  26.  
    }
  27.  
    }
学新通

发送mq消息

第一步:配置常量(即上一节的MqConstants)

第二步:修改controller的代码

  1.  
    @RestController
  2.  
    @RequestMapping("hotel")
  3.  
    public class HotelController {
  4.  
     
  5.  
    @Autowired
  6.  
    private IHotelService hotelService;
  7.  
     
  8.  
    @Autowired
  9.  
    private RabbitTemplate rabbitTemplate;
  10.  
     
  11.  
    @GetMapping("/{id}")
  12.  
    public Hotel queryById(@PathVariable("id") Long id){
  13.  
    return hotelService.getById(id);
  14.  
    }
  15.  
     
  16.  
    @GetMapping("/list")
  17.  
    public PageResult hotelList(
  18.  
    @RequestParam(value = "page", defaultValue = "1") Integer page,
  19.  
    @RequestParam(value = "size", defaultValue = "1") Integer size
  20.  
    ){
  21.  
    Page<Hotel> result = hotelService.page(new Page<>(page, size));
  22.  
     
  23.  
    return new PageResult(result.getTotal(), result.getRecords());
  24.  
    }
  25.  
     
  26.  
    @PostMapping
  27.  
    public void saveHotel(@RequestBody Hotel hotel){
  28.  
    hotelService.save(hotel);
  29.  
     
  30.  
    rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());
  31.  
    }
  32.  
     
  33.  
    @PutMapping()
  34.  
    public void updateById(@RequestBody Hotel hotel){
  35.  
    if (hotel.getId() == null) {
  36.  
    throw new InvalidParameterException("id不能为空");
  37.  
    }
  38.  
    hotelService.updateById(hotel);
  39.  
     
  40.  
    rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());
  41.  
    }
  42.  
     
  43.  
    @DeleteMapping("/{id}")
  44.  
    public void deleteById(@PathVariable("id") Long id) {
  45.  
    hotelService.removeById(id);
  46.  
    rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_DELETE_KEY,id);
  47.  
    }
  48.  
    }
学新通

监听mq消息

第一步:消费者监听队列

  1.  
    @Component
  2.  
    public class HotelListener {
  3.  
     
  4.  
    @Autowired
  5.  
    private IHotelService hotelService;
  6.  
     
  7.  
    /*
  8.  
    * 监听酒店新增或修改的业务
  9.  
    * */
  10.  
    @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
  11.  
    public void listenHotelInsertOrUpdate(Long id){
  12.  
    hotelService.insertById(id);
  13.  
    }
  14.  
     
  15.  
    /*
  16.  
    * 监听酒店删除的业务
  17.  
    * */
  18.  
    @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
  19.  
    public void listenHotelDelete(Long id){
  20.  
    hotelService.deleteById(id);
  21.  
    }
  22.  
     
  23.  
    }
学新通

第二步:在HotelService中编写对应的方法

  1.  
    @Override
  2.  
    public void deleteById(Long id) {
  3.  
    try {
  4.  
    //准备Request
  5.  
    DeleteRequest request = new DeleteRequest("hotel", id.toString());
  6.  
    //发送请求
  7.  
    client.delete(request, RequestOptions.DEFAULT);
  8.  
    }catch(IOException e){
  9.  
    throw new RuntimeException();
  10.  
    }
  11.  
    }
  12.  
     
  13.  
    @Override
  14.  
    public void insertById(Long id) {
  15.  
    try {
  16.  
    //根据id查询酒店数据
  17.  
    Hotel hotel = getById(id);
  18.  
    //转换为文档类型
  19.  
    HotelDoc hotelDoc = new HotelDoc(hotel);
  20.  
    //准备request对象
  21.  
    IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
  22.  
    //准备json文档
  23.  
    request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
  24.  
    //发送请求
  25.  
    client.index(request, RequestOptions.DEFAULT);
  26.  
    }catch (IOException e){
  27.  
    throw new RuntimeException();
  28.  
    }
  29.  
    }
学新通

四、elasticsearch集群

学新通

 ES集群结构

单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题

学新通

搭建ES集群

我们计划利用3个docker容器模拟3个es的节点。

①es运行需要修改一些linux系统权限,修改/etc/sysctl.conf文件

vi /etc/sysctl.conf

②添加下面的内容

vm.max_map_count=262144

③通过sysctl -p使上面的配置生效

sysctl -p

运行docker-compose.yml

docker-compose up -d

docker-compose.yml完整内容:

  1.  
    version: '2.2'
  2.  
    services:
  3.  
    es01:
  4.  
    image: elasticsearch:7.12.1
  5.  
    container_name: es01
  6.  
    environment:
  7.  
    - node.name=es01
  8.  
    - cluster.name=es-docker-cluster
  9.  
    - discovery.seed_hosts=es02,es03
  10.  
    - cluster.initial_master_nodes=es01,es02,es03
  11.  
    - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  12.  
    volumes:
  13.  
    - data01:/usr/share/elasticsearch/data
  14.  
    ports:
  15.  
    - 9200:9200
  16.  
    networks:
  17.  
    - elastic
  18.  
    es02:
  19.  
    image: elasticsearch:7.12.1
  20.  
    container_name: es02
  21.  
    environment:
  22.  
    - node.name=es02
  23.  
    - cluster.name=es-docker-cluster
  24.  
    - discovery.seed_hosts=es01,es03
  25.  
    - cluster.initial_master_nodes=es01,es02,es03
  26.  
    - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  27.  
    volumes:
  28.  
    - data02:/usr/share/elasticsearch/data
  29.  
    ports:
  30.  
    - 9201:9200
  31.  
    networks:
  32.  
    - elastic
  33.  
    es03:
  34.  
    image: elasticsearch:7.12.1
  35.  
    container_name: es03
  36.  
    environment:
  37.  
    - node.name=es03
  38.  
    - cluster.name=es-docker-cluster
  39.  
    - discovery.seed_hosts=es01,es02
  40.  
    - cluster.initial_master_nodes=es01,es02,es03
学新通

集群状态监控:

kibana可以监控es集群,不过新版本需要依赖es的x-pack功能,配置比较复杂

这里推荐使用cerebro来监控es集群状态

找到bin目录,双击cerebro.bat即可运行

学新通

进入cerebro管理界面,可以任意输入一个你想管理的地址,比如http://192.168.150.101:9200

学新通

绿色的条代表集群很健康:

学新通

分片备份(more-->create index)

学新通

 实线的方框是主分片,虚线的方框是备份分配

学新通

 集群职责及脑裂

elasticsearch中集群节点有不同的职责划分:

学新通

elasticsearch的每个节点角色都有自己不同的职责,因此建议集群部署时,每个节点都有独立的角色

es集群的脑裂:默认情况下,每个结点都是master eligible节点,因此一旦master节点宕机,其他候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。

为了避免脑裂,需要要求选票(eligible节点数量 1)/ 2,才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题

学新通

总结:

学新通

分布式新增和查询流程

当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到那个分片呢?

在9200端口插入三条数据:

学新通

 可以发现,任意三个端口之一都能查到这三条数据:

学新通

 通过explain命令查看数据到底存储在哪个分片:

  1.  
    {
  2.  
    "explain":true,
  3.  
    "query":{
  4.  
    "match_all":{}
  5.  
    }
  6.  
    }

查询结果:

  1.  
    "hits": [
  2.  
    {
  3.  
    "_shard": "[itcast][1]",
  4.  
    "_node": "vf7qm8YERz2UgcLwCpSX9A",
  5.  
    "_index": "itcast",
  6.  
    "_type": "_doc",
  7.  
    "_id": "2",
  8.  
    "_score": 1.0,
  9.  
    "_source": {
  10.  
    "title": "试着插入一条 id = 2"
  11.  
    },
  12.  
    "_explanation": {
  13.  
    "value": 1.0,
  14.  
    "description": "*:*",
  15.  
    "details": []
  16.  
    }
  17.  
    },
  18.  
    {
  19.  
    "_shard": "[itcast][1]",
  20.  
    "_node": "vf7qm8YERz2UgcLwCpSX9A",
  21.  
    "_index": "itcast",
  22.  
    "_type": "_doc",
  23.  
    "_id": "3",
  24.  
    "_score": 1.0,
  25.  
    "_source": {
  26.  
    "title": "试着插入一条 id = 3"
  27.  
    },
  28.  
    "_explanation": {
  29.  
    "value": 1.0,
  30.  
    "description": "*:*",
  31.  
    "details": []
  32.  
    }
  33.  
    },
  34.  
    {
  35.  
    "_shard": "[itcast][2]",
  36.  
    "_node": "C6sNM5bbT2arRKIrOos8iA",
  37.  
    "_index": "itcast",
  38.  
    "_type": "_doc",
  39.  
    "_id": "1",
  40.  
    "_score": 1.0,
  41.  
    "_source": {
  42.  
    "title": "试着插入一条 id = 1"
  43.  
    },
  44.  
    "_explanation": {
  45.  
    "value": 1.0,
  46.  
    "description": "*:*",
  47.  
    "details": []
  48.  
    }
  49.  
    }
  50.  
    ]
学新通

可以看到虽然是向9200存数据,但是数据会存到不同分片上。

原理如下:

学新通

新增文档流程:

学新通

学新通

总结:

分布式新增如何确定分片?

        coordinating node 根据id做hash运算,得到结果对shard数量取余,余数就是对应的分片

分布式查询:

        分散阶段:coordinating node将查询请求分发给不同分片

        收集阶段:将查询结果汇总到coordinating node,整理并返回给用户

故障转移

集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其他节点,确保数据安全,这个叫做故障转移

学新通

 演示:

停掉主节点

        [root@Soft soft]# docker-compose stop es02

此时集群状态不健康:

学新通

短暂等待后可以发现故障节点的数据成功迁移到健康的节点了:

学新通

 再次查询,数据没有丢失:

学新通

 重新开启es02

        [root@Soft soft]# docker-compose start es02

数据又会从健康分片上转移到es02

学新通

总结:

学新通


至此,springcloud的基础篇完结,恭喜大家通关~❀

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfkajkg
系列文章
更多 icon
同类精品
更多 icon
继续加载