微服务技术栈——实用篇

什么是微服务技术栈

微服务技术栈 = 微服务 + 持续集成
微服务技术栈——实用篇_第1张图片
微服务技术栈——实用篇_第2张图片
微服务技术栈——实用篇_第3张图片
微服务技术栈——实用篇_第4张图片

学习路线

微服务技术栈——实用篇_第5张图片

微服务治理

认识微服务

服务架构演变

单体架构

单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:

  1. 架构简单
  2. 部署成本低

缺点:

  1. 耦合度高
分布式架构

分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。
优点:

  1. 降低服务耦合
  2. 有利于服务升级拓展
服务治理

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

  1. 服务拆分粒度如何?
  2. 服务集群地址如何维护?
  3. 服务之间如何实现远程调用?
  4. 服务健康状态如何感知?
微服务

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

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

微服务技术栈——实用篇_第6张图片

总结
  1. 单体架构特点?

简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统

  1. 分布式架构特点?

松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝

  1. 微服务:一种良好的分布式架构方案
  • 优点:拆分粒度更小、服务更独立、耦合度更低
  • 缺点:架构非常复杂,运维、监控、部署难度提高

微服务结构

微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。
微服务技术对比
微服务技术栈——实用篇_第7张图片
微服务技术栈——实用篇_第8张图片

Spring Cloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud
SpringCloud集成了各种微服务功能组件,并基于Spring Boot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
微服务技术栈——实用篇_第9张图片

服务拆分及远程调用

服务拆分

服务拆分注意事项
  1. 不同微服务,不要重复开发相同业务
  2. 微服务数据独立,不要访问其它微服务的数据库
  3. 微服务可以将自己的业务暴露为接口,供其它微服务调用
总结
  1. 微服务需要根据业务模块拆分,做到单一职责,不要重复开发相同业务
  2. 微服务可以将业务暴露为接口,供其它微服务使用
  3. 不同微服务都应该有自己独立的数据库

服务间调用

远程调用方式分析

微服务技术栈——实用篇_第10张图片
步骤:

  1. 注册RestTemplate

在order-service的OrderApplication中注册RestTemplate

  1. 服务远程调用RestTemplate

修改order-service中的OrderServicel的queryOrderByld方法

总结

微服务调用方式

  1. 基于RestTemplate发起的http请求实现远程调用
  2. http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。

微服务远程调用

提供者与消费者

服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
服务既可以是服务提供者,也可以是服务消费者。

思考

服务调用关系

  • 服务提供者:暴露接口给其它微服务调用
  • 服务消费者:调用其它微服务提供的接口
  • 提供者与消费者角色其实是相对的
  • 一个服务可以同时是服务提供者和服务消费者

Eureka 注册中心

远程调用的问题

服务调用出现的问题
  1. 服务消费者该如何获取服务提供者的地址信息?
  • 服务提供者启动时向eureka注册自己的信息
  • eureka保存这些信息
  • 消费者根据服务名称向eureka拉取提供者信息
  1. 如果有多个服务提供者,消费者该如何选择?
  • 服务消费者利用负载均衡算法,从服务列表中挑选一个
  1. 消费者如何得知服务提供者的健康状态?
  • 服务提供者会每隔30秒向EurekaServer,发送心跳请求,报告健康状态
  • eureka会更新记录服务列表信息,心跳不正常会被剔除
  • 消费者就可以拉取到最新的信息
Eureka作用

微服务技术栈——实用篇_第11张图片

总结

在Eureka架构中,微服务角色有两类:

  1. EurekaServer:服务端,注册中心
  • 记录服务信息
  • 心跳监控
  1. EurekaClient:客户端
  • Provider:服务提供者,例如案例中的user-service
    • 注册自己的信息到EurekaServer
    • 每隔30秒向EurekaServer发送心跳
  • Consumer:服务消费者,例如案例中的order-service
    • 根据服务名称从EurekaServer拉取服务列表
    • 基于服务列表做负载均衡,选中一个微服务后发起远程调用

Eureka原理

Eureka注册中心

微服务技术栈——实用篇_第12张图片

搭建EurekaServer

搭建EurekaServer服务步骤如下:

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

	  
    
        org.springframework.cloud
        spring-cloud-starter-netflix-eureka-server
    

  1. 编写启动类,添加@EnableEurekaServer注解
  2. 添加application.yml文件,编写配置
服务注册

将user-service服务注册到EurekaServer步骤如下:

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


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client

  1. 在application.yml文件,编写配置

注册user-service
另外,我们可以将user-service多次启动,模拟多实例部署,但为了避免端口冲突,需要修改端口设置:
通过Copy Configuration…中的VM options: -Dserver.port=8082

总结

服务注册

  1. 引入eureka-client依赖
  2. 在application.yml中配置eureka地址

无论是消费者还是提供者,引入eureka-client依赖, 知道eureka地址后,都可以完成服务注册

服务拉取

在order-service完成服务拉取

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

  1. 修改OrderServicel的代码,修改访问的url路径,用服务名代替ip、端口
  2. 在order–service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解 @LoadBalanced
总结
  1. 搭建EurekaServer
    1. 引入eureka-server依赖
    2. 添加@EnableEurekaServer注解
    3. 在application.yml中配置eureka地址
  2. 服务注册
    1. 引入eureka-client依赖
    2. 在application.yml中配置eureka地址
  3. 服务发现
    1. 引入eureka-client依赖
    2. 在application.yml中配置eureka地址
    3. 给RestTemplate添加@LoadBalanced注解
    4. 用服务提供者的服务名称远程调用

Ribbon 负载均衡

负载均衡原理

负载均衡流程

微服务技术栈——实用篇_第13张图片
微服务技术栈——实用篇_第14张图片

负载均衡策略

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
微服务技术栈——实用篇_第15张图片
微服务技术栈——实用篇_第16张图片
通过定义IRue实现可以修改负载均衡规则,有两种方式:
代码方式:在order-service中的OrderApplication类中,定义一个新的IRule (全局)

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

配置文件方式:在order–service的application.yml文件中,添加新的配置也可以修改规则 (针对某个微服务)

userservice:
    ribbon:
		NFLoadBalancerRuleClassName: com.netflix.Loadbalancer.RandomRule   #负载均衡规则

饥饿加载

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

总结
  1. Ribbon负载均衡规则
    1. 规则接口是IRule
    2. 默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询
  2. 负载均衡自定义方式
    1. 代码方式:配置灵活,但修改时需要重新打包发布
    2. 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置
  3. 饥饿加载
    1. 开启饥饿加载
    2. 指定饥饿加载的微服务名称

Nacos 注册中心

认识和安装Nacos

认识Nacos

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

服务注册到Nacos
  1. 在cloud-demo父工程中添加spring-cloud-alilbaba的管理依赖:


    com.alibaba.cloud
    spring-cloud-alibaba-dependencies
    ${spring-cloud.nacos}
    pom
    import

  1. 注释掉order-service和user-servicet中原有的eureka依赖。

<dependency>
  <groupId>com.alibaba.cloudgroupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
  1. 添加nacos的客户端依赖:
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
  1. 修改user-service & order-service中的application.yml文件,注释eureka地址,添加nacos地址
  2. 启动并测试
总结
  1. Nacos服务搭建
    1. 下载安装包
    2. 解压
    3. 在bin目录下运行指令:startup.cmd -m standalone
  2. Nacos服务注册或发现
    1. 引入nacos.discovery依赖
    2. 配置nacos地址spring.cloud.nacos.server-addr

Nacos服务分级存储模型

微服务技术栈——实用篇_第17张图片

服务跨集群调用问题
  • 服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
  • 本地集群不可访问时,再去访问其它集群
服务集群属性
  1. 修改application.yml, 添加如下内容
spring:
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos的服务地址
      discovery:
        cluster-name: TJ  #集群名称
  1. 在Nacos控制台可以看到集群变化
总结
  1. Nacos服务分级存储模型
    1. 一级是服务,例如userservice
    2. 二级是集群,例如杭州或上海
    3. 三级是实例,例如杭州机房的某台部署了userservice的服务器
  2. 如何设置实例的集群属性

修改 application.yml 文件,添加 spring.cloud.nacos.discovery.cluster-name 即可

根据集群负载均衡
  1. 修政order-service中的application.yml, 设置集群为TJ
spring:
	cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: TJ
  1. 然后在order-service中设置负载均衡的IRule. 为NacosRule,这个规则优先会寻找与自己同集群的服务
userservice: #在服务消费者配置服务提供者的服务名
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule 
		# Nacos优先访问本地集群,在本地集群内部采用随机的负载均衡规则
  1. 注意将user-service的权重都设置为1
总结

NacosRule负载均衡策略

  • 优先选择同集群服务实例列表
  • 本地集群找不到提供者,才去其它集群寻找,井且会报警告
  • 确定了可用实例列表后,再采用随机负裁均衡挑选实例
根据权重负载均衡

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

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

步骤:

  1. 在Nacos 控制台可以设置实例的权重值,首先选中实例后面的编辑按钮
  2. 将权重设置为0.1,测试可以发现8081被访问到的频率大大降低
总结

实例的权重控制

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

Nacos环境隔离

环境隔离 namespace

Namespace -> Group -> Service (Data) -> 集群 -> 实例
步骤:

  1. 在 Nacos 控制台可以创建 namespace, 用来隔离不同环境
  2. 然后填写一个新的命名空间信息
  3. 保存后会在控制台看到这个命名空间的id (很重要,不写默认会随机生成一个UUID)
  4. 修改 order-service 的 application.yml, 添加 namespace
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: TJ
        namespace: 473adcbe-4c0f-4225-8189-44e195901c13   # dev环境的命名空间的 ID
  1. 重启order-service后,再来查看控制台
  2. 此时访问 order-service ,.因为 namespace 不同,会导致找不到 userservice ,控制台会报错
总结

Nacos环境隔离

  • namespace用来做环境隔离
  • 每个namespace都有唯一 id
  • 不同 namespace 下的服务不可见
nacos注册中心细节分析

微服务技术栈——实用篇_第18张图片

临时实例和非临时实例

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

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: TJ
        namespace: 473adcbe-4c0f-4225-8189-44e195901c13   # dev环境的命名空间的 ID
        ephemeral: false  # 临时实例

临时实例宕机时,会从nacos的服务列表中剔除,而非临时实例则不会

总结

Nacos与eureka的相同点和不同点

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

Nacosi 配置管理

统一配置管理

配置更改热更新

微服务技术栈——实用篇_第19张图片
步骤:

  1. 在Nacos中添加配置信息
  2. 在弹出表单中填写配置信息
    1. 配置文件的id: [服务名称]-[profile].[后缀名] ,后缀名一般是 .yaml
    2. 分组,默认即可
    3. 格式,目前支持yaml和properties
配置获取的步骤

微服务技术栈——实用篇_第20张图片
步骤:

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

<dependency>
  <groupId>com.alibaba.cloudgroupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
  1. 在userservicer中的resourcel目录添加一个bootstrap.yml文件,这个文件是引导文件,优先级高于application.yml
spring:
  application:
    name: userservice
  profiles:
    active: dev # 环境
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos 地址
      config:
        file-extension: yaml # 文件后缀名

总结

将配置交给Nacos管理的步骤

  1. 在 Nacos 中添加配置文件
  2. 在微服务中引入 nacos 的 config 依赖
  3. 在微服务中添加pootstrap.yml, 配置 nacos 地址、当前环境、服务名称、文件后缀名。这些决定了程序启动时去 nacos 读取哪个文件
配置自动刷新 (热更新)

Ncos中的配置文件变更后,微服务无需重启就可以感知。需要通过下面两种配置实现:
方式一:通过@Value注解注入,结合@RefreshScope来刷新

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

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

方式二:使用@ConfigurationProperties注解

@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateFormat;

}

总结
  • Nacos配置更改后,微服务可以实现热更新,方式:
    • 通过@Value注解注入,结合@RefreshScope来刷新
    • 通过@ConfigurationProperties注入,自动刷新
  • 注意事项:
    • 不是所有的配置都适合放到配置中心,维护起来比较麻烦
    • 建议将一些关键参数,需要运行时调整的参数放到 Nacosi 配置中心,一般都是自定义配置

多环境配置共享

微服务启动时会从nacos读取多个配置文件:
[spring.application.name]-[spring-profiles.active]yaml,例如:userservice-dev.yaml
[spring.application.name].yaml,例如:userservice.yaml
无论profiles如何变化,[spring.application.name],yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件
多种配置的优先级:
微服务技术栈——实用篇_第21张图片

总结
  1. 微服务会从nacos读取的配置文件:

[服务名]-[spring.profile.active].yaml, 环境配置
[服务名].yaml, 默认配置,多环境共享~

  1. 优先级:

[服务名]-[环境]yaml > [服务名].yaml > 本地配置

搭建Nacos集群

Nacos生产环境下一定要部署为集群状态 nacos集群搭建.md

总结

集群搭建步骤:

  1. 搭建MySQL集群并初始化数据库表
  2. 下载解压nacos
  3. 修改集群配置(节点信息)、数据库配置

进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
然后添加内容:

127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847

然后修改application.properties文件,添加数据库配置

spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=password
  1. 分别启动多个nacos节点
  • 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3
  • 然后分别修改三个文件夹中的application.properties
  • 然后分别启动三个nacos节点
  1. nginx反向代理

修改conf/nginx.conf文件,配置如下:

upstream nacos-cluster {
    server 127.0.0.1:8845;
	server 127.0.0.1:8846;
	server 127.0.0.1:8847;
}

server {
    listen       80; #注意: 80端口一般都会被占用,请采用其它端口
    server_name  localhost;

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

代码中application.yml文件配置如下:

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

HTTP客户端 Feign

Feign替代RestTemplate

RestTemplate方式调用存在的问题

先来看我们以前利用RestTemplate发起远程调用的代码:

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

存在下面的问题:

  1. 代码可读性差,编程体验不统一
  2. 参数复杂URL难以维护
Feign的介绍

Feign是一个声明式的http客户端,官方地址:
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
定义和使用Feign客户端
使用Feign的步骤如下:

  1. 引入依赖

  <dependency>
      <groupId>org.springframework.cloudgroupId>
      <artifactId>spring-cloud-starter-openfeignartifactId>
  dependency>
  1. 在order-service的启动类添加注解开启Feign的功能

使用注解 @EnableFeignClients

  1. 编写Feign客户端
@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

4.用Feign客户端代替RestTemplate
主要是基于SpringMVC的注解来声明远程调用的信息,比如:

  • 服务名称:userservice
  • 请求方式:GET
  • 请求路径:user/id
  • 请求参数:Long id
  • 返回值类型:User
总结

Feign的使用步骤

  1. 引入依赖
  2. 添加@EnableFeignClients注解
  3. 编写FeignClient接口
  4. 使用FeignClient中定义的方法代替RestTemplate

自定义配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
微服务技术栈——实用篇_第22张图片

Feign使用优化

最佳实践配置Feign日志有两种方式:

方式一:配置文件方式
①全局生效:

feign:
  client:
  	config:
    	default:  #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
  			loggerLevel: FULL  #日志级别

②局部生效:

feign:
  client:
    config:
      userservice:  #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
      	loggerLevel: FULL  #日志级别

方式二:java代码方式,需要先声明一个Bean

public class FeignClientConfiguration {
    @Bean
    public Logger.Level feignLogLevel(){
    	return Logger.Level.BASIC;
    }
}

①而后如果是全局配置,则把它放到 @EnableFeignClients 这个注解中:

@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)

②如果是局部配置,则把它放到@FeignClienti这个注解中:

@Feignclient(value "userservice",configuration = FeignclientConfiguration.class)

总结

Feign的日志配置:
1.方式一 配置文件,feign.client.config.Xxx.loggerLevel
①如果Xxx是default则代表全局
②如果Xxx是服务名称,例如userservice.则代表某服务
2.方式二 java代码配置Logger.Level这个Bean
①如果在@EnableFeignClients注解声明则代表全局

@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)

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

@FeignClient("userservice", configuration = {FeignClientConfiguration.class})

Feign 的性能优化

Feign 底层的客户端实现:

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

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

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

Feign添加HttpClient的支持:

  1. 引入依赖:

<dependency>
  <groupId>io.github.openfeigngroupId>
  <artifactId>feign-httpclientartifactId>
dependency>
  1. 配置连接池:
feign:
  httpclient:
    enabled: true # 支持httpclient开关
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 单个路径的最大连接数 

总结

Feign的优化:
1.日志级别尽量用basic
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数

Feign的最佳实践

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

  • 服务紧耦合
  • 父接口参数列表中的映射不会被继承

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

总结

Feign的最佳实践:
①让controller和FeignClient继承同一接口
②将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用

抽取FeignClient

实现最佳实践方式二的步骤如下:

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

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

@EnableFeignclients(basePackages = "cn.itcast.feign.clients")

方式二:指定 FeignClient 字节码

@EnableFeignclients(clients = {Userclient.class})

报错

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
这个警告信息是Spring依赖注入过程中出现的,因为存在多个同类型的Bean被注入到一个变量或参数中,Spring无法确定要使用哪一个Bean,建议使用以下方法解决:

  1. 使用@Qualifier注解标识要注入的Bean,明确指定要注入的Bean。例如:
@Autowired
@Qualifier("beanName")
private MyBean myBean;

这样Spring就会根据"beanName"找到唯一要注入的Bean。

  1. 使用@Primary注解标明默认的Bean。例如:
@Bean
@Primary
public MyBean myBean() {
    return new MyBean();
}

这里加上了@Primary注解表示该Bean是默认Bean,当有多个同类型的Bean时,Spring默认使用加上了@Primary注解的Bean。

  1. 在注入处声明一个集合,注入所有同类型的Bean。例如:
@Autowired
private List myBeans;

这样Spring会把所有同类型的Bean注入到这个List中,你可以遍历这个List去使用你需要的Bean。

总结

不同包的FeignClient的导入有两种方式:
① 在 @EnableFeignClients 注解中添加basePackages, 指定 FeignClient 所在的包
② 在 @EnableFeignClients 注解中添加clients, 指定具体 FeignClient 的字节码

Gateway 服务网关

为什么需要网关

网关功能:

  • 身份认证和权限校验
  • 服务路由、负载均衡
  • 请求限流

权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
微服务技术栈——实用篇_第23张图片
在 SpringCloud 中网关的实现包括两种:

  • gateway
  • zuul

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

总结

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

gateway快速入门

搭建网关服务的步骤:

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

<dependency>    
  <groupId>org.springframework.cloudgroupId>    
  <artifactId>spring-cloud-starter-gatewayartifactId>
dependency>


<dependency>    
  <groupId>com.alibaba.cloudgroupId>    
  <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
  1. 编写路由配置及nacos地址
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求

微服务技术栈——实用篇_第24张图片

总结:

网关搭建步骤:

  1. 创建项目,引入nacos服务发现和gateway依赖
  2. 配置application.yml,包括服务基本信息、nacos地址、路由

路由配置包括:

  1. 路由id:路由的唯一标示
  2. 路由目标(url):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
  3. 路由断言(predicates):判断路由的规则,
  4. 路由过滤器(filters):对请求或响应做处理

断言工厂

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

  • 路由id : 路由唯一标示
  • uri : 路由目的地,支持特 bl 和 http 两种
  • predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
  • filters : 路由过滤器,处理请求或响应

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factoryi读取并处理,转变为路由判断的条件
Spring 提供了11种基本的 Predicate 工厂:

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

总结

PredicateFactory 的作用是什么?
读取用户定义的断言条件,对请求做出判断
Path=/user/** 是什么含义?
路径是以 /user 开头的就认为是符合的

过滤器工厂

路由过滤器 GatewayFilter

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

过滤器工厂 GatewayFilterFactory

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

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

过滤器

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

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

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

总结

过滤器的作用是什么?
① 对路由的请求或响应做加工处理,比如添加请求头
② 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
① 对所有路由都生效的过滤器

全局过滤器 GlobalFilter

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

public interface GlobalFilter {
    /**
     *  处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
     *
     * @param exchange 请求上下文,里面可以获取Request、Response等信息
     * @param chain 用来把请求委托给下一个过滤器 
     * @return {@code Mono} 返回标示当前过滤器业务结束
     */
    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

总结

全局过滤器的作用是什么?
对所有路由都生效的过滤器,并且可以自定义处理逻辑
实现全局过滤器的步骤?
①实现GlobalFilter接口
②添加@Order注解或实现Ordered接口
③编写处理逻辑

过滤器执行顺序

求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
微服务技术栈——实用篇_第25张图片
过滤器的执行顺序

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

详细内容,可以查看源码:

// 方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()

// 方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链
org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()

总结

路由过滤器、defaultFilter、全局过滤器的执行顺序?
① order值越小,优先级越高
② 当 order 值一样时,顺序是defaultFilter 最先,然后是局部的路由过滤器,最后是全局过滤器
默认过滤器 defaultFilter (default-filters 全局) >
路由过滤器 filters (filters 局部) >
全局过滤器 GlobalFilter (自定义过滤器)

跨域问题处理

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

  • 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
  • 域名相同,端口不同:localhost:8080和localhost8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,
在 gateway 服务的application.yml文件中,添加下面的配置:

spring:
  cloud:
    gateway:
      # 。。。
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:8090"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期
DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
head>
<body>
<pre>
在 gateway 服务的application.yml文件中添加配置
pre>
body>
<script src="https://unpkg.com/axios/dist/axios.min.js">script>
<script>
  axios.get("http://localhost:10010/user/1?authorization=admin")
  .then(resp => console.log(resp.data))
  .catch(err => console.log(err))
script>
html>

总结

CORS跨域要配置的参数包括哪几个?

  • 允许哪些域名跨域?
  • 允许哪些请求头?
  • 允许哪些请求方式?
  • 是否允许使用cookie ?
  • 有效期是多久?

Docker

初识 Docker

什么是Docker

项目部署的问题

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

  • 依赖关系复杂,容易出现兼容性问题
  • 开发、测试、生产环境有差异

微服务技术栈——实用篇_第26张图片

  1. Docker?如何解决依赖的兼容问题的?
  • 将应用的 Libs (函数库)、Deps (依赖)、配置与应用一起打包
  • 将每个应用放到一个隔离容器去运行,避免互相干扰
  1. Linux 操作系统
    1. 内核与硬件交互,提供操作硬件的指令
    2. 系统应用封装内核指令为函数,便于程序员调用
    3. 用户程序基于系统函数库实现功能
    4. Ubuntu:和Centos都是基于Linux内核,只是系统应用不同,提供的函数库有差异
  2. Docker 如何解决不同系统环境的问题?
    1. Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
    2. Dockeri运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
  3. Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
    1. Docke下允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
    2. Docker应用运行在容器中,使用沙箱机制,相互隔离
  4. Docker如何解决开发、测试、生产环境有差异的问题

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

总结

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

  1. 可以将程序及其依赖、运行环境一起打包为一个镜像,
  2. 可以迁移到任意Linux操作系统运行时利用沙箱机制形成隔离容器,各个应用互不干扰
  3. 启动、移除都可以通过一行命令完成,方便快捷

Docker和虚拟机的区别

Docker与虚拟机

虚拟机 是在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在Windows系统里面运行Ubuntu系统,这样就可以运行任意的Ubuntu应用了。
微服务技术栈——实用篇_第27张图片

总结

Docker和虚拟机的差异:
docker是一个系统进程;虚拟机是在操作系统中的操作系统
docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般

镜像和容器

镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
容器(Container.):镜像中的应用程序运行后形成的进程就是容器,只是 Docker 会给容器做隔离,对外不可见。
微服务技术栈——实用篇_第28张图片

Docker 和 DockerHub

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

docker 架构

Docker 是一个CS架构的程序,由两部分组成:
服务端(server) Docker守护进程,负责处理Docker指令,管理镜像、容器等
客户端(client) 通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
微服务技术栈——实用篇_第29张图片

总结

镜像: 将应用程序及其依赖、环境、配置打包在一起
容器:镜像运行起来就是容器,一个镜像可以运行多个容器
Docker 结构:
服务端:接收命令或远程请求,操作镜像或容器
客户端:发送命令或者请求到Docker服务端
DockerHub:
一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry

安装Docker

企业部署一般都是采用Linux操作系统,而其中又数CentOS发行版占比最多,因此我们在CentOS下安装 Docker

安装时遇到的问题

/var/run/yum.pid 已被锁定,PID 为 2985 的另一个程序正在运行
这个错误表示 Yum 已经在运行并且已使用linux系统的锁,以避免出现竞争条件。如果尝试启动另一个 Yum 进程,将会引发此类冲突,并且该进程将无法运行。
为了解决这个问题,您可以尝试以下几个步骤:

  1. 结束正在运行的 Yum 进程:尝试关闭可能已经在运行的Yum进程。您可以使用ps -ef | grep yum 和 kill -9 命令来找到并杀死卡住的状态进程。
  2. 删除 yum.pid 文件: 如果上述步骤不起作用,则可以尝试手动删除/var/run/yum.pid文件。请注意,操作时要谨慎,确保只删除正在使用yum.pid 文件的进程的 PID 码。删除之前,最好先备份文件。
  3. 重启系统:如果上述两种方法都没有解决问题,可以尝试简单地重新启动系统。这应该使得所有的锁定变得可用,同时您可以重新更新或安装需要的软件包。

错误:软件包:docker-ce-rootless-extras-23.0.5-1.el7.x86_64 (docker-ce-stable) 需要:fuse-overlayfs >= 0.7
这个问题表明您的系统缺少所需的软件包或软件包版本过低,因此Docker安装失败。为了解决这些错误,您可以尝试以下几个步骤:

Docker的基本操作

镜像操作

镜像相关命令

镜像名称一般分两部分组成:[repository]:[tag]
在没有指定tag时,默认是latest,代表最新版本的镜像
微服务技术栈——实用篇_第30张图片

总结

镜像操作:
docker irages 查看镜像
docker rmi 删除镜像
docker pull 拉取镜像
docker push 推送镜像
docker save 压缩镜像
docker load 加载压缩镜像
docker build 构建镜像
docker xxx --help docker帮助文档
systemctl status docker 查看docker服务状态
systemctl start docker 启动docker服务
如何开机自启动docker服务

  1. 使用 root 用户或拥有 sudo 权限的用户登录到 Linux 操作系统中。
  2. 打开 systemd 的配置文件 /etc/systemd/system/docker.service.d/override.conf。如果目录或文件不存在,请创建它。
sudo vim /etc/systemd/system/docker.service.d/override.conf
  1. 在 override.conf 文件中添加以下内容:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

以上命令告诉 systemd 在启动 docker 服务时执行指定的命令,这里我们指定了 /usr/bin/dockerd 命令来启动 docker 服务。

  1. 保存并关闭 override.conf 配置文件。
  2. 接下来,使用 systemctl 命令启用 docker 服务并将其设置为自启动:
sudo systemctl enable docker.service
  1. 最后,重新启动 Linux 操作系统以使更改生效。

容器操作

去DockerHub搜索并拉取一个Redis镜像

1.去DockerHub搜索Redis镜像

2.查看Redis镜像的名称和版本
image.png
3.利用docker pull命令拉取镜像
docker pull redis
4.利用docker save命令将redis:latest打包为一个redis.tar包
docker save -o redis.tar redis:latest
5.利用docker rmi删除本地的redis:latest
docker rmi redis:latest
6.利用docker load重新加载redis.tar文件
docker load -i redis.tar

容器相关命令

微服务技术栈——实用篇_第31张图片
创建运行一个Nginx容器
步骤一:去docker hub查看Nginx的容器运行命令

docker run --name nginxName -p 8064:80 -d nginx
# 创建之后,容器会产生唯一id
20a50c792b2ad315bb9dcc9d15a453ee6634c966872ee0fdf1bc114c716de047
# 修改容器的名称 注意:修改之前需要停止容器运行
docker rename old_container_name new_container_name

命令解读:
docker run 创建并运行一个容器
–name 给容器起一个名字,比如叫做mn
-p 将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口
-d 后台运行容器
nginx 镜像名称,例如nginx
步骤二:进入容器。进入我们刚刚创建的 nginx 容器的命令为:

docker exec -it nginxName bash

命令解读:
docker exec 进入容器内部,执行一个命令
-t 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
containerName 要进入的容器的名称
bash 进入容器后执行的命令,bash是一个inux终端交互命令

sed -i 's###g' index.html
# 替换,插入
sed -i 's#

Welcome to nginx!

#

Kelly, I want to be with you!

\n

Welcome to nginx!

#g' index.html sed -i '9s/^/h1 { color: #ff4777; }\n/' index.html

创建并运行一个redis容器,并且支持数据持久化
步骤一:到DockerHub搜索Redis镜像,查看Redis镜像文档中的帮助信息
步骤二:利用docker run命令运行一个Redis容器

docker run --name redisName -p 8065:81 -d redis redis-server --appendonly yes
# 创建之后,容器会产生唯一id
2ded09c13a92dcbaf91bf40d44abe458d372be1d8fb26f05b00cc25c736c02ed
# 进入容器
docker exec -it redisName bash
docker exec -it redisName redis-cli

总结
  1. docker run 创建并运行一个容器
    1. -name 指定容器名称
    2. -p 指定端口映射
    3. -d 让容器后台运行
  2. docker exec 进入容器执行命令
    1. docker exec -it containerName bash 进入容器内部,执行 bash 命令
    2. exec 命令可以进入容器修改文件,但是在容器内修改文件是不推荐的
  3. docker logs 查看容器运行日志
    1. docker logs -f containerName 实时监控 containerName 的日志信息
  4. docker ps 查看所有运行的容器及状态
    1. docker ps -a 添加-a参数查看所有状态的容器
  5. docker rm 删除指定容器
    1. docker rm -f 不能删除运行中的容器,添加 -f 强制删除
  6. docker pause 暂停运行容器
  7. docker unpause 取消暂停运行容器
  8. docker stop 停止容器运行
  9. docker start 启动容器运行

数据卷(容器数据管理)

容器与数据耦合的问题

微服务技术栈——实用篇_第32张图片
数据卷(volume) 是一个虚拟目录,指向宿主机文件系统中的某个目录。
微服务技术栈——实用篇_第33张图片
操作数据卷

docker volume [COMMAND]
# docker volume命令是数据卷操作,根据命令后跟随的command:来确定下一步的操作:
create   创建一个volume
inspect   显示一个或多个volume的信息
ls   列出所有的volume
prune   删除未使用的volume
rm   删除一个或多个指定的volume
[
  {
    "CreatedAt": "2023-05-07T17:35:18+08:00",
    "Driver": "local",
    "Labels": null,
    "Mountpoint": "/var/lib/docker/volumes/html/_data",
    "Name": "html",
    "Options": null,
    "Scope": "local"
  }
]

总结

数据卷的作用:
将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全
数据卷操作:
docker volume create 创建一个数据卷
docker volume Is 列出所有的数据卷
docker volume inspect 查看数据卷详细信息
docker volume rm 删除一个或多个指定的数据卷
docker volume prune 删除未使用的数据卷

挂载数据卷

我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器目录
微服务技术栈——实用篇_第34张图片
创建一个 nginx 容器,修改容器内的html目录内的 inde.html 内容
需求说明:上个案例中,我们进入nginx容器内部,已经知道nginx的html目录所在置/usr/share./nginx./html, 我们需要把这个目录挂载到html这个数据卷上,方便操作其中的内容。
提示:运行容器时使用 -v 参数挂载数据卷
步骤:
① 创建容器并挂载数据卷到容器内的HTML目录

# 如果数据卷不存在,就直接创建,docker会自动创建数据卷
docker run --name nginxName -p 8064:80 -v html:/usr/share/nginx/html -d nginx

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

#查看html数据卷的位置
docker volume inspect html
#进入该目录
cd /var/lib/docker/volumes/html/_data
#修改文件
vim index.html

总结

数据卷挂载方式:
-v volumeName:/targetContainerPath
如果容器运行时volume不存在,会自动被创建出来

将宿主机目录直接挂载到容器

创建并运行一个MySQL容器,将宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的:
-v [宿主机目录] : [容器内目录]
-v [宿主机文件] : [容器内文件]
实现思路如下:
1.将mysqL.tar文件上传到虚拟机,通过load命令加载为镜像
2.创建目录/tmp/mysql/data
3.创建目录/tmp/mysql/conf , 将hmy.cnf文件上传到/tmp/mysql/conf
4.去DockerHub查阅资料,创建并运行MySQL容器,要求:

# 这里需要注意 3306端口已被mysqld占用,所以采用3307端口
docker run --name mysql -e MYSQL_ROOT_PASSWORD=hcy080325... -p 3307:3306 -v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf -v /tmp/mysql/data:/var/lib/mysql  -d mysql:5.7.25

①挂载/tmp/mygl/data到mysql容器内数据存储目录
②挂载/tmp/myql/conf/hmy.cnf到mysql容器的配置文件
③设置MySQL密码
微服务技术栈——实用篇_第35张图片

总结
  1. docker run的命令中通过-v参数挂载文件或目录到容器中:
    1. -v volume名称:容器内目录
    2. -v 宿主机文件:容器内文件
    3. -v 宿主机目录:容器内目录
  2. 数据卷挂载与目录直接挂载的
    1. 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
    2. 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看

Dockerfile自定义镜像

镜像结构

镜像 是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
微服务技术栈——实用篇_第36张图片

总结

镜像是分层结构,每一层称为一个Layer
Baselmage层:包含基本的系统函数库、环境变量、文件系统
Entrypoint :入口,是镜像中应用启动的命令
其它:在Baselmage基础上添加依赖、安装程序、完成整个应用的安装和配置

Dockerfile 语法

什么是Dockerfile

Dockerfile 就是一个文本文件,其中包含一个个的指令(Instruction), 用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层 Layer
微服务技术栈——实用篇_第37张图片
请参考官网文档:

# 构建javaweb镜像
docker build -t javaweb:1.0 .
# 查看镜像
docker images
# 创建并运行javaweb容器
docker run --name web -p 8090:8090 -d javaweb:1.0
# 查看容器
docker ps
# 直接访问 192.168.147.146:8090/hello/count
# 构建javaweb镜像
docker build -t javaweb :2.0 .
# 查看镜像
docker images
# 创建并运行javaweb容器
docker run --name webAlpines -p 8091:8091 -d javaweb:2.0
# 查看容器
docker ps

总结
  1. Dockerfile的本质是一个文件,通过指令描述镜像的构建过程
  2. Dockerfile的第一行必须是FROM,从一个基础镜像来构建
  3. 基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如:java:8-alpine

Docker-Compose

初识 DockerCompose

什么是DockerCompose
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。
微服务技术栈——实用篇_第38张图片
DockerCompose的详细语法参考官网:

安装DockerCompose遇到的问题

在执行Base自动补全命令时出现TCP connection reset by peer

“TCP connection reset by peer” 的错误消息通常表示在尝试连接到远程服务器时遇到了主机或防火墙的问题。一些可能导致该错误的原因包括:

  1. 远程服务器主机出现故障: 由于远程服务器主机由于某种原因崩溃或重启而导致无法提供所需的服务
  2. 防火墙配置不当: 当Base程序需要访问其他服务器上的资源时,可能会被本地防火墙屏蔽,如果防火墙过于严格则会拒绝所有或者部分入站请求。
  3. 网络问题: 偶尔的网络中断、传输中数据包损坏也会导致“TCP connection reset by peer” 错误。

为了解决此问题,您可以尝试以下几个步骤:

  1. 确保您的网络畅通: 您可以通过 ping 命令测试网络并确保你的网络畅通,例如:ping www.baidu.com
  2. 检查您的防火墙: 如果您使用了防火墙,可以暂时将其禁用或者放行 Base 所需的端口;同时也要确保 Base 程序的目标主机和端口都是正确无误的。
  3. 检查远程服务器: 如果错误是从远程服务器返回的,那么您需要检查该服务器是否正常运行并且配置是否正确。
  4. 配置应用程序的超时: 有可能是因网络等原因队列堆积导致TCP连接被重置,你可以尝试增加 base 连接远程服务器的超时参数值以缓解 TCP 连接被中断的问题。例如:设置Base的长连接,默认4小时后无请求断开。
# 首先查看网络的连通性,没有问题
ping www.baidu.com
# 查看防火墙的状态,看一下是否在运行,

禁用防火墙

1.使用防火墙管理工具打开防火墙配置。 在大部分的Linux发行版中,都有一个默认的防火墙管理工具(如firewalld或iptables等)。您可以使用以下命令打开防火墙配置界面, 并记住开启前的配置:

复制代码sudo firewall-cmd --state   #检查当前防火墙状态并确定防火墙所采用的管理器,不同管理器操作可参考相关文档

sudo firewall-cmd --list-all  #列出当前所有的防火墙规则
  1. 关闭防火墙功能:如果您已经决定禁用防火墙,可以运行以下命令关闭防火墙服务:
复制代码sudo systemctl stop firewalld.service     #火墙管理工具为systemd时

sudo service iptables stop                #火墙管理工具为iptables时
  1. 禁用防火墙开机自启动: 您可以通过以下命令来禁止防火墙服务在启动时自动运行:
复制代码sudo systemctl disable firewalld.service   #火墙管理工具为systemd时

sudo chkconfig iptables off                 #火墙管理工具为iptables时

总结

DockerCompose 有什么作用?
帮助我们快速部署分布式应用,无需一个个微服务去构建镜像和部署。

部署微服务集群

# 构建java项目
docker-compose up -d
# 查看运行的容器
docker ps 
# 查看日志
docker-compose logs -f
# 重新启动
docker-compose restart gateway userservice orderservice 

Docker 镜像服务

搭建私有镜像仓库

私有镜像仓库地址 http://192.168.147.146:8080

常见镜像仓库服务

镜像仓库(Docker Registry) 有公共的和私有的两种形式:
公共仓库:例如Docker官方的Docker Hub,国内也有一些云服务商提供类似于Docker Hub的公开服务,比如网易云镜像服务、DaoCloud镜像服务、阿里云镜像服务等。
除了使用公开仓库外,用户还可以在本地搭建私有Docker Registry。企业自己的镜像最好是采用私有Docker
Registry来实现。

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

推送镜像到私有镜像服务必须先tag,步骤如下:
①重新tag本地镜像,名称前缀为私有仓库的地址:192.168.147.146:8080/

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

②推送镜像

docker push 192.168.147.146:8080/nginx:1.0

③拉取镜像

docker pull 192.168.147.146:8080/nginx:1.0

总结
  1. 推送本地镜像到仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
  2. 镜像仓库推送前需要把仓库地址配置到docker服务的daemon,json文件中,被docker信任
  3. 推送使用docker push命令
  4. 拉取使用docker pulla命令

服务异步通讯

初识 MQ

同步通讯

同步通讯和异步通讯

同步调用的问题

微服务间基于Feign的调用就属于同步方式,存在一些问题。
微服务技术栈——实用篇_第39张图片
总结
同步调用的优点:
时效性较强,可以立即得到结果
同步调用的问题:
耦合度高
性能和吞吐能力下降
有额外的资源消耗
有级联失败问题

异步调用方案

异步调用常见实现就是事件驱动模式
优势一:服务解耦
优势二:性能提升,吞吐量提高
优势三:服务没有强依赖,不担心级联失败问题
优势四:流量削峰
总结
异步通信的优点:
耦合度低
吞吐量提升
故障隔离
流量削峰
异步通信的缺点:
依赖于Broker的可靠性、安全性、吞吐能力
架构复杂了,业务没有明显的流程线,不好追踪管理

什么是MQ

MQ(MessageQueue), 中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker
微服务技术栈——实用篇_第40张图片

RabbitMQ 快速入门

RabbitMQ概述和安装

RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:Messaging that just works — RabbitMQ
RabbitMQ的结构和概念
微服务技术栈——实用篇_第41张图片

总结

RabbitMQ 中的几个概念:
Channel 操作MQ的工具
Exchange 路由消消
Queues 缓存消息
Virtual Hosts 虚拟主机,是对queue、exchange等资源的逻辑分组

常见消息模型

MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:

基本消息队列(BasicQueue)
工作消息队列 (WorkQueue)
发布订阅 (Publish、Subscribe), 又根据交换机类型不同分为三种:
Fanout Exchange 广播
Direct Exchange 路由
Topic Exchange 主题

HelloWorld案例

官方的 HelloWorld 是基于最基础的消息队列模型来实现的,只包括三个角色:
publisher 消息发布者,将消息发送到队列queue
queue 消息队列,负责接受并缓存消息
consumer 订阅队列,处理队列中的消息
微服务技术栈——实用篇_第42张图片
总结
基本消息队列的消息发送流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
基本消息队列的消息接收流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消费行为handleDelivery()
5.利用channel将消费者与队列绑定

SpringAMQP

Basic Queue简单队列模型

什么是SpringAMQP

SpringAmqp的官方地址:
微服务技术栈——实用篇_第43张图片
利用SpringAMQP实现HelloWorld中的基础消息队列功能
流程如下:

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

<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-amqpartifactId>
dependency>
  1. 在 publisher 服务中利用 RabbitTemplate 发送消息到simple.queue这个队列
spring:
  rabbitmq:
    host: 192.168.147.146  #主机名
    port: 5672   #端口
    virtual-host: /   #虚拟主机
    username: itzyj   #用户名
    password: hcy080325...  #密码
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
	private RabbitTemplate rabbitTemplate;

    @Test
    public void testsimpleQueve() {
        String queveName "simple.queve";
        String message "hello,spring amqp!";
    	rabbitTemplate.convertAndSend(queueName,message);
    }
}

  1. 在 consumer 服务中编写消费逻辑,绑定 simple.queue 这个队列
spring:
	rabbitmq:
    host: 192.168.147.146  #主机名
    port: 5672  #端口
    virtual-host: /  #虚拟主机
    username: itzyj   #用户多
    password: hcy080325...  #密码
@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg) throws InterruptedException {
        System.out.println("spring消费者接收到消息:【" + msg + "】");
    }
}

总结

什么是AMQP?
应用间消息通信的一种协议,与语言和平台无关。
SpringAMQP如何发送消息?

  1. 引入amgp的starter依赖
  2. 配置RabbitMQ地址
  3. 利用RabbitTemplate的convertAndSend方法

SpringAMQP如何接收消息?
引入 amqp 的 starter 依赖
配置 RabbitMQ 地址
定义类,添加 @Component 注解
类中声明方法,添加 @RabbitListener 注解,方法参数就时消息

Vork Queue工作队列模型

Work queue 工作队列,可以提高消息处理速度,避免队列消息堆积
微服务技术栈——实用篇_第44张图片
模拟 WorkQueue , 实现一个队列绑定多个消费者
基本思路如下:
1.在oublisher服务中定义测试方法,每秒产生50条消息,发送到 simple.queu
2.在consumer服务中定义两个消息监听者,都监听simple.queue队列
3.消费者1每秒处理50条消息,消费者2每秒处理10条消息

总结

Work模型的使用:
多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
通过设置 prefetch 来控制消费者预取的消息数量

发布、订阅模型-Fanout

发布(Publish)、订阅(Subscribe)

发布订阅模式与之前案例的区别就是允许将同一 消息发送给多个消费者。实现方式是加入了exchange(交换机)。
常见exchange类型包括:
Fanout 广播
Direct 路由
Topic 话题
微服务技术栈——实用篇_第45张图片

发布订阅-Fanout Exchange

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的 queue
微服务技术栈——实用篇_第46张图片
利用SpringAMQP演示FanoutExchange的使用
实现思路如下:
1.在consumer服务中,利用代码声明队列、交换机,并将两者绑定
2.在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
3.在oublisher中编写测试方法,向litcast.fanout发送消息
微服务技术栈——实用篇_第47张图片

步骤1:在consumer服务声明Exchange、Queue、Binding微服务技术栈——实用篇_第48张图片
在consumer服务常见一个类,添加@Configuration:注解,并声明FanoutExchange、Queue和绑定
关系对象Binding,代码如下:

@Configuration
public class FanoutConfig {
    //声明 FanoutExchange 交换机
    @Bean
    public FanoutExchange fanoutExchange(){
    	return new FanoutExchange("itcast.fanout");
    }
	//声明第1个队列
	@Bean
	public Queue fanoutQueuel(){
		return new Queve("fanout.queue1");
	}
	//绑定队1和交换机
	@Bean
	public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(fanoutQueve1).to(fanoutExchange);
	}
	// ··略,以相同方式声明第2个队列,并完成绑定
}

步骤2:在consumer服务声明两个消费者
在consumer服务的SpringRabbitListener类中,添加两个方法,分别监听fanout.queue1和fanout.queue2

@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) throws InterruptedException {
    System.out.println("spring消费者接收到fanout.queue1的消息:【" + msg + "】");
}

@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) throws InterruptedException {
    System.out.println("spring消费者接收到fanout.queue2的消息:【" + msg + "】");
}

步骤3:在publisher服务发送消息到FanoutExchange
在publisher服务的SpringAmqpTest类中添加测试方法:

@Test
public void testSendFanoutExchange() throws InterruptedException {
    String exchangeName = "itzyj.fanout";
    Object message = "hello, every one!";
    rabbitTemplate.convertAndSend(exchangeName, "", message);
}

总结

  1. 交换机的作用是什么?

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

  1. 声明队列、交换机、绑定关系的Bean是什么?
  2. 声明队列(Queue):这是保存传入消息的地方。消费者从队列中接收消息并进行处理。
  3. 声明交换机(Exchange):将消息路由到队列的规则设定在交换机中。生产者通过交换机将消息路由到特定队列。
  4. 声明绑定关系(Binding):将一个队列绑定到交换机上。生产者可以朝着特定的交换机发布消息,而消费者可以从相同或不同的队列中获取消息。

在 spring-amqp 框架中,声明队列、交换机和绑定关系有两种方法:一种是直接使用 RabbitMQ 的 API 来进行声明,另一种是使用 Spring Boot 的 AMQP 自动配置功能来创建 Bean 对象实现声明。这些声明的 Bean 包含以下信息:

  1. Queue:这个 Bean 定义了 Queue 的名称、是否持久化存储以及其他属性。
  2. Exchange:这个 Bean 定义了 Exchange 的名称、类型和其他属性。与 Queue 不同,Exchange 通常不会直接与消息进行交互,它主要定义了如何路由消息到相应的队列中。
  3. Binding:这个 Bean 定义了绑定的关系,即一个队列如何绑定到交换机上。

通过这些 Bean 对象,无需手动进行声明,Spring Boot 会自动将这些 Bean 注册到 RabbitMQ 中,并开始消息的传递和处理。
在配置声明队列、交换机和绑定关系的 Bean 时,需要注意队列和交换机名称的唯一性,否则可能会导致冲突和误操作。同时,建议按照业务类型对队列和交换机进行命名,以便在项目庞大之后进行统一管理和协调。

发布订阅-DirectExchange

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

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey 一致的队列

微服务技术栈——实用篇_第49张图片
利用 SpringAMQP 演示 DirectExchange 的使用
实现思路如下:

  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey
  2. 在consumer服务中,编写两个消费者方法,分别监听 direct.queue1 和 direct.queue2
  3. 在publisher中编写测试方法,向itcast.direct2发送消息

步骤1:在 consumer 服务声明 Exchange、Queue

  1. 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue.2
  2. 并利用 @RabbitListener 声明 Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueveBinding(
    value = @Queue(name = "direct.queue1"),
    exchange = @Exchange(name = "itzyj.direct",type = ExchangeTypes.DIRECT),
    key = {"red","blue"}
))
public void listenDirectQuevel(String msg){
    System.out.println("消费者1 接收到Direct消息:【"+msg+"】");
}
@RabbitListener(bindings = @QueveBinding(
    value = @Queue(name = "direct.queue2"),
    exchange = @Exchange(name = "itzyj.direct",type = ExchangeTypes.DIRECT),
    key = {"red","yellow"}
))
public void listenDirectQueve2(String msg){
	System.oUt.println("消费者2 接收到Direct消息:【"+msg+"】");
}

步骤2:在 publisher 服务发送消息到DirectExchange
在publisher服务的SpringAmqpTest类中添加测试方法

    @Test
    public void testSendDirectExchange() throws InterruptedException {
        String routingKey = "red";
        String exchangeName = "itzyj.direct";
        String message = "hello, " + routingKey + "!";
        rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
    }

总结
描述下Direct交换机与Fanout交换机的差异?
Fanout 交换机将消息路由给每一个与之绑定的队列
Direct 交换机根据RoutingKey判断路由给哪个队列
如果多个队列具有相同的RoutingKey, 则与Fanout功能类似
基于 @RabbitListener 注解声明队列和交换机有哪些常见注解?
@Queue
@Exchange

发布订阅-TopicExchange

TopicExchange 与DirectExchange 类似,区别在于routing Key必须是多个单词的列表,并且以 . 分割。
Queue与Exchange指定BindingKey时可以使用通配符:
# 代指0个或多个单词
* 代指一个单词
微服务技术栈——实用篇_第50张图片利用 SpringAMQP 演示TopicExchange的使用
实现思路如下:

  1. 并利用@RabbitListener声明Exchange、Queue、RoutingKey
  2. 在consumer服务中,编写两个消费者方法,分别监听topic…queue1和topic.queue2
  3. 在publisher中编写测试方法,向litcast.topic 发送消息

步骤1:在consumer服务声明Exchange、Queue

  1. 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
  2. 并利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "itzyj.topic", type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenTopicQueue1(String msg) {
        System.out.println("消费者1 接收到Direct消息:【" + msg + "】");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "itzyj.topic", type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listenTopicQueue2(String msg) {
        System.out.println("消费者2 接收到Direct消息:【" + msg + "】");
    }

步骤2:在publisher服务发送消息到TopicExchange
在publisher服务的SpringAmqpTesta类中添加测试方法:

  @Test
    public void testSendTopicExchange() throws InterruptedException {
        String routingKey = "china.news";
        String exchangeName = "itzyj.topic";
        String message = "hello, " + routingKey + "!";
        rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
    }

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

  1. Direct交换机:直接绑定。Direct 交换机会按照消息中指定的 routing key 将消息路由到一个指定的队列中。即在生产者发布消息时,需要指定消息的 routing key 值,并将其绑定到对应的 Queue 上。在消费者端,直接监听该 Queue 即可接收并处理该 Queue 上的消息。Direct 交换机可以简单地理解为将 RoutingKey 和 Queue 进行一一映射,从而将消息路由到对应的 Queue 中。
  2. Topic交换机:主题匹配。Topic 交换机将消息交给那些 binding key 与 routing key 匹配的 Queue 中。在创建交换机和绑定时,可以使用通配符 ‘_’ (单词)或 ‘#’(任意单词)来模糊匹配 RoutingKey 和 BindingKey。 '’ _可以用来替代一个单词;'#'可以替代零个或多个单词。因此,在使用 Topic 交换机时,生产者在业务逻辑处理完后会将消息根据某种参数转换成 Topic 格式的 routing key,在发送时作为消息的 routing key 发布;消费者方面,则根据自身业务场景针对性地订阅各自关注的消息,采用相应的 Topic 模糊匹配规则来接收符合要求的消息。

消息转换器

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

Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter类型的Bean即可。推荐用SON方式序列化,步骤如下:

<dependency>
  <groupId>com.fasterxml.jackson.dataformatgroupId>
  <artifactId>jackson-dataformat-xmlartifactId>
  <version>2.9.10version>
dependency>
@Bean
public MessageConverter jsonMessageConverter(){
	return new Jackson2JsonMessageConverter();
}

总结

SpringAMQP中消息的序列化和反序列化是怎么实现的?
· 利用MessageConverter 实现的,默认是JDK的序列化
· 注意发送方与接收方必须使用相同的 MessageConverter

分布式搜索

初识 elasticsearch

了解 ES

什么是 elasticsearch

elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch 结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。
elasticsearch 是elastic stack的核心,负责存储、搜索、分析数据。
微服务技术栈——实用篇_第51张图片
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由Doug Cutting于1999年研发。
官网地址:https://lucene.apache.org/
Lucene的优势:
易扩展
高性能(基于倒排索引)
Lucene的缺点:
只限于ava语言开发
学习曲线陡峭
不支持水平扩展
elasticsearch的发展
2004年Shay Banon基于Lucene开发了Compass
2010年Shay Banon重写了Compass,取名为Elasticsearch。
官网地址:
目前最新的版本是:7.12.1
相比与lucene,elasticsearch具备下列优势:
·支持分布式,可水平扩展
·提供Restful接口,可被任何语言调用

总结
  1. 什么是 elasticsearch

一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

  1. 什么是 elastic stack (ELK) ?

是以elasticsearch为核心的技术栈,包括Beats、Logstash、kibana、elasticsearch

  1. 什么是Lucene ?

    是Apache的开源搜索引擎类库,提供了搜索引擎的核心API

倒排索引

正向索引和倒排索引

传统数据库(如MySQL) 采用正向索引,例如给下表(tb_goods)中的id创建索引:

微服务技术栈——实用篇_第52张图片
elasticsearch采用倒排索引:
·文档(document):每条数据就是一个文档
·词条(term):文档按照语义分成的词语
微服务技术栈——实用篇_第53张图片

总结
  1. 什么是文档和词条?

·每一条数据就是一个文档
·对文档中的内容分词,得到的词语就是词条

  1. 什么是正向索引?

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

  1. 什么是倒排索引?

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

es 的一些概念

文档

elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。
索引
索引(index): 相同类型的文档的集合
映射(mapping): 索引中文档的字段约束信息,类似表的结构约束
微服务技术栈——实用篇_第54张图片
架构
Mysql: 擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch: 擅长海量数据的搜索、分析、计算
微服务技术栈——实用篇_第55张图片

总结

文档:一条数据就是一个文档,es中是json格式
字段:Json文档中的字段
索引:同类型文档的集合
映射:索引中文档的约束,比如字段名称、类型
elasticsearch与数据库的关系:
数据库负责事务类型操作
elasticsearch: 负责海量数据的搜索、分析、计算

安装es、kibana

** 问题: Kibana server is not ready yet**
Elasticsearch 尚未启动:Kibana 依赖 Elasticsearch 来存储和索引数据。
在启动Kibana时首先启动Elasticsearch

# 启动Elasticsearch
docker start es
# 启动Kibana
docker start kibana
# 查看启动状态
docker ps

分词器

https://github.com/medcl/elasticsearch-analysis-ik
es 在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。
安装IK分词器

ik分词器-拓展词库
要拓展ik分词器的词库,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:


DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">

<properties>
  <comment>IK Analyzer扩展配置comment>
  
  <entry key="ext_dict">ext.dicentry>
properties>

ik分词器-停用词库
要禁用某些敏感词条,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:


DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">

<properties>
	<comment>IK Analyzer扩展配置comment>
  <!-用户可以在这里配置自己的扩展字典-->
  <entry key="ext_dict">ext.dicentry>
  
	<entry key="ext_stopwords">stopword.dicentry>
properties>

测试it分词器

# 测试es的连通性
GET /

# IK分词器
# IK分词器有几种模式
# ik_smart: 智能切分,粗粒度
# ik_max_word:最细切分,细粒度
POST /_analyze
{
  "text": "kelly是最棒的",
  "analyzer": "ik_max_word"
}

POST /_analyze
{
  "text": "人生若有你相伴,盛世美好如春来",
  "analyzer": "ik_smart"
}

总结
  1. 分词器的作用是什么?

创建倒排索引时对文档分词
用户搜索时,对输入的内容分词

  1. IK分词器有几种模式?

ik_smart: 智能切分,粗粒度
ik_max_word:最细切分,细粒度

  1. IK分词器如何拓展词条?如何停用词条?

利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
在词典中添加拓展词条或者停用词条

索引库操作

mapping 映射属性

mapping属性

mapping是对索引库中文档的约束,常见的mapping属性包括:
·type:字段数据类型,常见的简单类型有:
·字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
数值:long、integer、short、byte、double、float
·布尔:boolean
·日期:date
·对象:object
index:是否创建索引,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段

总结

mapping常见属性有哪些?
type:数据类型
index:是否索引
analyzer:分词器
properties:子字段
type常见的有哪些?
字符串:text、keyword
数字:long、integer、short.、byte、double、float
布尔:boolean
日期:date
对象:object

索引库的 CRUD

创建、查询、删除、修改索引库
ES中通过Restfuli请求操作索引库、文档。请求内容用DSL语句来表示。
创建 PUT /索引库名
查询 GET /索引库名
删除 DELETE /索引库名
索引库是禁止修改的,一旦创建就无法进行修改,但是可以对其添加新的字段,字段名不能与索引库中的字段重复,否则它会认为你要修改字段,会产生报错
添加字段 PUT /索引库名/_mapping

# 索引库操作
# 创建  PUT /索引库名
PUT /itzyj
{
  "mappings": {
    "properties": {
      "info": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "email": {
        "type": "keyword",
        "index": false
      },
      "name": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "keyword"
          },
          "lastName": {
            "type": "keyword"
          }
        }
      },
      "age": {
        "type": "integer",
        "index": false
      },
      "weight": {
        "type": "double",
        "index": false
      },
      "height": {
        "type": "double",
        "index": false
      },
      "score": {
        "type": "double"
      }
    }
  }
}

# 查询  GET /索引库名
GET /itzyj

# 删除  DELETE /索引库名
DELETE /itzyj

# 添加字段 PUT /索引库名/_mapping
PUT /itzyj/_mapping
{
  "properties": {
    "isMarried": {
      "type": "boolean",
      "index": false
    }
  }
}

总结

索引库操作有哪些?
创建 PUT /索引库名
查询 GET /索引库名
删除 DELETE /索引库名
添加字段 PUT /索引库名/_mapping

文档操作

新增文档

POST /索引库名/_doc/文档id
如果不写文档id,它就会认为你没有id,es会随机生成一个id

查询文档

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

删除文档

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

修改文档

方式一:全量修改,会删除旧文档,添加新文档 ,如果id不存在,就是添加新文档
PUT /索引库名/_doc/文档id
方式二:增量修改,修改指定字段值
POST /索引库名/_update/文档id

# 文档操作有哪些?
# 创建文档:P0ST   /索引库名/_doc/文档id{json文档}
POST /itzyj/_doc/1
{
  "age": 24,
  "email": "[email protected]",
  "height": 177,
  "info": "Kelly的最爱",
  "isMarried": false,
  "name": {
    "firstName": "燕军",
    "lastName": "赵"
  },
  "score": [98,96,70],
  "weight": 126
}

# 查询文档:GET   /索引库名/_doc/文档id
GET /itzyj/_doc/1

# 删除文档:DELETE /索引库名/_doc/文档id
DELETE /itzyj/_doc/1

# 修改文档:
# 全量修改:PUT  /索引库名/_doc/文档id{json文档}
PUT /itzyj/_doc/1
{
  "age": 23,
  "email": "[email protected]",
  "height": 177,
  "info": "zyj的最爱",
  "isMarried": false,
  "name": {
    "firstName": "凯丽",
    "lastName": "王"
  },
  "score": [98,96,100],
  "weight": 98
}

# 增量修改:POST /索引库名/_update/文档id("doc":{字段h})
POST /itzyj/_update/1
{
  "doc": {
    "info": "海上月是天上月,眼前人是心上人。向来心是看客心,奈何人是剧中人。"
  }
}

总结

文档操作有哪些?
创建文档:P0ST   /索引库名/_doc/文档id{json文档}
查询文档:GET   /索引库名/doc/文档id
删除文档:DELETE /索引库名/doc/文档id
修改文档:
全量修改:PUT  /索引库名/doc/文档id{json文档}
增量修改:POST /索引库名/_update/文档id(“doc”:{字段h})

RestAPI

RestClient操作es索引库_编程彦祖的博客-CSDN博客

RestClient 索引库操作

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/quide/en/elasticsearch/client/index.html
利用 JavaRestClient 实现创建、删除索引库,判断索引库是否存在
根据酒店数据创建索引库,索引库名为hotel,mapping属性根据数据库结构定义。
基本步骤如下:

  1. 导入资料Demo
  2. 分析数据结构,定义mapping属性
  3. 初始化JavaRestClient
    1. 引入es的RestHighLevelclient依赖:
<dependency>
  <groupId>org.elasticsearch.clientgroupId>
  <artifactId>elasticsearch-rest-high-level-clientartifactId>
dependency>
  1. 因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
<properties>
  <java.version>1.8java.version>
  <elasticsearch.version>7.12.1elasticsearch.version>
properties>
  1. 初始化RestHighLevelClient:
RestHighLevelclient client = new RestHighLevelclient(
    Restclient.builder(HttpHost.create("http://192,168,147.146:9200"))
);
  1. 利用JavaRestClient创建索引库
@Test
void testCreateHotelIndex()throws IOException{
    // 1.创建Requset对象
    CreateIndexRequest request = new CreateIndexRequest("hotel");
    // 2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
    request.source(MAPPING_TEMPLATE,XContentType.JSON);
    // 3.发起请求
    client.indices().create(request,RequestOptions.DEFAULT);
}

  1. 利用JavaRestClient删除索引库
@Test
void testDeleteHotelIndex() throws IOException{
    // 1.创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    // 2.发起请求
    client.indices().delete(request, RequestOptions.DEFAULT);
}
  1. 利用JavaRestClient判断索引库是否存在
@Test
void testExistsHotelIndex() throws IOException{
    // 1.创建Request对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    // 2.发起请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    System.out.println(exists ? "hotel 存在" : "hotel 不存在");
}

微服务技术栈——实用篇_第56张图片微服务技术栈——实用篇_第57张图片

总结

索引库操作的基本步骤:
·初始化RestHighLevelClient
·创建XxxIndexRequest XXX是CREATE、Get、Delete
·准备DSL(CREATE时需要)
·发送请求。调用RestHighLevelClient#indices().Xxx)方法
Xxx是create、exists、delete

RestClient 操作文档

新增文档
// 新增文档
@Test
void testAddDocument() throws IOException {
    //根据id查询酒店数据
    Hotel hotel = hotelService.getById(61083L);
    //转换成文档类型
    HotelDoc hotelDoc = new HotelDoc(hotel);
    //准备request对象
    IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
    //将hotel转换为json格式
    request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
    //发送请求
    client.index(request, RequestOptions.DEFAULT);

}

查询文档
// 根据id查询文档
@Test
void testGetDocumentById() throws IOException {
    GetRequest request = new GetRequest("hotel","61083");
    //发送请求,得到响应
    GetResponse response = client.get(request,RequestOptions.DEFAULT);
    String json = response.getSourceAsString();
    HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);
    System.out.println(hotelDoc);
}

删除文档
// 根据id删除文档
@Test
void testDelDocument() throws IOException {
    DeleteRequest request = new DeleteRequest("hotel","61083");
    client.delete(request,RequestOptions.DEFAULT);
}

修改文档
// 更新文档 (全局更新,局部更新)
//局部更新
@Test
void testUpdateDocument() throws IOException {
    UpdateRequest request = new UpdateRequest("hotel","61083");
    //准备修改的字段请求参数
    request.doc(
            "price" , "999",
            "starName" , "4星"
    );
    client.update(request,RequestOptions.DEFAULT);
}

批量导入文档
// 批量添加文档
@Test
void testBulkRequest() throws IOException {
    List<Hotel> hotels = hotelService.list();
    //准备request对象,BulkRequest实际是将多个index请求整合起来
    BulkRequest request = new BulkRequest();
    for (Hotel hotel : hotels) {
        HotelDoc hotelDoc = new HotelDoc(hotel);
        request.add(new IndexRequest("hotel")
                .id(hotelDoc.getId().toString())
                .source(JSON.toJSONString(hotelDoc),XContentType.JSON));
    }
    //发送请求
    client.bulk(request,RequestOptions.DEFAULT);
}

批量删除文档
// 批量删除文档
@Test
void testBatchDeleteDocument() throws IOException {
    // 需要删除的文档id列表
    List<String> ids = Arrays.asList("309208", "396506", "432335");
    BulkRequest bulkRequest = new BulkRequest();
    for (String id : ids) {
        DeleteRequest request = new DeleteRequest("hotel", id);
        bulkRequest.add(request);
    }
    BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
    System.out.println(bulkResponse.status());
}

批量更新文档
// 批量更新文档
@Test
void testBatchUpdateDocument() throws IOException {
    // 需要更新的文档id列表和更新内容
    Map<String, Map<String, Object>> documentUpdates = new HashMap<>();
    Map<String, Object> docFields = new HashMap<>();
    docFields.put("price", 450);
    docFields.put("starName", "5星级");
    documentUpdates.put("56201", docFields);
    
    Map<String, Object> docFields2 = new HashMap<>();
    docFields2.put("price", 200);
    docFields2.put("starName", "3星级");
    documentUpdates.put("56214", docFields2);
    
    BulkRequest bulkRequest = new BulkRequest();
    for (Map.Entry<String, Map<String, Object>> entry : documentUpdates.entrySet()) {
        UpdateRequest request = new UpdateRequest("hotel", entry.getKey()).doc(entry.getValue());
        bulkRequest.add(request);
    }
    BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
    System.out.println(bulkResponse.status());
}

条件查询 和 分页查询
//条件查询
@Test
public void testGetDocumentByCondition() throws IOException {
    // 构造查询条件
    QueryBuilder queryBuilder = QueryBuilders.boolQuery()
            .must(QueryBuilders.matchQuery("name", "北京"))
            .must(QueryBuilders.termQuery("starName", "二钻"))
            .mustNot(QueryBuilders.termQuery("price", 300));

    SearchRequest searchRequest = new SearchRequest("hotel");
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.query(queryBuilder);
    searchSourceBuilder.from(0);
    searchSourceBuilder.size(10);
    searchRequest.source(searchSourceBuilder);

    SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    System.out.println(searchResponse);
}

//分页查询
@Test
public void testGetDocumentByPage() throws IOException {
    // 按价格升序排序,从第5个开始,查询10个文档
    SearchRequest searchRequest = new SearchRequest("hotel");
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.sort("price", SortOrder.ASC);
    searchSourceBuilder.from(4);
    searchSourceBuilder.size(10);
    searchRequest.source(searchSourceBuilder);

    SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    System.out.println(searchResponse);
}

总结

文档操作的基本步骤:

  1. 初始化RestHighLevelClient
  2. 创建XxxRequest。XXX是Index、Get、Update、Delete
  3. 准备参数(Index和Update时需要)
  4. 发送请求。调用RestHighLevelClient#.XXx(O方法,Xxx是index、get、update、delete
  5. 解析结果(Get时需要)

elasticsearch 搜索功能

DSL查询文档

DSL查询分类

DSL Query 的分类

Elasticsearch 提供了基于JSON的DSL来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,一般测试用。例如:match_all

# 查询所有 默认会查询10条数据
GET /hotel/_search
{
  "query":{
    "match_all":{
      
		}
  }
}	

全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
match查询:全文检素查询的一种,会对用户输入内容分词,然后去倒排索引库检索
match_query
multi_match 与match查询类似,只不过允许同时查询多个字段
multi_match_query

# 全文检索
GET /hotel/_search
{
  "query":{
    "match":{
      "all": "外滩如家"
		}
  }
}	
GET /hotel/_search
{
  "query":{
    "multi_match": {
      "query": "外滩如家",
      "fields": ["brand","name","business"]
    }
  }
}	

精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean等类型字段。例如:
range 根据值范围查询
term 根据词条精确查询

# 精确查询
# term 查询
GET /hotel/_search
{
  "query":{
    "term": {
      "city": {
        "value": "北京"
      }
    }
  }
}	
# range 查询
# [100,200]
GET /hotel/_search
{
  "query":{
    "range": {
      "price": {
        "gte": 100,
        "lte": 200
      }
    }
  }
}
# (100,200)
GET /hotel/_search
{
  "query":{
    "range": {
      "price": {
        "gt": 100,
        "lt": 200
      }
    }
  }
}	

地理(geo)查询:根据经纬度查询。例如:
geo_distance 查询到指定中心点小于某个距离值的所有文档
geo_bounding_box 查询geo_point值落在某个矩形范围的所有文档

# 地理查询
# geo_bounding_box  
GET /hotel/_search
{
  "query":{
    "geo_bounding_box": {
      "FIELD": {
        "top_left": {
          "lat": 31.1,
          "lon": 121.5
        },
        "bottom_right": {
          "lat": 30.9,
          "lon": 129.9
        }
      }
    }
  }
}	
# geo_distance
GET /hotel/_search
{
  "query": {
    "geo_distance": {
      "distance": "5km",
      "location": "31.21,121.5"
    }
  }
}	

复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
bool 一个成多个查询子句的组合
function_score 算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价
相关性算分
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score), 返回结果时按照分值降序排列。
微服务技术栈——实用篇_第58张图片
使用function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。
微服务技术栈——实用篇_第59张图片
复合查询Boolean Query
布尔查询是一个成多个查询子句的组合。子查询的组合方式有:
must: 必须匹配每个子查询,类似“与“
should: 选择性匹配子查询,类似"或“
must_not: 必须不匹配,不参与算分,类似“非”
filter: 必须匹配,不参与算分

# 复合查询
# function_score  算分函数查询
# 给“如家”这个品牌的酒店排名靠前一些
GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "all": "外滩"
        }
      },
      "functions": [
        {
          "filter": {
            "term": {
              "brand": "如家"
            }
          },
          "weight": 10
        }
      ],
      "boost_mode": "sum"
    }
  }
}	

# 利用bool查询实现功能
# 搜索名字包含"如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "如家"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gt": 400
            }
          }
        }
      ], 
      "filter": [
        {
          "geo_distance": {
            "distance": "10km",
            "location": {
              "lat": 31.21,
              "lon": 121.5
            }
          }
        }
      ]
    }
  }
}


总结

GET /indexName/_search
{
  "query":{
    "查询类型":{
  		"查询条件": "条件值"
		}
  }
}	

  1. match 和 multi_match 的区别是什么?

match 根据一个字段查询
multi_match 根据多个字段查询,参与查询字段越多,查询性能越差

  1. 精确查询常见的有哪些?

term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
range查询:根据数值范国查询,可以是数值、日期的范国

  1. elasticsearch中的相关性打分算法是什么?

TF-IDF 在elasticsearch5.0之前,会随着词频增加而越来越大
8M25 在elasticsearch5.0之后,会题着词频增加而增大,但增长曲线会趋于水平

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

过滤条件:哪些文档要加分
算分函数:如问计算function score
加权方式:function score与query score如何运算

  1. bool 查询有几种逻辑关系?

must 必须匹配的条件,可以理解为 “与”
should 选择性匹配的条件,可以理解为 “或”
must_not 必须不匹配的条件,不参与打分
filter 必须匹配的条件,不参与打分

搜索结果处理

排序

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

# 排序
# 对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score": "desc"
    },
    {
      "price": "asc"
    }
  ]
}
# 实现对酒店数据按照到你的位置坐标的距离升序排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 31.175445,
          "lon": 121.44688
        },
        "order": "asc",
        "unit": "km"
      }
    }
  ]
}

分页

elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch中通过修改from、size参数来控制要返回的分页结果:

# 分页查询
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": "asc"
    }
  ],
  "from": 0,
  "size": 10
}

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

  1. 首先在每个数据分片上都排序并查询前1000条文档。
  2. 然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
  3. 最后从这1000条中,选取从990开始的10条文档
    如果搜索页数过深,或者结果集 (from+size)越大,对内存和CPU的消凭也越高。因此ES设定结果集查询的上限是10000

针对深度分页,ES提供了两种解决方案:
search after: 分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
scroll: 原理将排序数据形成快照,保存在内存。官方已轻不推荐使用。

总结

from + size:
优点:支持随机翻页
缺点:深度分页问题,默认查询上限 (from+size) 是10000
场景:百度、京东、谷歌、淘宝这样的随机分页搜索
after search:
优点:没有查询上限〔单次查询的size不超过10000)
缺点:只能向后逐页查询,不支持随机翻页
场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:
优点:没有查询上限(单次查询的size不超过10000)
缺点:会有额外内存消耗,并且搜索结果是非实时的
场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用afer search方案

高亮

高亮:就是在搜索结果中把搜索关键字突出显示。
原理是这样的:
将摆索结果中的关细学用标签标记出来
在页而中给标签添加CSS样式

# 高亮查询 默认情况下,搜索字段必须与高亮字段保持一致,才能实现高亮
GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "require_field_match": "false"
      }
    }
  }
}

总结
# 整体查询处理
GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "如家"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gt": 400
            }
          }
        }
      ], 
      "filter": [
        {
          "geo_distance": {
            "distance": "10km",
            "location": {
              "lat": 31.21,
              "lon": 121.5
            }
          }
        }
      ]
    }
  },
  "from": 0,
  "size": 10,
  "sort": [
    {
      "score": "desc"
    },
    {
      "price": "asc"
    },
    {
      "_geo_distance": {
        "location": {
          "lat": 31.175445,
          "lon": 121.44688
        },
        "order": "asc",
        "unit": "km"
      }
    }
  ],
  "highlight": {
    "fields": {
      "name": {
        "require_field_match": "false"
      }
    }
  }
}

RestClient 查询文档

快速入门

RestAPI 中其中构建DSL是通过HighLevelRestClient中的resource()来实现的,具中包含了查询、排序、分页、高亮等所有功能
RestAP1中具中构建查询条件的核心部分是由一个名为QueryBuilders的工具类提供的,具中包含了各种查询方法

//查询数据
@Test
void testMathAll() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    request.source().query(QueryBuilders.matchAllQuery());
    //发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //解析响应
    SearchHits searchHits = response.getHits();
    //获取搜索数据条数
    long value = searchHits.getTotalHits().value;
    System.out.println("搜索数据条数:" + value);
    //将hits数据数组中,进行遍历
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit : hits) {
        //获取文档
        String json = hit.getSourceAsString();
        //反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println("数据:" + hotelDoc);
    }

    //System.out.println(response);
}

总结

查询的基本步骤是:

  1. 创建SearchRequest.对象
  2. 准备Request…source(),也就是DSL。

QueryBuilders 来构建查询条件
传入Request.source() 的 query() 方法

  1. 发送请求,得到结果
  2. 解析结果(参考JSON结果,从外到内,逐层解析)
全文检索查询

全文检素的matchi和multi_match查询与match_all的AP基本一致。差别是查询条件,也就是query的部分。

private void HotelRequest(SearchRequest request) throws IOException {
    //发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //解析响应
    SearchHits searchHits = response.getHits();
    //获取搜索数据条数
    long value = searchHits.getTotalHits().value;
    System.out.println("搜索数据条数:" + value);
    //将hits数据数组中,进行遍历
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit : hits) {
        //获取文档
        String json = hit.getSourceAsString();
        //反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        //高亮结果解析
        hotelDoc.setName(testHighlighter(hit));

        System.out.println("数据:" + hotelDoc);
    }
    //System.out.println(response);
}
//全文检索查询
@Test
void testMath() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL,通过Math
    //单字段查询
    request.source().query(QueryBuilders.matchQuery("all",  "如家"));
    //多字段查询
    request.source().query(QueryBuilders.multiMatchQuery("如家","name","business"));
    //发送请求
    HotelRequest(request);

}

private void HotelRequest(SearchRequest request) throws IOException {
        //发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //解析响应
        SearchHits searchHits = response.getHits();
        //获取搜索数据条数
        long value = searchHits.getTotalHits().value;
        System.out.println("搜索数据条数:" + value);
        //将hits数据数组中,进行遍历
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            //获取文档
            String json = hit.getSourceAsString();
            //反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("数据:" + hotelDoc);
        }
        //System.out.println(response);
    }

精确查询

精确查询常见的有term查询 和 range查询,同样利用 QueryBuilders 实现

//精确查询
@Test
void testTerm() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    //词条查询
    request.source().query(QueryBuilders.termQuery("city",  "杭州"));
    //范围查询
    request.source().query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
    //发送请求
    HotelRequest(request);

}

地理查询
//地理查询
    @Test
    void testGeo() throws IOException {
        //请求request对象
        SearchRequest request = new SearchRequest("hotel");
        //准备DSL
        //查询到指定中心点小于某个距离值的所有文档
        request.source().query(QueryBuilders.geoDistanceQuery("location")
                .point(31.21,121.5)
                .distance(5, DistanceUnit.KILOMETERS));

        //查询geo_point值落在某个矩形范围的所有文档
        request.source().query(QueryBuilders.geoBoundingBoxQuery("location")
                .setCorners(31.1,121.5,30.9,129.9));
        //发送请求
        HotelRequest(request);

    }

复合查询
//复合查询
@Test
void testBool() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    //创建Bool查询
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    //添加must条件
    boolQuery.must(QueryBuilders.termQuery("city","上海"));
    //添加filter条件
    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
    request.source().query(boolQuery);
    //发送请求
    HotelRequest(request);

}

//# function_score  算分函数查询
//# 给“如家”这个品牌的酒店排名靠前一些
@Test
void testFunctionScore() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    //创建function_score查询
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
            .must(QueryBuilders.matchQuery("all", "外滩"));

    FunctionScoreQueryBuilder.FilterFunctionBuilder[] filterFunctions = {
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                    QueryBuilders.termQuery("brand", "如家"),
                    new WeightBuilder().setWeight(10))
    };

    FunctionScoreQueryBuilder functionScoreQueryBuilder =
            QueryBuilders.functionScoreQuery(boolQuery, filterFunctions)
                        .boostMode(CombineFunction.SUM);


    request.source().query(functionScoreQueryBuilder);
    //发送请求
    HotelRequest(request);


}


//# 利用bool查询实现功能
//# 搜索名字包含"如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
@Test
void testBoolTest() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    //创建Bool查询
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    boolQuery.must(QueryBuilders.matchQuery("name", "如家"))
            .mustNot(QueryBuilders.rangeQuery("price").gt(400))
            .filter(QueryBuilders.geoDistanceQuery("location")
                    .point(31.21, 121.5)
                    .distance(10, DistanceUnit.KILOMETERS));

    request.source().query(boolQuery);
    //发送请求
    HotelRequest(request);
}

排序、分页、高亮

搜索结果的排序和分页是与quey同级的参数

//排序、分页、高亮
@Test
void testPage() throws IOException {
    int page = 1;
    int size = 10;
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    request.source().query(QueryBuilders.termQuery("city",  "上海"));

    // 排序
    request.source().sort("price", SortOrder.ASC);
    // 分页
    request.source().from((page-1)*size).size(size);
    // 高亮
    request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

    //发送请求
    HotelRequest(request);

}

//# 排序
//# 对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
@Test
void testSort() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    request.source().query(QueryBuilders.matchAllQuery());

    // 排序
    request.source().sort("score", SortOrder.DESC)
                    .sort("price", SortOrder.ASC);

    //发送请求
    HotelRequest(request);

}

//# 实现对酒店数据按照到你的位置坐标的距离升序排序
@Test
void testSortAndGeo() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    request.source().query(QueryBuilders.matchAllQuery());

    // 排序
    request.source().sort(SortBuilders.geoDistanceSort("location", new GeoPoint().reset(31.175445,121.44688))
            .order(SortOrder.ASC)
            .unit(DistanceUnit.DEFAULT));

    //发送请求
    HotelRequest(request);

}

//分页
@Test
void testPageDemo() throws Exception {
    int page = 1;
    int size = 10;

    SearchRequest request = new SearchRequest("hotel");
    request.source().query(QueryBuilders.matchAllQuery())
                    .sort("price", SortOrder.ASC)
                    .from((page - 1) * size).size(size);

    HotelRequest(request);

}

//高亮
@Test
void testHighlighterDemo() throws Exception {
    SearchRequest request = new SearchRequest("hotel");
    request.source().query(QueryBuilders.matchQuery("all", "如家"))
            .highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

    HotelRequest(request);

}
//高亮结果解析
String testHighlighter(SearchHit hit) {
    //获取高亮结果
    Map<String, HighlightField> highlightFields = hit.getHighlightFields();
    if (!CollectionUtils.isEmpty(highlightFields)) {
        //根据字段名获取高亮结果
        HighlightField highlightField = highlightFields.get("name");
        if (highlightField != null) {
            //获取高亮值
            return highlightField.getFragments()[0].string();
        }
    }
    return "";
}

总结

所有搜索DSL的构建,记住一个API —— SearchRequest的 source() 方法。
高亮结果解析是参考JSON结果,逐层解析

黑马旅游案例

酒店搜索和分页

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

  1. 定义实体类,接收前端请求
  2. 定义controller接口,接收贡面请求,调用IHotelService的search方法
  3. 定义IHotelService中的search方法,利用match查询实现根据关键字搜索酒店信息
//查询所有
@Override
public PageResult search(RequestParams params){
    try {
        //请求request对象
        SearchRequest request = new SearchRequest("hotel");
        //构建query
        buildBasicQuery(params, request);
        //分页功能
        int page = params.getPage();
        int size = params.getSize();
        request.source().from((page - 1) * size).size(size);
        //周边查询(地理查询)
        String location = params.getLocation();
        if (StringUtils.isNotBlank(location)) {
            request.source().sort(SortBuilders
                    .geoDistanceSort("location",new GeoPoint(location))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS));
        }

        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //发送请求
        return HotelResponse(response);
    } catch (IOException e) {
        throw new RuntimeException("搜索数据失败",e);
    }
}

酒店结果过滤

过滤条件包括:
city 精确匹配
brand 精确匹配
starName 精确匹配
price 范围过滤
注意事项:
多个条件之间是AND关系,组合多条件用BooleanQuery
参数存在才需要过滤,做好非空判断

//构建query
private static void buildBasicQuery(RequestParams params, SearchRequest request) {
    //准备DSL
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    request.source().query(boolQuery);
    
    String key = params.getKey();
    if (!StringUtils.isNotBlank(key)) {
        boolQuery.must(QueryBuilders.matchAllQuery());
    }else {
        boolQuery.must(QueryBuilders.matchQuery("all", params.getKey()));
    }
    //brand 精确匹配
    if (StringUtils.isNotBlank(params.getBrand())){
        boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
    }
    //starName 精确匹配
    if (StringUtils.isNotBlank(params.getStarName())){
        boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
    }
    //city 精确匹配
    if (StringUtils.isNotBlank(params.getCity())){
        boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
    }
    //price 范围过滤
    Integer maxPrice = params.getMaxPrice();
    Integer minPrice = params.getMinPrice();
    if (minPrice != null && maxPrice != null) {
        maxPrice = maxPrice == 0 ? Integer.MAX_VALUE : maxPrice;
        boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
    }

    //算分控制
    FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
            boolQuery, // 原始查询,boolQuery
            new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // function数组
                    new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                            QueryBuilders.termQuery("isAD", true), // 过滤条件
                            ScoreFunctionBuilders.weightFactorFunction(10) // 算分函数
                    )
            }
    );
    request.source().query(functionScoreQuery);

}

我周边的酒店

我们要根据这个坐标,将酒店结果按照到这个点的距离升序排序。
实现思路如下:

  • 修改RequestParams参数,接收location字段
  • 修放search方法业务逐辑,如果location有值,添加根据geo distance排序的功能
//周边查询(地理查询)
String location = params.getLocation();
if (StringUtils.isNotBlank(location)) {
    request.source().sort(SortBuilders
            .geoDistanceSort("location",new GeoPoint(location))
            .order(SortOrder.ASC)
            .unit(DistanceUnit.KILOMETERS));
}

酒店竞价排名

广告置顶

需要置顶的酒店文档添加一个标记。然后利用function score给带有标记的文档增加权重。
实现步骤分析:
1. 给HotelDoc类添加 isAD 字段,Boolean类型
2. 桃选几个喜欢的酒店,给它的文档数据添加 isAD字段,值为true
3. 修search方法,添加function score功能给isAD值为true的酒店增加权重

//算分控制
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
        boolQuery, // 原始查询,boolQuery
        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // function数组
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        QueryBuilders.termQuery("isAD", true), // 过滤条件
                        ScoreFunctionBuilders.weightFactorFunction(10) // 算分函数
                )
        }
);
request.source().query(functionScoreQuery);

深入 elasticsearch

数据聚合

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

  1. 桶 (Bucket) 聚合:用来对文档做分组
    TermAggregation  按照文档字段值分组
    DateHistogram  按照日期阶梯分组。例如一周为一组,或者一月为一组
  2. 度量 (Metric) 聚合:用以计算一些值,比如:最大值、最小值、平均值等
    Avg: 求平均值
    Max: 求最大值
    Min: 求最小值
    Stats: 同时求max、min、avg、sum等
  3. 管道 (pipeline) 聚合:其它聚合的结果为基础做聚合
总结
  1. 什么是聚合?

聚合是对文档数据的统计、分析、计算

  1. 聚合的常见种类有离些?

Bucket:对文档数据分组,并统计每组数量
Metric:对文档数据做计算,例如avg
Pipeline:基于具它聚合结果再做聚合

  1. 参与聚合的字段类型必须是:

keyword
数值
日期
布尔聚合的种类

DSL 实现聚合

DSL实现 Bucket 聚合

默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序,
我们可以修改结果排序方式:

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200  //限定聚合范围
      }
    }
  }, 
  "size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": {  //定义聚合
    "brandAgg": { //给聚合起个名字
      "terms": { //聚合的类型,按照品牌值聚合,所以选择term
        "field": "brand", //参与聚合的字段
        "order": {
          "_count": "asc" //按照_count降序排序
        }, 
        "size": 10  //希望获取的聚合结果数量
      }
    }
  }
}

总结

  1. aggs代表聚合,与quey同级,此时query的作用是?
    限定聚合的的文档范国
  2. 聚合必须的三要素:
    聚合名称
    聚合类型
    聚合字段
  3. 聚合可配置属性有:
    size: 指定聚合结果数量, 默认10
    order: 指定聚合结果排序方式
    field: 指定聚合字段
DSL实现 Metrics 聚合

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

# DSL实现 Metrics 聚合
GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200
      }
    }
  }, 
  "size": 0, 
  "aggs": {  
    "brandAgg": { 
      "terms": { 
        "field": "brand", 
        "order": {
          "scoreAgg.avg": "desc"
        }, 
        "size": 10  
      },
      "aggs": {
        "scoreAgg": {
          "stats": {
            "field": "score"
          }
        }
      }
    }
  }
}

RestAPI 实现聚合
@Test
void testAggregations() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    request.source().query(QueryBuilders.rangeQuery("price").lte(200));
    request.source().size(0);

    request.source().aggregation(AggregationBuilders
                .terms("brandAgg")
                .field("brand")
                // .order(BucketOrder.aggregation("scoreAgg.avg", false))
                .size(10)
                .subAggregation(AggregationBuilders.terms("scoreAgg").field("score")));

    //发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //解析响应
    Aggregations aggregations = response.getAggregations();
    Terms brandTerms = aggregations.get("brandAgg");
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
    for (Terms.Bucket bucket : buckets) {
        String key = bucket.getKeyAsString();
        System.out.println(key);

    }

}

自动补全

拼音分词器

当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,这时就会用到拼音分词器

# 拼音
GET /_analyze
{
  "text": ["如家"],
  "analyzer": "pinyin"
  
}

自定义分词器

elasticsearch 中分词器(analyzer)的组成包含三部分:
character filters 在tokenizer之前对文本进行处理。例如删除字符、替换字符
tokenizer 将文本按照一定的规则切制成词条(term),例如keyword,就是不分词;还有ik_smart
tokenizer filter 将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
微服务技术栈——实用篇_第60张图片
拼音分词器适合在创建倒排素引的时候使用,但不能在搜索的时候使用。
因此字段在创建倒排索引时应该用my_analyzer分词器;字段在搜索时应该使用ik_mart分词器

# 自定义拼音分词器
PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": {
        "py": { 
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

# 拼音
GET /test/_analyze
{
  "text": ["如家"],
  "analyzer": "pinyin"
  
}

POST /test/_doc/1
{
  "id": 1,
  "name": "狮子"
}
POST /test/_doc/2
{
  "id": 2,
  "name": "虱子"
}

GET /test/_search
{
  "query": {
    "match": {
      "name": "掉入狮子笼咋办"
    }
  }
}

总结
  1. 如何使用拼音分词器?
    下载拼音分词器
    解压并放到elasticsearch的plugin目录
    重启即可
  2. 如何自定义分词器?
    创建索引库时,在settings中配置,可以包含三部分
    character filter
    tokenizer
    filter
  3. 拼音分词器注意事项?
    为了避免搜索到同音字,接索时不要使用拼音分词器
自动补全查询

completion suggester 查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束
参与补全查询的字段必须是completion类型
字段的内容一般是用来补全的多个词条形成的数组

# 自动补全的索引库
PUT /test2
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}
# 示例数据
POST /test2/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}
POST /test2/_doc
{
  "title": ["SK-II", "PITERA"]
}
POST /test2/_doc
{
  "title": ["Nintendo", "switch"]
}

DELETE /test2

# 自动补全查询
GET /test2/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", 
      "completion": {
        "field": "title", 
        "skip_duplicates": true,
        "size": 10 
      }
    }
  }
}

总结

自动补全对字段的要求:
类型是 completion 类型
字段值是多词条的数组

实现酒店搜索框自动补全
GET /hotel/_search
{
  "suggest": {
    "suggestions": {
      "text": "sd",
      "completion": {
        "field": "suggestion",
        "skip_duplicates": true,
        "size": 10
      }
    }
  }
}
//自动补全
@Test
void testSuggest() throws IOException {
    //请求request对象
    SearchRequest request = new SearchRequest("hotel");
    //准备DSL
    request.source().suggest(
            new SuggestBuilder().addSuggestion("suggestions",
                    SuggestBuilders.completionSuggestion("suggestion").prefix("sd").skipDuplicates(true).size(10))
    );

    //发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    Suggest suggest = response.getSuggest();
    CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
    for (Suggest.Suggestion.Entry.Option option : suggestions.getOptions()) {
        String text = option.getText().toString();
        System.out.println(text);

    }

    //解析响应
    //System.out.println(response);

}

数据同步

数据同步思路分析

elasticsearch中的酒店数据来自于mysqls数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是 elasticsearch 与 mysql 之间的数据同步。
在微服务中,负责酒店管理(操作mysql) 的业务与负责酒店搜索(操作 elasticsearch )的业务可能在两个不同的微服务上,数据同步该如何实现呢?
方案一:同步调用
微服务技术栈——实用篇_第61张图片
方案二:异步通知
微服务技术栈——实用篇_第62张图片
方案三:监听binlog
微服务技术栈——实用篇_第63张图片

总结

方式一:同步调用
·优点:实现简单,粗暴
·缺点:业务精合度高
方式二:异步通知
·优点:低偶合,实现提度一般
·缺点:依赖mq的可靠性
方式三:监听binlog
·优点:完全解除服务间属合
·缺点:开启binlog增加数据库负担、实现复杂度高

实现 elasticsearch 与数据库数据同步

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

利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增、别、改时,要求对
elasticsearch中致据也要完成相同操作。
微服务技术栈——实用篇_第64张图片

黑马旅游-所有步骤
  1. 导入课前资料提供的hotel-admin项目,启动并测试酒店数据的CRUD
  2. 声明exchange、queue、RoutingKey
  3. 在hotel-admin中的增,删、改业务中完成消息发送
  4. 在hotel-demo中完成消息监听,并更新elasticsearch中数据
  5. 启动并测试数据同步功能

集群

搭建ES集群

单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。
海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
单点故障问题:将分片数据在不同节点备份(replica)
微服务技术栈——实用篇_第65张图片

集群脑裂问题

ES集群的节点角色
elasticsearch中集群节点有不同的职责划分:
微服务技术栈——实用篇_第66张图片
ES集群的分布式查询
elasticsearch中的每个节点角色都有自己不同的职责,因此建议集群部署时,每个节点都有独立的角色。
微服务技术栈——实用篇_第67张图片
ES集群的脑裂
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。
为了避免脑裂,需要要求选票超过 (eligible节点数量+1)/2 才能当选为主,因此 eligible 节点数量最好是奇数。对应配置项是 discovery.zen.minimum_master_nodes 在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题

总结
  1. master eligible节点的作用是什么?
  • 参与集群选主
  • 主节点可以管理集群状态、管理分片信息、处理创建和删除
  • 索引库的请求
  1. data节点的作用是什么?
  • 数据的CRUD
  1. coordinator节点的作用是什么?
  • 路由请求到其它节点
  • 合并查询到的结果,返回给用户
集群故障转移

集群的 master 节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到具它节点,确保数据安全,这个叫做故障转移。
微服务技术栈——实用篇_第68张图片

总结

故障转移:

  • master宕机后,EligibleMaster选举为新的主节点。
  • master节点监控分片、节点状态,将故障节点上的分片转移到正常节点,确保数据安全。
集群分布式存储

ES集群的分布式存储
当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到哪个分片呢?
elasticsearch会通过hash算法来计算文档应该存储到哪个分片:
image.png
说明:
_routing默认是文档的id
算法与分片数量有关,因此索引库一旦创建。分片数量不能修改!
微服务技术栈——实用篇_第69张图片

集群分布式查询

elasticsearch的查询分成两个阶段:
scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
gather phase:聚集阶段,coordinating node汇总data node的搜索结集,并处理为最终结果集返回给用户
微服务技术栈——实用篇_第70张图片

总结

分布式新增如何确定分片?
coordinating node 根锯id做hash运算,得到结果对shard数量取余,余数就是对应的分片
分布式查询:
分散阶段:coordinating node将查询请求分发给不同分片
收集阶段:将查询结果汇总到coordinating node, 整理并返回给用户

你可能感兴趣的:(微服务技术栈,spring,cloud,微服务)