单体架构

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

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

缺点:
  • 耦合度高

分布式架构

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

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

服务治理

分布式架构的要考虑的问题:
  • 服务拆分力度如何?
  • 服务集群地址如何维护?
  • 服务之间如何实现远程调用?
  • 服务健康状态如何感知?

微服务

微服务是一种经过良好架构设计的分布式架构方案,微服务结构特征:
  • 单一职责: 微服务拆分力度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自制:团队独立,技术独立,数据独立,部署独立
  • 隔离性强: 服务调用做好隔离,容错,降级,避免出现级联问题

总结

单体架构特点?

  • 检点方便,高度耦合,扩展性差,适合小型项目. 例如: 学生管理系统

分布式架构特点?

  • 松耦合,扩展性好,单框架结构复杂,难度大.适合大型互联网项目,例如:京东,淘宝

微服务: 一种良好的分布式架构方案

  • 优点:拆分力度更小,服务更独立,耦合度更低
  • 缺点:结构非常复杂,运维,监控,部署难度提高

微服务结构

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

微服务技术对比

企业需求

SpringCloud

  • SpringCloud是目前国内使用最广泛的微服务框架.官网地址: https://spring.io/projects/spring-cloud.
  • SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:

SpringCloud与SpringBoot的版本兼容关系如下:

服务拆分及远程调用

服务拆分注意事项

  1. 不同微服务,不要重复开发相同业务
  2. 微服务数据独立,不要访问其他微服务的数据库
  3. 微服务可以将自己的业务暴露为接口,供启其他微服务调用

服务拆分及远程调用

导入服务拆分Demo

  1. 导入课前资料提供的工程: cloud-demo
  2. 项目结构
  3. 将课前资料准备的sql导入数据库中:

总结

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

微服务远程调用

案例: 根据订单id查询订单功能

需求: 根据订单id查询订单的同时,把订单所属的用户信息一起返回

远程调用方式分析

步骤: 1)注册RestTemplate

在order-service的OrderApplication中注册RestTemplate

  1. @MapperScan("cn.itcast.order.mapper")
  2. @SpringBootApplication
  3. public class OrderApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(OrderApplication.class, args);
  6. }
  7. /**
  8. * 创建RestTemplate 并注入容器
  9. * @return
  10. */
  11. @Bean
  12. public RestTemplate restTemplate(){
  13. return new RestTemplate();
  14. }
  15. }

步骤: 2)服务远程调用RestTemplate

修改order-service中的OrderService的queryOrderById方法:

  1. @Service
  2. public class OrderService {
  3. @Autowired
  4. private OrderMapper orderMapper;
  5. @Autowired
  6. private RestTemplate restTemplate;
  7. public Order queryOrderById(Long orderId) {
  8. // 1.查询订单
  9. Order order = orderMapper.findById(orderId);
  10. //2.利用RestTemplate发起http请求,查询用户
  11. //2.1.url路径
  12. String url = "http://localhost:8081/user/"+order.getUserId();
  13. //2.2.发送http请求,实现远程调用 参数1: url地址 参数2:返回json转换的类型
  14. User user = restTemplate.getForObject(url, User.class);
  15. //3.封装user到Order
  16. order.setUser(user);
  17. // 4.返回
  18. return order;
  19. }
  20. }

总结

1.微服务调用方式

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

提供者与消费者

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

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

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

一个服务既可以是消费者,也可以是一个提供者.

1.服务调用关系

  • 服务提供者:暴露接口给其他微服务调用
  • 服务消费者:调用其他微服务提供的接口
  • 提供者与消费者角色其实是相对的

Eureka注册中心

服务调用出现的问题

url路径不能使用硬编码

服务消费者改如何获取服务提供者的地址信息?

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

Eureka的作用

服务消费者改如何获取服务提供者的地址信息?

  • 服务提供者启动时向eureka注册自己的信息
  • eureka保存这些信息
  • 消费者根据服务名称向eureka拉取提供者信息

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

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

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

  • 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
  • erueka会更新记录服务器列表信息,心跳不正常会被剔除
  • 消费者就可以拉取到最新的信息

总结

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

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

动手实践

搭建EurekaServer *

搭建EurekaServer服务步骤如下:

  1. **Eureka** **server** **服务端**
  1. 创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖
    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    4. </dependency>
  1. 编写启动列,添加@EnableEurekaServer注解
    1. @EnableEurekaServer
    2. @SpringBootApplication
    3. public class EurekaApplication {
    4. public static void main(String[] args) {
    5. SpringApplication.run(EurekaApplication.class,args);
    6. }
    7. }
  1. 添加application.yaml文件,编写下面的配置
  1. server:
  2. port: 10086
  3. spring:
  4. application:
  5. name: eurekaserver #设置 当前 项目名,服务名
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://127.0.0.1:10086/eureka/

总结

1,搭建EurekaServer

  • 引入eureka-server依赖
  • 添加@EnableEurekaServer注解
  • 在application.yaml中配置eureka地址

注册user-service *

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

  1. **Eureke** **client** **客户端**
  1. 在user-server项目引入spring-cloud-starter-netflix-eureka-client的依赖
    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    4. </dependency>
  1. 在application,yaml文件,编写下面的配置:
    1. spring:
    2. application:
    3. name: userservice #设置 当前 项目名,服务名
    4. eureka:
    5. client:
    6. service-url:
    7. defaultZone: http://127.0.0.1:10086/eureka/

另外,我们可以将user-service多次启动,模拟多实例部署,但为为了避免端口冲突,需要配置端口设置:

总结

1. 服务注册

  • 引入eureka-client依赖
  • 在application.yaml中配置eureka地址

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

在order-service完成服务拉取 *

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

  1. 修改OrderService的代码,修改访问的url路径,用服务名代表ip,端口 修改为application的name:
    1. String url = "http://userservice/user/"+order.getUserId();
  1. 在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:
    1. @Bean
    2. @LoadBalanced //负载均衡
    3. public RestTemplate restTemplate(){
    4. return new RestTemplate();
    5. }

总结

  1. 搭建EurekaServer
    • 引入eureka-server依赖
    • 添加@EnableEurekaServer注解
    • 在application.yaml中配置eureka地址
  2. 服务注册
    • 引入eureka-client依赖
    • 在application.yaml中配置eureka地址
  3. 服务发现
    • 引入eureka-client依赖
    • 在application.yaml中配置eureka地址
    • 给RestTemplate添加@LoadBalanced注解 负载均衡
    • 用服务提供者的服务名称远程调用

Ribbon负载均衡 *

  • 负载均衡原理
  • 负载均衡策略
  • 懒加载

负载均衡流程

负载均衡策略

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

通过定义IRule实现可以修改负载均衡规则,有两种方式:

  1. 代码方式: 在order-service中的OrderApplication类中,定义一个新的IRule:
    全局负载均衡规则
    1. @Bean
    2. public IRule randomRule()}{
    3. return new RandomRule();
    4. }
  1. 配置文件方式: 在order-service的application.yaml文件中,添加新的配置也可以修改规则:
    1. **先写服务名称 : 指定微服务来做负载均衡规则**

饥饿加载

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

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

  1. ribbon:
  2. eager-load:
  3. enabled: true #开启饥饿加载
  4. clients: userservice #指定对userservice这个服务饥饿加载

总结

1.Ribbon负载均衡规则

  • 规则接口是IRule
  • 默认实现是ZoneAvoidanceRule, 根据zone选择服务列表, 然后轮询

2.负载均衡自定义方式

  • 代码方式: 配置灵活, 但修改时需要重新打包发布
  • 配置方式:直观, 方便, 无需重新打包发布,但是无法做全局配置

3.饥饿加载

  • 开启饥饿加载
  • 指定饥饿加载的微服务名称

Nacos注册中心

  • 认识和安装Nacos
  • Nacos快速入门
  • Nacos服务分级储存模型
  • Nacos环境隔离

认识Nacos

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

服务注册到Nacos *

  1. 本地安装了Nacos
  1. 在cloud-demo父工程中添加spring-cloud-alibaba的管理依赖:
    1. <!-- nacos 的管理依赖 -->
    2. <dependency>
    3. <groupId>com.alibaba.cloud</groupId>
    4. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    5. <version>2.2.5.RELEASE</version>
    6. <type>pom</type>
    7. <scope>import</scope>
    8. </dependency>
  1. 注解掉order-service和user-service中原有的eureka依赖.
  2. 添加nacos的客户端依赖
    1. <!-- nacos客户端 依赖 -->
    2. <dependency>
    3. <groupId>com.alibaba.cloud</groupId>
    4. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    5. </dependency>
  1. 修改user-service&order-service中的application.yaml文件,注释eureka地址,添加nacos地址:
    1. spring:
    2. cloud:
    3. nacos:
    4. server-addr: localhost:8848 # nacos 服务端地址
  1. 启动并测试

总结 * 启动nacos

  1. Nacos服务搭建
    1. 下载安装包
    2. 解压
    3. 在bin目录下运行指令: .\startup.cmd -m standalone
  2. Nacos服务注册或发现
    1. 引入nacos.discovery依赖
    2. 配置nacos地址spring.cloud.nacos.server-addr

Nacos服务分级储存模型

服务跨集群调用问题

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

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

服务集群属性

  1. 修改application.yaml, 添加如下内容
    1. spring:
    2. cloud:
    3. nacos:
    4. server-addr: localhost:8848 #nacos 服务端地址
    5. discovery:
    6. cluster-name: SD #配置集群名称, 也就是机房位置, 例如: HZ,杭州
  1. 在Nacos控制台可以看到集群变化:

总结

  1. Nacos服务分级存储模型
    1. 一级是服务, 例如userservice
    2. 二级是集群,例如杭州或上海
    3. 三级是实例,例如杭州机房的某台部署了userservice的服务器
  2. 如何设置实例的集群属性
    1. 修改application.yaml文件 , 添加spring,cloud.nacos.discovery.cluster-name属性即可

根据集群负载均衡

  1. **请求只会发送同集群名称 服务**
  1. 修改order-service中的application.yaml, 设置集群为SD:
  1. spring:
  2. application:
  3. name: orderservice #设置服务名称
  4. cloud:
  5. nacos:
  6. server-addr: 127.0.0.1:8848 #设置nacos服务地址
  7. discovery:
  8. cluster-name: SD # 集群名称
  1. 然后在order-service中设置负载均衡的IRule为NacosRule, 这个规则优先会寻找与自己同集群的服务:
  1. userservice:
  2. ribbon:
  3. NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则
  1. 注意将user-service的权重都设置为1

总结

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

根据权重负载均衡

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

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

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

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

总结

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

环境隔离 - namespace

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

Namespace —> Group —> Service/Data

  1. 在Nacos控制台可以创建namespace, 用来隔离不同环境
  1. 然后填写一个新的命名空间信息:
  1. 保存后会在控制台看到这个命名空间的id:
  1. 修改order-service的application.yaml, 添加namespace:
  1. spring:
  2. application:
  3. name: orderservice #设置服务名称
  4. cloud:
  5. nacos:
  6. server-addr: localhost:8848 #安装nacos的地址 也就是server
  7. discovery:
  8. cluster-name: SD #山东
  9. namespace: b7933534-5e17-4012-a768-cec51d660abc #命名空间,填ID
  1. 重启order-service后, 再来查看控制台:
  1. 此时访问order-service, 因为namespace不同, 会导致找不到userservice, 控制台会报错:

总结

  1. Nacos环境隔离
    1. namespace用来做环境隔离
    2. 每个namspace都有唯一id
    3. 不同namespace下的服务不可见

nacos注册中心细节分析

临时实例和非临时实例

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

  1. spring:
  2. cloud:
  3. nacos:
  4. discovery:
  5. ephemeral: false #设置为非临时实例 #默认为 true临时实例 改为 false非临时实例

总结

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

Nacos配置管理

统一配置管理

  • 配置更改热更新

在Nacos中添加配置信息,

在弹出表单中填写配置信息:

配置获取的步骤如下:

  1. 引入Nacos的配置管理客户端依赖:
  1. <!-- Nacos配置管理依赖 -->
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  5. </dependency>
  1. 在userservice中的resource目录添加一个bootstrap.yaml文件, 这个文件是引导文件, 优先级高于application.yaml:
  1. spring:
  2. application:
  3. name: userservice #服务名称
  4. profiles:
  5. active: dev #开发环境, 这里是dev
  6. cloud:
  7. nacos:
  8. server-addr: localhost:8848 #Nacos地址
  9. config:
  10. file-extension: yaml #文件后缀名

我们在user-service中将pattern.dateformat这个属性注入到UserController中做测试:

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4. @Value("${pattern.dateformat}")
  5. private String dateformat;
  6. @GetMapping("now")
  7. public String now(){
  8. return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
  9. }
  10. }

总结

将配置交给Nacos管理的步骤

  1. 在Nacos中添加配置文件
  2. 在微服务中引入nacos的config依赖
  3. 在微服务中添加bootstrap.yaml, 配置nacos地址, 当前环境, 服务名称, 文件后缀名. 这些决定了程序启动是去nacos读取那个文件.

配置自动刷新

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

  • 方式一: 在@Value注入的变量所在类上添加注解@RefreshScope
  • 方式二:使用@ConfigurationProperties注解
  1. @Data
  2. @Component
  3. @ConfigurationProperties(prefix = "pattern")
  4. public class PatternProperties {
  5. private String dateformat;
  6. }
  7. @RestController
  8. @RequestMapping("/user")
  9. public class UserController {
  10. @Autowired
  11. private PatternProperties dateformat;
  12. @GetMapping("now")
  13. public String now(){
  14. return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat.getDateformat()));
  15. }
  16. }

总结

Nacos配置更改后, 微服务可以实现热更新,方式:

  1. 通过@Value注解注入, 结合@RefreshScope来刷新
  2. 通过@ConfigurationProperties注入, 自动刷新

注意事项:

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

多环境配置共享

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

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

多种配置的优先级:

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

微服务会从nacos读取的配置文件:

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

优先级:

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

Nacos集群搭建

Nacos生产环境下一定要部署为集群状态, 部署方式参考课前资料中的文档:

总结

集群搭建步骤:

  1. 搭建MySQL集群并初始化数据库表 Nacos的数据存储
  2. 下载解压nacos
  3. 修改集群配置(节点信息) , 数据库配置
  4. 分别启动多个nacos节点
  5. nginx反向代理

HTTP客户端Feign

RestTemolate方式调用存在的问题

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

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

存在下面的问题:

  • 代码可读性差, 编程体验不统一
  • 参数复杂URL难以维护

Feign的介绍

Feign是一个声明式的http客户端, 官方地址: https://github.com/OpenFeign/feign

其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题.

定义和使用Feign客户端

使用Feign的步骤如下:

  1. 引入依赖
  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-openfeign</artifactId>
  4. </dependency>
  1. 在order-service的启动类添加注解开启Feign的功能:
  1. @EnableFeignClients
  2. @MapperScan("com.xiao.order.mapper")
  3. @SpringBootApplication
  4. public class OrderApplication{
  5. public static void main(String[] args){
  6. SpringApplication.run(OrderApplication.class, args);
  7. }
  8. }
  1. 编写Feign客户端:
  1. @FeignClient("userservice") //指定客户端
  2. public interface UserClient {
  3. @GetMapping("/user/{id}") //客户端接口开放接口
  4. User findById(@PathVariable("id") Long id); //路径里的参数
  5. }
  1. service调用
  1. @Service
  2. public class OrderService {
  3. @Autowired
  4. private OrderMapper orderMapper;
  5. @Autowired
  6. private UserClient userClient;
  7. public Order queryOrderById(Long orderId) {
  8. // 1.查询订单
  9. Order order = orderMapper.findById(orderId);
  10. //2.用Feign远程调用
  11. User user = userClient.findById(order.getUserId());
  12. //3.封装user到Order
  13. order.setUser(user);
  14. // 4.返回
  15. return order;
  16. }
  17. }

主要是基于SpringMVC的注解来声明远程调用的信息, 比如:

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

Feign 内部 集成了Ribbon 自动实现了负载均衡

总结

Feign的使用步骤

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

自定义Feign的配置

Feign运行自定义配置来覆盖默认配置, 可以修改的配置如下:

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

一般我们需要配置的就是日志级别.

配置Feign日志有两种方式:

方式一: 配置文件方式

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

配置Feign日志的方式二: java 代码方式, 需要先声明一个Bean:

  1. public class FeignClientConfiguration {
  2. @Bean
  3. public Logger.Level feignLogLevel(){
  4. return Logger.Level.BASIC:
  5. }
  6. }
  1. 而后如果是全局配置, 则把它放到@EnableFeignClients这个注解中:
  1. @EnableFeignClients(defultConfiguration = FeignClientConfiguration.class)
  1. 如果是局部配置, 则把它放到@FeignClient这个注解中:
  1. @FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)

总结

Feign的日志配置:

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

Feign的性能优化

Feign底层的客户端实现:

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

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

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

Feign的性能优化-连接池配置

Feign添加HttpClient的支持:

引入依赖:

  1. <!-- httpClient的依赖 -->
  2. <dependency>
  3. <groupId>io.github.openfeign</groupId>
  4. <artifactId>feign-httpclent</artifactId>
  5. </dependency>

配置连接池:

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

总结

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

Feign的最佳实践

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

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

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

总结

  1. 让controller和FeignClient继承同一接口
  2. 让FeignClient, POJO, Feign的默认配置都定义到一个项目中, 供消费者使用

抽取FeignClient 实现方法二

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

  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中的包
  5. 重启测试

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

方式一: 指定FeignClient所在包

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

方式二:指定FeignClient字节码

  1. EnableFeignClients(clients = {UserClient.class})

总结

不同包的FeignClient的导入有两种方式:

  1. 在@EnableFeignClients注解中添加basePackages, 指定FeignClient所在的包
  2. 在@EnableFeignClients注解中添加clients, 指定具体FiegnClient的字节码

统一网关Gateway

为什么需要网关

网关功能:

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

网关的技术实现

在SpringCloud中网关的实现包括两种:

  • gateway
  • zuul

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

总结

网关的作用:

  • 对用户请求做身份认证, 权限校验
  • 将用户请求路由到微服务, 并实现服务均衡
  • 对用户请求做限流

搭建网关服务

搭建网关服务的步骤:

  1. 创建新的module, 引入SpringCloudGateway的依赖和nacos的服务发现依赖:
  1. <!-- 网关依赖 -->
  2. <dependency>
  3. <gropuId>org.springframework.cloud</gropuId>
  4. <artifactId>spring-cloud-starter-gateway</artifactId>
  5. </dependency>
  6. <!-- nacos 服务发现依赖-->
  7. <dependency>
  8. <groupId>com.alibaba.cloud</groupId>
  9. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  10. </dependency>
  1. 编写路由配置及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: http://127.0.0.1:8081 #路由的目标地址 http就是固定地址
  13. uri: lb://userservice #路由的目标地址 lb就是负载均衡, 后面跟服务名称
  14. predicates: #路由断言, 也就是判断请求是否符合路由规则的条件
  15. - Path=/user/** # 这个是按照路径匹配, 只要以/user/开头就符合要求

流程图:

总结

网关搭建步骤:

  1. 创建项目, 引入nacos服务发现和gateway依赖
  2. 配置application.yaml, 包括服务基本信息, 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类来处理**
  • 像这样的断言工厂在SpringCloudGateway还有十几个

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]
Cookic 请求必须包含些Cookie - Cookie=Cholate, 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 是网关中提供的一种过滤器,可以对进行网关的请求和微服务返回的响应做处理:

过滤器工厂 GatewayFilterFactory

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

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

案例 给所有进入userservice的请求添加一个请求头

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

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

  1. spring:
  2. cloud:
  3. gateway:
  4. routes: #网关路由设置
  5. - id: user-service
  6. uri: lb://userservice
  7. predicates:
  8. -Path=/user/**
  9. filters: #过滤器
  10. - AddRequestHeader=Truth, Itcast is freaking awesome! #添加请求头

默认过滤器

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

  1. spring:
  2. application:
  3. name: gateway #服务名称
  4. cloud:
  5. nacos:
  6. server-addr: localhost:8848 # nacos 地址
  7. gateway:
  8. routes: #网关路由配置
  9. - id: user-service
  10. uri: lb://userservice
  11. predicates:
  12. - Path=/user/**
  13. - id: order-service
  14. uri: lb://orderservice
  15. predicates:
  16. - Path=/order/**
  17. default-filters: #默认过滤器, 会对所有的路由请求都生
  18. - AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头

总结

过滤器的作用是什么?

  1. 对路由的请求或响应做加工处理, 比如添加请求头
  2. 配置在路由下的过滤器只对当前路由的请求生效

defaultFilters的作用是什么?

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

全局过滤器 GlobalFilter

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

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

定义方式是实现GlobalFilter接口.

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

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

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

  • 参数中是否有authorization,
  • authorization参数值是否为admin

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

步骤1: 自定义过滤器

自定义类, 实现GlobalFilter接口, 添加@Order注解 或者 实现 Ordered:

  1. //@Order(-1) //顺序 越小优先值越高 Filter执行顺序
  2. @Component
  3. public class AuthorizeFilter implements GlobalFilter, Ordered { //也可以使用Ordered 来实现顺序
  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);
  19. // 5.2拦截请求 结束请求
  20. return exchange.getResponse().setComplete();
  21. }
  22. //实现Ordered 顺序
  23. @Override
  24. public int getOrder() {
  25. return -1;
  26. }
  27. }

总结

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

过滤器执行顺序

请求进入网关会碰到三类过滤器, DefaultFilter , GlobalFilter

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

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

可以参考下面几个类的源码来查看:

  1. org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters, 然后在加载某个routefilters, 然后合并.
  2. org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序, 组织过滤器链

总结

  • 路由过滤器, defaultFilter, 全局过滤器的执行顺序?
    1. order值越小, 优先级越高
    2. 当order值一样时, 顺序是defaultFilter最先, 人后是局部的路由过滤器, 最后是全局过滤器

跨域问题处理 ajax

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

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

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

解决方案: CORS

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

  1. spring:
  2. cloud:
  3. gateway:
  4. globalcors: # 全局的跨域处理
  5. add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
  6. corsConfigurations:
  7. '[/**]':
  8. allowedOrigi ns: # 允许哪些网站的跨域请求 所有要发起ajax请求都都要加上 包括本地端口
  9. - "http://localhost:5500" #是localhost 就写 地址栏就写localhost 不要127 ***
  10. - "http://www.leyou.com"
  11. allowedMethods: # 允许的跨域ajax的请求方式
  12. - "GET"
  13. - "POST"
  14. - "DELETE"
  15. - "PUT"
  16. - "OPTIONS"
  17. allowedHeaders: "*" # 允许在请求中携带的头信息
  18. allowCredentials: true # 是否允许携带cookie
  19. maxAge: 360000 # 这次跨域检测的有效期

总结

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

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

Docker

项目部署的问题

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

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

Docker

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

  • 将应用的Libs(函数库), Deps(依赖), 配置与应用一起打包
  • 将每个应用放到一个隔离容器去运行, 避免互相干扰

不同环境的操作系统不同, Docker如何解决? 我们先来了解下操作系统结构

内核与硬件交互, 提供操作硬件的指令

系统应用封装内核指令为函数, 便于程序员调用

用户程序基于系统函数库实现功能

Ubuntu和CentOS都是基于Linux内核, 只是系统应用不同, 提供的函数库有差异

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

  • Docker将用户程序与需要调用的系统(比如Ubuntu)函数库一起打包
  • Docker运行到不同操作系统时,直接基于的打包的库函数, 借助于操作系统的Linux内核来运行

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

  • Docker 允许开发中将应用, 依赖 , 函数库, 配置一起打包, 形成可移植镜像
  • Docker 应用运行在容器中, 使用沙箱机制, 互相隔离

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

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

总结

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

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

Docker与虚拟机

虚拟机(virtual machine)是在操作系统中模拟硬件设备, 然后运行另一个操作系统, 比如在Windows系统里面运行Ubuntu系统, 这样就可以运行任意的Ubuntu应用了.

总结

Docker和虚拟机的差异:

  • docker是一个系统进程; 虚拟机是在操作系统中的操作系统
  • docker体积小,启动速度快, 性能好; 虚拟机体积打, 启动速度慢, 性能一般

镜像和容器

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

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

Docker和DockerHub

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

docker架构

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

  • 服务端(server) : Docker守护进程, 负责处理Docker指令, 管理镜像, 容器
  • 客户端(client) : 通过命令或RestAPI项Docker服务端发送指令. 可以在本地或远程向服务端发送指令.

总结

镜像:

  • 将应用程序及其依赖, 环境, 配置打包在一起

容器:

  • 镜像运行起来就是容器, 一个镜像可以运行多个容器

Docker结构:

  • 服务端 : 接收命令或远程请求, 操作镜像或容器
  • 客户端 : 发送命令或者请求到Docker服务端

DockerHub:

  • 一个镜像托管的服务器, 类似的还有阿里云镜像服务, 统称为DockerRegistry

安装Docker

企业部署一般都是采用Linux操作系统, 而其中又数CentOS发行版占比最多, 因此我们在CentOS下安装Docker. 参考课前资料中的文档:

Docker基本操作

镜像相关命令

  • 镜像名称一般分两部分组成: [reqository]:[tag].
  • 在没有指定tag时, 默认是latest, 代表最新版本的镜像

镜像操作命令

  1. Docker --help 查看文档
  1. docker build 构建镜像
  2. docker imagers 查看镜像
  3. docker rmi 删除镜像
  4. docker push 推送镜像到服务
  5. docker pull 从服务拉取镜像
  6. docker save 保存镜像为一个压缩包
  7. docker load 加载压缩包为镜像

案例 从DockerHub中拉取一个nginx镜像查看

  1. 首先去镜像仓库搜索nginx镜像, 比如DockerHub:
  1. 根据查看到的镜像名称, 拉取自己需要的镜像, 通过命令: docker pull nginx
  1. 通过命令: docker images 查看拉取到的镜像

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

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

步骤二 : 使用docker tag 创建新镜像 mynginx1.0

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

总结

镜像操作有哪些?

  • docker images
  • docker rmi
  • docker pull
  • docker push
  • docker save
  • docker load

Docker基本操作-镜像操作

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

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

容器相关命令

创建一个容器(默认开启)

  1. docker run

暂停容器

  1. docker pause

从暂停中开启

  1. docker unpause

停止容器

  1. docker stop

开启容器

  1. docker start

查看所有运行的容器以及状态

  1. docker ps

查看容器运行日志

  1. docker logs

进入容器执行命令

  1. docker exec

删除指定容器

  1. docker rm

Docker基本操作-容器

案例 创建运行一个Nginx容器

步骤一: 去docker hub查看Nginx的容器运行命令

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

命令解读:

  • docker run: 创建并运行一个容器
  • —name : 给容器起一个名字,比如叫mn
  • -p : 将宿主机端口与容器端口映射, 冒号左侧是宿主机端口, 右侧是容器端口
  • -d : 后台运行
  • nginx : 镜像名称, 例如nginx

总结

docker run命令 的常见参数有哪些?

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

查看容器日志的命令:

  • docker logs
  • 添加 -f 参数可以持续查看日志

查看容器状态:

  • docker ps

案例 进入Nginx容器, 修改HTML文件内容, 添加”爱玩游戏的傲,欢迎您”

步骤一: 进入容器. 进入我们刚刚创建的nginx容器的命令为:

  1. docker exec -it mn bash

命令解读:

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

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

  1. cd /usr/share/nginx/html

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

  1. sed -i 's#Welcome to nginx#爱玩游戏的傲#g' index.html
  2. sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html

总结

查看容器状态:

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

删除容器:

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

进入容器:

  • 命令是docker exec -it [容器名] [要执行的命令]
  • exec命令可以进入容器修改文件, 但是在容器内修改文件是不推荐的

exit 退出

练习 并创建一个redis容器, 并支持数据持久化

步骤一 : 到DockerHub搜索Redis镜像

步骤二 : 查看Redis镜像文档中的帮助信息

步骤三 : 里兜docker run命令运行一个Redis容器

数据卷

容器与数据耦合的问题

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

操作数据卷

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

  1. docker volume [COMMAND]

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

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

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

  1. 创建数据卷
    1. docker volume create [name]
  1. 查看所有数据
    1. docker volume ls
  1. 查看数据卷详细信息卷
    1. docker volume inspect

总结

数据卷的作用:

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

数据卷操作:

  • docker volume create
  • docker volume ls
  • docker volume inspect
  • docker volume rm
  • docker volume prune

挂载数据卷

-v

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

举例说明

  1. docker run\ dockerrun : 就是创建并运行容器
  2. --name mn\ --name mn : 给容器起个名字叫mn
  3. -v html:/root/html\ -v html:/root/html : html数据卷挂在到容器内的/root/html这个目录中
  4. -p 8080:80 -p 8080:80 : 把宿主机的8080端口映射到容器内的80端口
  5. nginx\ nginx : 镜像名称

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

需求说明 : 上个案例中, 我们进入nginx容器内部, 已经知道nginx的html目录所在位置

/usr/share/nginx/html , 我们需要把这个目录挂在到html这个数据卷上, 方便操作其中的内容.

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

步骤:

  1. 创建容器并挂载数据卷到容器内的HTML目录
    1. docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx
  1. 进入html数据卷所在位置, 并修改HTML内容
  1. # 查看html数据卷的位置
  2. docker volume inspect html
  3. # 进入该目录
  4. cd /var/lib/docker/volumes/html/_data
  5. # 修改文件
  6. vi 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容器, 要求:
    1. 挂载/tmp/mysql/data到mysql容器内数据存储目录
    2. 挂载/tmp/mysql/conf/hmy.cnf到mysql容器的配置文件
    3. 设置MySQL密码

docker run —name mysql \

-e MYSQL_ROOT_PASSWORD=zhang..0902 \

-p 3306: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 \

数据卷挂载的方式对比

总结

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

Dockerfile自定义镜像

镜像结构

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

总结

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

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

什么是Dockerfile

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

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

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

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

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

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

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

步骤五 : 进入docker-demo

步骤六 : 运行命令

  1. docker bulid -t javaweb:1.0.

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

实现思路如下:

  1. 新建一个空的目录, 然后在目录中新建一个文件, 命名为Dockerfile
  2. 拷贝课前资料提供的docker-demo.jar到这个目录中
  3. 编写Dockerfile文件:
    1. 基于java:8-alpine作为基础镜像
    2. 将app.jar拷贝到镜像中
    3. 暴露端口
    4. 编写入口ENTRYPOINT
  4. 使用docker build命令构建镜像
  5. 使用docker run创建容器并运行
  1. #指定基础镜像
  2. FROM ubuntu:16.04
  3. #配置环境变量,JDK的安装目录
  4. ENV JAVA_DIR=/usr/local
  5. #拷贝jdk和java项目的包
  6. COPY ./jdk8.tar.gz $JAVA_DIR
  7. COPY ./docker-demo.jar /tmp/app.jar
  8. #安装JDK
  9. RUN cd $JAVA_DIR \
  10. && tar -xf ./jdk8.tar.gz \
  11. && mv ./jdk1.8.0_144 ./java8
  12. #配置环境变量
  13. ENV JAVA_HOME=$JAVA_DIR/java8
  14. ENV PATH=$PATH:$JAVA_HOME/bin
  15. #暴露端口
  16. EXPOSE 8090
  17. #入口, java项目的启动命令
  18. ENTRYPOINT java -jar /tmp/app.jar

总结

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

DockerCompose

什么是DockerCompose

  • Docker Compose可以基于Compose文件帮助我们快速的部署分布式应用, 无需手动一个个创建和运行容器!
  • Compose文件是一个文本文件, 通过指令定义集群中的每个容器如何运行.

安装DockerCompose

参考课前资料

Centos7安装Dokcer.md

总结

version: “3.2”

services:

nacos:

image: nacos/nacos-server

environment:

MODE: standalone

ports:

  • “8848:8848”

mysql:

image: mysql:5.7.25

environment:

MYSQL_ROOT_PASSWORD: 123

volumes:

  • “$PWD/mysql/data:/var/lib/mysql”

  • “$PWD/mysql/conf:/etc/mysql/conf.d/“

userservice:

build: ./user-service

orderservice:

build: ./order-service

gateway:

build: ./gateway

ports:

  • “10010:10010”

DokcerCompose有什么作用?

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

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

实现思路如下:

  1. 查看课前资料提供的cloud-demo文件夹, 里面已经编写好了, 里面已经编写好了docker-compose文件
  2. 修改自己的cloud-demo项目, 将数据库, nacos地址都命名为docker-dompose中的服务名
  3. 使用Maven打包工具, 将项目中的每个微服务都打为app.jar
  4. 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录
  5. 将cloud-demo上传至虚拟机, 利用docker-compose up -d 来部署

Docker镜像仓库

常见镜像仓库服务

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

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

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

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

  1. 重新tag本地镜像, 名称前缀为私有仓库的地址: 你配置的ip124.221.236.57:8080/
    1. docker tag nginx:latest 124.221.236.57:8080/nginx:1.0
  1. 推送镜像
    1. docker push 124.221.236.57:8080/nginx:1.0
  1. 拉取镜像
    1. docker pull 124.221.236.57:8080/nginx:1.0

总结

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

服务异步通讯 RabbitMQ

初识MQ

同步通讯和异步通讯

同步调用的问题

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

总结

同步调用的有点:

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

同步调用的问题:

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

异步调用方案

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

优势一 : 服务解耦

优势二 : 性能提升, 吞吐量提高

优势三 : 服务没有强依赖 , 不担心级联失败问题

优势四 : 流量削峰

总结

异步通信的优点:

  • 耦合度低
  • 吞吐量提升
  • 故障隔离
  • 流量削峰

异步通信的缺点:

  • 依赖于Broker的可靠性,安全性,吞吐能力
  • 架构复杂了,业务没有明显的流程线, 不好追踪管理

什么是MQ

MQ(MessageQueue) , 中文是消息队列, 字面来看就是存放消息的队列. 也就是事件驱动架构中的Broker.

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

RabbitMQ快速入门

RabbitMQ概述

RabbitMQ是基于Erlang语言开发的开源消息通信中间件, 官网地址:https://www.rabbitmq.com/

安装RabbitMQ, 参考课前资料:

RabbitMQ部署指南

RabbitMQ的结构和概念

总结

RabbitMQ中的几个概念:

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

常规消息模型

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

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

HelloWorld案例

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

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

案例 完成官方Demo中的hello world案例

实现步骤:

  • 导入课前资料中的demo工程 (mq-demo) 去看 去看代码
  • 运行publisher服务中的测试类PublisherTest中的测试方法testSendMessage()
  • 查看RabbitMQ控制太的消息
  • 启动consumer服务 , 查看是否能接收消息
  1. <!--AMQP依赖,包含RabbitMQ-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-amqp</artifactId>
  5. </dependency>

总结

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

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 利用channel向队列发送消息

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

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 定义consumer的消费行为handleDelivery()
  5. 利用channel将消费者与队伍绑定

SpringAMQP

什么是SpringAMQP

SpringAmqp的官方地址 : https://spring.io/projects/spring-amqp

案例 利用springAMQP实现HelloWorld中的基础消息队列功能

流程如下:

  1. 在父工程中引入spring-amqp的依赖
  2. 在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列
  3. 在consumer服务中别写消费逻辑, 绑定simple.queue这个队列

步骤 步骤一:引入AMQP依赖

因为publisher和consumer服务都需要amqp依赖, 因此这里把依赖直接放到父工程mq-demo中:

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

步骤 步骤二:在publisher中编写测试方法, 向simple.queue发送消息
  1. 在publisher服务中编写application.yaml, 添加mq连接信息:
  1. spring:
  2. rabbitmq:
  3. host: 124.221.236.57 #主机名
  4. port: 5672 #端口
  5. virtual-host: / #虚拟主机
  6. username: itcast #用户名
  7. password: 123321 #密码
  1. 在publisher服务中新建一个测试类, 编写测试方法:
  1. @RumWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class SpringAmqpTest{
  4. @Autowired
  5. private RabbitTemplate rabbitTemplatep;
  6. @Test
  7. public void testSimpleQueue(){
  8. String queueName = "simple.queue";
  9. String message = "hello , Spring amqp!";
  10. rabbitTemplatep.convertAndSend(queueName,message);
  11. }
  12. }

总结

什么是AMQP?

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

SpringAMQP如何发送消息?

  • 引入amqp的starter依赖
  • 配置RabbitMQ地址
  • 利用RabbitTemplate的convertAndSend方法

案例 步骤三: 在consumer中编写消费逻辑, 监听simple.queue
  1. 在consumer服务中编写application.yaml , 添加mq连接信息:
  1. spring:
  2. rabbitmq:
  3. host: 124.221.236.57 #主机名
  4. port: 5672 #端口
  5. virtual-host: / #虚拟主机
  6. username: itcast #用户名
  7. password: 123321 #密码
  1. 在consumer服务中新建一个类, 编写消费逻辑:
  1. @Component
  2. public class SpringRabbitListener(){
  3. //指定监控那个对应的消息
  4. @RabbitListener(queue = "simple.queue") //参数 获取到的数据
  5. public void listenSimpleQueueMessage(String msg) throws InterruptedExcption {
  6. System.out.println("spring 消费者接受到消息 : [" + msg + "]");
  7. }
  8. }

总结

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条消息

步骤 步骤一 : 生产者循环发送消息到simple.queue

在publisher服务中添加一个测试方法 , 循环发送50条消息到simple.queue队列

  1. @Test
  2. public void testWorkQueue() throws InterruptedException{
  3. //队列名称
  4. String queuename = "simple.queue";
  5. //消息
  6. String message = "hello, message__";
  7. for (int i = 0; i < 50;i++){
  8. //发送消息
  9. rabbitTemplate.converAndSend(queuename, message + i);
  10. //避免发送太快
  11. Thread.sleep(20);
  12. }
  13. }

步骤 步骤二 : 编写两个消费者 , 都监听simple.queue

在consumer服务中添加一个消费者 , 也监听simple.queue:

  1. @RabbitListener(queues = "simple.queue")
  2. public void listenSimpleQueueMessage1(String msg) throws InterruptedException{
  3. System.out.println("spring 消费者1接收到消息:["+msg+"]");
  4. Thread.sleep(25);
  5. }
  6. @RabbitListener(queues = "simple.queue")
  7. public void listenSimpleQueueMessage2(String msg) throws InterruptedException{
  8. System.err.println("spring 消费者2接收到消息:["+msg+"]");
  9. Thread.sleep(100);
  10. }

消费预取限制

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

  1. spring:
  2. rabbitmq:
  3. host: 124.221.236.57 #主机名
  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发送消息

步骤 步骤一 : 在consumer服务声明Exchange, Queue, Binding

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

步骤 步骤一: 在consumer服务声明Exchange, Queue, Binding

在consumer服务常见一个类, 添加@Configuration注解, 并声明FanoutExchange, Queue和绑定关系对象Binding,代码如下:

  1. @Configuration
  2. public class FanoutConfig{
  3. //声明FanoutExchange交换机
  4. @Bean
  5. public FanoutExchange fanoutExchange(){
  6. return new FanoutExchange("itcast.fanout");
  7. }
  8. //声明第一个队列
  9. @Bean
  10. public Queue fanoutQueue1(){
  11. return new Queue("fanout.queue1");
  12. }
  13. //绑定队列1和交换机
  14. @Bean
  15. public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
  16. return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
  17. }
  18. // ... 略, 以相同方式声明第2个队列, 并完成绑定
  19. }

步骤 步骤二 : 在consumer服务声明两个消费者

在consumer服务的SpringRabbitListener类中, 添加两个方法, 分别监听fanout.queue1 和 fanout.queue2:

  1. @RabbitListener(queues = "fanout.queue1")
  2. public void listenFangoutQueue1(String msg){
  3. System.out.println("消费者1接收到Fanout消息:["+msg+"]");
  4. }
  5. @RabbitListener(queues = "fanout.queue2")
  6. public void listFanoutQueue2(String msg){
  7. System.out.println("消费者2接收到Fanout消息:["+msg+"]");
  8. }

步骤 步骤三 : 在publisher服务发送消息到FanoutExchange

在pulisher服务的SpringAMQPTest类中添加测试方法:

  1. @Test
  2. public void testFanoutExchange(){
  3. //队列名称
  4. String exchangeName = "itcast.fanout";
  5. //消息
  6. String message = "hello, everyone!";
  7. //发送消息,参数分别是 : 交互机名称, RoutingKey(暂时为空),消息
  8. rabbitTemplate.convertAndSend(exchangeName, "" ,message);
  9. }

总结

交换机的作用是什么?

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

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

  • Queue
  • FanoutExchange
  • Binding

发布订阅-DirectExchange

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

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

案例 利用SpringAMQP演示DirectExchange的使用

实现思路如下:

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

步骤 步骤一 : 在consumer服务声明Exchange,Queue
  1. 在consumer服务中, 编写两个消费者方法, 分别监听direct.queue1和direct.queue2
  2. 并利用@RabbitListener声明Exchange,Queue,Routingkey
  1. @RabbitListener(bindings = @QueueBinding(
  2. value = @Queue(name = "direct.queue1"), //设置 接收direct.queue1队列
  3. //设置路由itcast.direct type: 设置获取类型 DIRECT
  4. exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
  5. key = {"red","blue"} //设置接收指定的ID **可以设置多个,可以同ID
  6. //如果绑定同个key 那么将会被多个队列收到
  7. ))
  8. public void listenDirectQueue1(String msg){
  9. System.out.println("消费者接收到direct.queue1的消息:["+msg+"]");
  10. }
  11. @RabbitListener(bindings = @QueueBinding(
  12. value = @Queue(name = "direct.queue2"),
  13. exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
  14. key = {"red","yellow"}
  15. ))
  16. public void listenDirectQueue2(String msg){
  17. System.out.println("消费者接收到direct.queue2的消息:["+msg+"]");
  18. }

步骤 步骤二 : 在publisher服务发送消息到DirectExchange

在publish服务的SpringAmqpTest类中添加测试方法:

  1. @Test
  2. public void testDirectExchange(){
  3. //路由名称
  4. String exchangeName = "itcast.direct";
  5. //消息
  6. Stirng message = "红色警报! 日本乱排核废水, 导致海洋生物变异, 惊现哥斯拉! "
  7. //发送消息,参数一次为: 交换机名称, Routingkey, 消息
  8. rabbitTemplate.convertAndSend(exchangeName,"red",message);
  9. }

总结

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

  • Fanout交换机将消息路由给每一个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给那个队列
  • 如果多个队列具有相投的RoutingKey, 则与Fanout功能类似

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

发布订阅-TopicExchange

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

实例:

  • china.news 代表有中国的新闻消息;
  • china.weather 代表中国的天气消息;
  • japan.news 则带代表日本新闻;
  • japan.weather 代表日本的天气消息;

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

#: 代指0个或多个单词

**: 代指一个单词

案例 利用SpringAMQP演示TopicExchange的使用

实现思路如下:

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

步骤 步骤一 : 在consumer服务声明Exchange,Queue
  1. 在consumer服务中, 编写两个消费者方法, 分别监听topic.queue1 和 topic.queue2
  2. 并利用@RabbitListener声明Exchange,Queue,RoutingKey
  1. @RabbitListener(bindings = @QueueBinding(
  2. value = @Queue(name = "topic.queue1"),
  3. exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
  4. key = "china.#"
  5. ))
  6. public void listTopicQueue1(String msg){
  7. System.out.println("消费者接收到topic.queue1的消息:["+msg+"]");
  8. }
  9. @RabbitListener(bindings = @QueueBinding(
  10. value = @Queue(name = "topic.queue2"),
  11. exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
  12. key = "#.news"
  13. ))
  14. public void listTopicQueue2(String msg){
  15. System.out.println("消费者接收到topic.queue2的消息:["+msg+"]");
  16. }

步骤 步骤二 : 在publisher服务发送消息到TopicExchange

在publisher服务的SpringAmqpTest类中添加测试方法:

  1. @Test
  2. public void testSendTopicExchange1(){
  3. //交换机名称
  4. String exchangeName = "itcast.topic";
  5. //消息
  6. String message = "hello , china! . I like Xiao";
  7. // 发送消息
  8. rabbitTemplate.convertAndSend(exchangeName,"china.weather",message);
  9. }

总结

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

  • 主要是可以使用*,#通配符

SpringAMQP消息转换器

案例 测试发送Object类型消息

  1. **说明: SpringAMQP的发送方法中, 接收消息的类型是Object, 也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送.**
  1. @Bean
  2. public Queue objectMessageQueue(){
  3. return new Queue("object.queue");
  4. }

在publisher中发送消息以测试:

  1. @Test
  2. public void testSendMap() throws InterruptedException{
  3. //准备消息
  4. Map<String,Object> msg = new HashMap<>();
  5. msg.put("name","Jack");
  6. msg.puy("age",21);
  7. //发送消息
  8. rabbitTemplate.convertAndSend("objcet.queue",msg);
  9. }

消息转换器

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

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

  • 我们在publisher服务引入依赖
  1. <dependency>
  2. <groupId>com.fasterxml.jackson.dataformat</groupId>
  3. <artifactId>jackson-dataformat-xml</artifactId>
  4. <version>2.9.10</version>
  5. </dependency>
  • 我们在publisher服务声明MessageConverter:
  1. @Bean
  2. public MessageConverter jsonMessageConverter(){
  3. return new Jackson2JsonMessageConverter();
  4. }

我们在consumer服务引入Jackson依赖:

  1. <dependency>
  2. <groupId>com.fasterxml.jackson.dataformat</groupId>
  3. <artifactId>jackson-dataformat-xml</artifactId>
  4. <version>2.9.10</version>
  5. </dependency>

我们在consumer服务定义MessageConverter:

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

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

  1. @RabbitListener(queues = "object.queue")
  2. public void listenObjectQueue(Map<Spring, Object> msg){
  3. System.out.println("收到消息:["+msg+"]");
  4. }

总结

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

  • 利用MessageConverter实现的,默认是JDK的序列化
  • 注意发送方法与接收方必须使用相同的MessageConverter

* 推荐使用json

思考

思考一下,这种基于MQ的异步通知操作, 在哪些业务场景中可以用到呢?

分布式搜索 elasticsearch

处识elasticsearch

什么是elasticsearch

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

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

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

elasticsearch的发展

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

官网地址:https://lucene.apache.org/.

Lucene的优势:

  • 易扩展
  • 高性能(基于倒排索引)

Lucene的缺点:

  • 只限于java开发
  • 学习曲线陡峭
  • 不支持水平拓展

2004年Shay Banon基于Lucene开发了Compass

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

官网地址:https://www.elastic.co/cn

目前最新的版本是: 7.12.1

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

  • 支持分布式,可水平拓展
  • 提供Restful接口, 可被任何语言调用

为什么要学习elasticsearch?

搜索引擎技术排名:

  1. Elasticsearch: 开源的分布式搜索引擎
  2. Splunk: 商业项目
  3. Solr: Apache的开源搜索引擎

总结

什么是elasticsearch?

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

什么是elastic stack(ELK) ?

  • 是以elasticsearch为核心的技术栈, 包括beats,Logstash,kibana,elasticsearch

什么是Lucene?

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

正向索引和倒排索引

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

elasticsearch采用倒排索引:

  • 文档(document) : 每条数据就是一个文档
  • 词条(term) : 文档按照语义分成的词语

总结

什么是文档和词条?

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

什么是正向索引?

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

什么是倒排索引?

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

文档

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

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

索引(Index)

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

概念对比

MySQL Elasticsearch 说明
Table Index 索引(index),就是文档的集合,类似数据库的表(table)
Row Document 文档(Document), 就是一条条的数据, 类似数据库中的行(Row), 文档就是JSON格式
Column Field 字段(Field) , 就是JSON文档中的字段, 类似数据库中的列(Column)
Schema Mapping Mapping(映射)是索引中文档的约束, 例如字段类型的约束. 类似数据库的表结构(Schema)
SQL DSL DSL是elasticsearch提供的JSON风格的请求语句, 用来操作elasticsearch,实现CRUD

架构

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

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

总结

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

字段: Json文档中的字段

索引: 同类型文档的集合

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

elasticsearch与数据库的关系:

  • 数据库负责事务类型操作
  • elasticsearch负责海量数据的搜索,分析,计算

安装elasticsearch, kibana

参考课前资料中的文档:

安装elasticsearch

分词器

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

  1. POST /_analyze
  2. {
  3. "analyzer": "standard",
  4. "text": "爱玩游戏的傲!"
  5. }

语法说明:

  • POST : 请求方式
  • /_analyze : 请求路径,这里省略了http://124.221.236.57:9200, 有kibana帮我们补充
  • 请求参数, json风格:
    • analyzer : 分词器类型, 这里默认的standard分词器
    • test : 要分词的内容

处理中文分词, 一般会使用IK分词器. https://github.com/medcl/elasticsearch-analysis-ik

安装IK分词器, 参考课前资料 : 安装elasticsearch.md

测试

IK分词器包含两种模式:

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

ik分词器-拓展字典

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

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
  3. <properties>
  4. <comment>IK Analyzer 扩展配置</comment>
  5. <!-- 用户可以在这里配置自己的扩展字典 *** 添加扩展词典 -->
  6. <entry key="ext_dict">ext.dic</entry>
  7. </properties>

ik分词器-停用词库

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

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

总结

分词器的作用是什么?

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

IK分词器有几种模式?

  • ik_smart : 智能切分, 粗粒度
  • il_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
  • analyzer : 使用那种分词器
  • properties : 该字段的子字段

总结

mapping常见属性有哪些?

  • type : 数据类型
  • index : 是否索引
  • analyzer : 分词器
  • properties : 子字段

type常见的有哪些?

  • 字符串 : text , keyword
  • 数字 : long , integer , short , byte , double , float
  • 布尔 : boolean
  • 日期 : date
  • 对象 : object

创建索引库

ES中通过Restful请求创建索引库 , 文档. 请求内容用DSL语句来表示. 创建索引库克mapping的DSL语法如下:

示例:

查看, 删除索引库

查看索引库语法:

  1. GET /索引库名

示例:

  1. GET /heima

删除索引库的语法:

  1. DELETE /索引库名

示例:

  1. DELETE /heima

修改索引库

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

  1. PUT /索引库名/_mapping
  2. {
  3. "properties":{
  4. "新字段名":{
  5. "type":"integer"
  6. }
  7. }
  8. }

示例:

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

总结

索引库操作有哪些?

  • 创建索引库: PUT/索引库名
  • 查询索引库: GET/索引库名
  • 删除索引库: DELETE/索引库名
  • 添加字段: PUT /索引库名/_mapping

文档操作

添加文档

新增文档的DSL语法如下:

示例:

查看,删除文档

查看文档语法:

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

示例:

  1. GET /heima/_doc/1

删除索引库的语法:

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

示例:

  1. DELETE /heima/_doc/1

修改文档

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

示例:

方式二: 增量修改, 修改指定字段值

实例:

总结

文档操作有哪些?

  • 创建文档: POST /索引库名/_doc/文档id (json文档)
  • 查询文档: GET /索引库名/_doc/文档id
  • 删除文档: DELETE /索引库名/_doc/文档id
  • 修改文档:
    • 全量修改: PUT /索引库名/_doc/文档id{json文档}
    • 增量修改: POST /索引库名/_update/文档id{“doc”:{字段}]

RestClient操作索引库

什么是RestClient

ES官方提供了各种不同语音的客户端, 用来操作ES. 这些客户端的本质就是组装DSL语句, 通过http请求发送给ES. 官方文档地址: https://www.elastic.co/guide/en/elasticsearch/client/index.html

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

根据课前资料提供的酒店数据创建索引库, 索引库名为hotel, mapping属性根据数据库结构定义.

基本步骤如下:

  1. 导入课前资料Demo
  2. 分析数据结构, 定义mapping属性
  3. 初始化JavaRestClient
  4. 利用JavaRestClient创建索引库
  5. 利用JavaRestClient删除索引库
  6. 利用JavaRestClient判断索引库是否存在

步骤 步骤二 : 分析数据结构

mapping要考虑的问题 :

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

小提示

ES中支持两种地理坐标数据类型:

  • geo_point : 由纬度(latitude) 和经度(longitude)确定一个点. 例如: “32.8752345”,”120.2981576”
  • geo_shape : 有多个geo_point组成的复杂几何图形. 例如一条直线, “LINESTRING(-77.03653 38.897676, -77.009051 38.889939)”

小提示

字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段. 示例:

步骤 步骤三 : 初始化JavaRestClient

  1. 引入es的RestHighLevelClient依赖:
    1. <dependency>
    2. <groupId>org.elasticsearch.client</groupId>
    3. <artifactId>elasticsearch-rest-high-level-client</artifactId>
    4. </dependency>
  1. 因为SpringBoot默认的ES版本是7.6.2 , 所以我们需要覆盖默认的ES版本: 一定要跟服务端的版本一致
    1. <properties>
    2. <java.version>1.8</java.version>
    3. <elasticsearch.version>7.12.1</elasticsearch.version>
    4. </properties>
  1. 初始化RestHighLeveClient:
    1. RestHighLevelClient client = new RestHighLeveClient(RestClient.buider(
    2. HttpHost.create("http://http://124.221.236.57/:9200") //自家地址
    3. ))

对象转JSON

  1. <!--FastJson-->
  2. <dependency>
  3. <groupId>com.alibaba</groupId>
  4. <artifactId>fastjson</artifactId>
  5. <version>1.2.71</version>
  6. </dependency>

步骤 步骤四 : 创建索引库

  1. private RestHighLevelClient client;
  2. @Test
  3. void testCreateHotelIndex() throws IOException{
  4. //1 . 创建Request对象
  5. CreateIndexRequest request = new CreateIndexRequest("hotel");
  6. //2 . 请求参数, MAPPING_TEMPLATE 是静态常量字符串, 内容是创建索引库的DSL语句
  7. request.source(MAPPING_TEMPLATE,XContentType.JSON);
  8. //3 . 发起请求
  9. client.indices().create(request, RequestOptions.DEFAULT);
  10. }

步骤 步骤五 : 删除索引库, 判断索引库是否存在

  • 删除索引库代码如下:
  1. private RestHighLevelClient client;
  2. @Test
  3. void testDeleteHotelIndex() throws IOException{
  4. //1. 创建Request对象
  5. DeleteIndexRequest request = new DeleteIndexRequest("hotel);
  6. //2. 发起请求
  7. client.indices().delete(request,RequestOptions.DEFAULT);
  8. }
  • 判断索引库是否存在
  1. private RestHighLevelClient client;
  2. @Test
  3. void testExistsHotelIndex() throws IOException{
  4. //1. 创建Request对象
  5. GetIndexRequest new GetIndexRequest("hotel");
  6. //2. 发起请求
  7. boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
  8. //3. 输出
  9. System.out.println(exists);
  10. }

总结

索引库操作的基本步骤:

  • 初始化RestHighLevelClient
  • 创建XxxIndexRequest. XXX是CREATE , Get , Delete .
  • 准备DSL(CREATE 时需要)
  • 发送请求 . 调用RestHighLevelClient#indices().xxx()方法, xxx是create , exists , delete

案例 利用JavaRestClient实现文档的CRUD

去数据库查询酒店数据, 导入到hotel索引库 , 实现酒店数据的CRUD.
基本操作步骤如下:

  1. 初始化JavaRestClient
  2. 利用JavaRestClient新增酒店数据
  3. 利用JavaRestClient根据id查询酒店数据
  4. 利用JavaRestClient删除酒店数据
  5. 利用JavaRestClient修改酒店数据

步骤 步骤一: 初始化JavaRestClient

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

  1. private RestHighLevelClient client;
  2. public class ElassticsearchDocumentTest{
  3. //客户端
  4. private RestHighLeveLclient client;
  5. @BeforeEach
  6. void setUp(){
  7. client = new RestHighLevelClient(RestClient.builder(
  8. HttpHost.create("http://124.221.236.57:9200")
  9. ));
  10. }
  11. @AfterEach
  12. void tearDown() throws IOException{
  13. client.close();
  14. }
  15. }

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

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

  1. private RestHighLevelClient client;
  2. @Test
  3. void testIndexDocument() throws IOException{
  4. //1. 创建request对象
  5. IndexRequest request = new IndexRequest("indexName").id("1");
  6. //2. 准备JSON文档
  7. reqeust.source("{\"name\":\"Jack\",\"age\":21}",XContentType.JSON);
  8. //3. 发送请求
  9. client.index(request, RequestOptions.DEFAULT);
  10. }

步骤 步骤三: 根据id查询酒店数据

根据id查询到的文档是json, 需要反序列化为java对象:

  1. private RestHighLevelClient client;
  2. @Test
  3. void testGetDocumentById() throws IOException{
  4. //1. 创建request对象
  5. GetRequest request = new GetRequest("indexName","1");
  6. //2. 发送请求, 得到结果
  7. GetRequest response = client.get(request, RequestOptions.DEFAULT);
  8. //解析结果
  9. String json = response.getSourceAsString();
  10. System.out.println(json);
  11. }

步骤 步骤四: 根据id修改酒店数据

修改文档数据有两种:

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

方法二: 局部更新. 只更新部分字段, 我们演示方法二

  1. private RestHighLevelClient client;
  2. @Test
  3. void testUpdateDocumentById() throws IOException{
  4. //1. 创建request对象
  5. UpdateRequest request = new UpdateRequest("indexName","1");
  6. //2. 准备参数, 每2个参数为一对 key value
  7. request.doc(
  8. "age",18,
  9. "name","Rose"
  10. );
  11. //3. 更细文档 提交
  12. client.update(request, RequestOptions.DEFAULT)
  13. }

步骤 步骤五: 根据id删除文档

  1. private RestHighLevelClient client;
  2. @Test
  3. void testUpdateDocument() throws IOException{
  4. //1. 准备request
  5. DeleteRequest request = new DeleteRequest("IndexName","id");
  6. //2. 发起请求
  7. client.delete(request,RequestOptions.DEFAULT)
  8. }

总结

文档操作的基本步骤:

  • 初始化RestHighLevelClient
  • 创建XxxRequest. XXX是Index,Get,Update,Delete
  • 准备参数(Index和Update是需要)
  • 发起请求. 调用RestHighLevelCient#.xxx()方法, xxx是index,get,update,delete
  • 解析结果(Get)时需要

案例 利用JavaRestClient批量导入酒店数据到ES

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

思路:

  1. 利用mybatis-plus查询酒店数据
  2. 将查询到的酒店数据(Hotel)转换为文档数据类型(HotelDoc)
  3. 利用JavaRestClient中的Bulk批量处理, 实现批量新增文档, 示例代码如下
  1. @Test
  2. void testBulk() throws IOEception{
  3. //1. 创建Bulk请求
  4. BulkRequest request = new BulkRequest();
  5. //2. 添加要批量提交的请求 : 这里添加两个新增文档的请求
  6. request.add(new IndexRequest("hotel")
  7. .id("101").source("json source",XContentType.JSON));
  8. request.add(new IndexRequest("hotel")
  9. .id("102").source("json source2", XContentType.JSON));
  10. //3. 发起bulk请求
  11. client.bulk(request,RequestOptions.DEFAULT);
  12. }
  1. //批量创建 bulk
  2. @Test
  3. void testBulkRequest() throws IOException {
  4. // 批量查询酒店数据
  5. List<Hotel> list = iHotelService.list();
  6. //1. 创建Request
  7. BulkRequest request = new BulkRequest();
  8. //2. 准备参数, 添加多个新增的Request
  9. // 转换为文档类型
  10. for (Hotel hotel : list) {
  11. HotelDoc hotelDoc = new HotelDoc(hotel);
  12. String s = JSON.toJSONString(hotelDoc);
  13. request.add(new IndexRequest("hotel")
  14. .id(hotelDoc.getId().toString())
  15. .source(s,XContentType.JSON));
  16. }
  17. //3. 发起请求
  18. client.bulk(request, RequestOptions.DEFAULT);
  19. }

思考

MySQL与Elasticsearch有什么差别呢?

Elasticsearch的文档操作API有什么样的规律?

DSL查询语法

DSL Query的分类

Elasticsearch提供了基于JSON的DSL(Domain Specific Language) 来定义查询. 常见的查询类型包括:

  • 查询所有: 查询出所有数据, 一般测试用. 例如: match_all
  • 全文检索(full text) 查询: 利用分词器对用户输入内容分词, 然后去倒排索引库中匹配. 例如:
    • match_query
    • multi_match_query
  • 精确查询: 根据精确词条值查找数据, 一般是查找keyword, 数值, 日期, boolean等类型字段. 例如:
    • ids
    • range
    • term
  • 地理(geo)查询: 根据经纬度查询. 例如:
    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询: 复合查询可以将上述各种查询条件组合起来, 合并查询条件. 例如:
    • bool
    • function_score

DSL Query基本语法

查询的基本语法如下:

  1. GET /indexName/_search
  2. {
  3. "query":{
  4. "查询类型":{
  5. "查询条件":"条件值"
  6. }
  7. }
  8. }

总结

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

  • GET /索引库名/_search
  • {“query”:{“查询类型”:{“FIELD”:”TEXT”}}}

全文检索查询

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

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

  1. GET /indexName/_search
  2. {
  3. "query":{
  4. "match":{
  5. "FIELD":"TEXE"
  6. }
  7. }
  8. }

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

  1. GET /indexName/_search
  2. {
  3. "query":{
  4. "multi_match":{
  5. "query":"TEXT",
  6. "fields":["FIELD1","FIELD2"]
  7. }
  8. }
  9. }

总结

match个和multi_match的区别是什么?

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

精确查询

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

  • term: 根据词条精确值查询
  • range: 根据值的范围查询

精确查询-语法

精确查询一般是根据id, 数值, keyword类型, 或布尔字段来查询. 语法如下:

term查询:

  1. #term查询
  2. GET /indexName/_search
  3. {
  4. "query":{
  5. "term":{
  6. "FIELD":{
  7. "value":"VALUE"
  8. }
  9. }
  10. }
  11. }

range查询:

  1. //range查询
  2. GET /indexName/_search
  3. {
  4. "query":{
  5. "range":{
  6. "FIELD":{
  7. "gte":10,
  8. "lte":20
  9. }
  10. }
  11. }
  12. }

总结

精确查询常见的有哪些?

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

地理查询

根据经纬度查询. 常见的使用场景包括:

  • 携程: 查询为附近的酒店
  • 滴滴: 查询我附近的出租车
  • 微信: 查询我附近的人

根据经纬度查询, 官方文档. 例如:

  • geo_bounding_box: 查询geo_point值落在某个矩形范围的所有文档
  1. //geo_bounding_box查询
  2. GET /indexName/_search
  3. {
  4. "query":{
  5. "geo_bounding_box":{
  6. "FIELD":{
  7. "top_left":{
  8. "lat":31.1,
  9. "lon":121.5
  10. },
  11. "bottom_right":{
  12. "lat":30.9,
  13. "lon":121.7
  14. }
  15. }
  16. }
  17. }
  18. }
  • lte : 小于等于
  • gte : 大于等于

根据经纬度查询, 官方文档. 例如:

  • geo_distance: 查询到指定中心点小于某个距离值的所有文档
  1. //geo_distance查询
  2. GET /indexName/_search
  3. {
  4. "query":{
  5. "geo_distance":{
  6. "distance":"15km",
  7. "FIELD":"31.21,121.5"
  8. }
  9. }
  10. }

复合查询

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

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

相关性算分

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

例如,我们搜索”虹桥如家”,结果如下:

总结

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

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

Function Score Query

使用 function score query , 可以修改文档的相关性算分(query score) , 根据新得到的算分排序.

  1. GET /hotel/_search
  2. {
  3. "qeury":{
  4. "function_score":{
  5. "query":{"match":{"all":"外滩"}},
  6. "functions":[
  7. {
  8. "filter":{"term":{"id":"1"}},
  9. "weight":10
  10. }
  11. ],
  12. "boost_mode":"multiply"
  13. }
  14. }
  15. }

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

把这个问题翻译一下, function score需要的三要素:

  1. 哪些文档需要加权?
    品牌为如家的酒店
  2. 算分函数是什么?
    weight就可以
  3. 加权模式是什么?
    求和

总 结

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

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

Boolean Query

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

  • must : 必须匹配每个子查询, 类似”与”
  • should : 选择性匹配子查询, 类似”或”
  • must_not : 必须不匹配, 参与算分, 类似”非”
  • filter : 必须匹配, 不参与算分

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

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

总结

bool 查询有几种逻辑关系?

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

搜索结果处理

排序

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

普通类型排序

  1. GET /indexName/_search
  2. {
  3. "query":{
  4. "match_all":{}
  5. },
  6. "sort":[
  7. {
  8. "FIELD":"desc" //排序字段和排序方式 ASC, DESC
  9. }
  10. ]
  11. }

地理坐标排序

  1. GET /indexName/_search
  2. {
  3. "query":{
  4. "match_all":{}
  5. },
  6. "sort":[
  7. {
  8. "_geo_distance":{
  9. "FIELD":"纬度,经度",
  10. "order":"asc",
  11. "unit":"km"
  12. }
  13. }
  14. ]
  15. }

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

评价是score字段, 价格是price字段, 按照顺序添加两个排序规则即可.

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

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

获取经纬度的方式: https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/

  1. # 找到121.612282, 31.034661距离的酒店, 升序排序
  2. GET /hotel/_search
  3. {
  4. "query": {
  5. "match_all": {}
  6. }
  7. , "sort": [
  8. {
  9. "_geo_distance": {
  10. "location": {
  11. "lat": 31.034661,
  12. "lon": 121.612282
  13. },
  14. "order": "asc",
  15. "unit": "km"
  16. }
  17. }
  18. ]
  19. }

分页

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

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

  1. GET /hotel/_search
  2. {
  3. "query":{
  4. "match_all":{}
  5. }
  6. "from": 990, //分页开始的位置, 默认为0
  7. "size": 10, //期望获取的文档总数
  8. "sort":[
  9. {"price":"asc"}
  10. ]
  11. }

深度分页问题

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 开始不推荐, 建议使用after search方案.

高亮

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

原理是这样的:

  • 将搜索结果中的关键字用标签标记出来
  • 在页面中给标签添加css样式
  1. GET /hotel/_search
  2. {
  3. "query":{
  4. "match":{
  5. //这里不能用match_all 一定要带上关键字
  6. "FIELD":"TEXT"
  7. }
  8. },
  9. "highlight":{
  10. "fields":{//指定要高亮的字段
  11. "FIELD":{
  12. "pre_tags":"<em>", //用来标记高亮字段的前缀标签
  13. "post_tags":"</em>" //用来标记高亮字段的后缀标签
  14. }
  15. }
  16. }
  17. }
  1. # 高亮查询 , 默认情况下 , ES搜索字段必须与高亮字段一致否则不会高亮
  2. #可以不用写前后缀标签 以为默认是 <em>标签
  3. GET /hotel/_search
  4. {
  5. "query": {
  6. "match": {
  7. "all": "如家"
  8. }
  9. },
  10. "highlight": {
  11. "fields": {
  12. "name": {
  13. //需不需要字段匹配
  14. "require_field_match": "false"
  15. }
  16. }
  17. }
  18. }

总结

搜索结果处理整体语法:

RestClient查询文档

快速入门

我们通过match_all来演示下基本的API, 先看请求DSL的组织:

  1. @Test
  2. void testMatchAll() throws IOException{
  3. //1. 准备Request
  4. SearchRequest request = SearchRequest("hotel");
  5. //2. 组织DSL参数
  6. request.source()
  7. .query(QueryBuilders.matchAllQuery());
  8. //3. 发送请求 , 得到响应
  9. SearchResponse response = client.search(request,RequestOptions.DEFAULT);
  10. //...解析响应结果
  11. }

我们通过match_all来演示下基本的API, 在看结果的解析:

  1. @Test
  2. void testMatchAll() throws IOException{
  3. //...略
  4. //4. 解析结果
  5. SearchHits searchHits = response.getHits();
  6. //4.1 查询总条数
  7. long total = searchHits.getTotalHits().value;
  8. //4.2 查询的结果数组
  9. SearchHit[] hits = searchHits.getHits();
  10. for(SearchHit hit : hits){
  11. //4.3 得到source
  12. String json = hit.getSourceAsString();
  13. //4.4 打印
  14. System.out.println(json);
  15. }
  16. }

RestAPI中其中构建DSL是通过HighLevelRestClient中的resource()来实现的, 其中包含了查询, 排序, 分页, 高亮等所有功能:

RestAPI中其中构建查询条件的核心部分是由一个名为QueryBuilders的工具类提供的, 其中包含了各种查询方法:

总结

查询的基本步骤是:

  1. 创建SearchRequest对象
  2. 准备Request.source(), 也就是DSL
    1. QueryBuilders来构建查询条件
    2. 传入Request.source()的query()方法
  3. 发送请求,得到结果
  4. 解析结果(参考JSON结果, 从外到内,逐层解析)

全文检索查询

全文检索的match和multi_match查询与match_all的API基本一致. 差别就是查询条件, 也就是query的部分.

  1. //单字段查询
  2. QueryBuilders.matchQuery("all","如家");
  3. //多字段查询
  4. QueryBuilders.multiMatchQuery("如家","name","business");

精确查询

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

  1. // 词条查询
  2. QueryBuilders.termQuery("city","杭州");
  3. // 范围查询
  4. QueryBuilders.rangeQuery("price").gte(100).lte(150);

复合查询

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

  1. //创建布尔查询
  2. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  3. //添加must条件
  4. boolQuery.must(QueryBuilders.termQuery("city","杭州"));
  5. //添加filter条件
  6. boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

总结

要构建查询条件, 只要记住一个类:QueryBuilders

排序和分页

搜索结果的排序和分页是与query同级的参数, 对应的API如下:

  1. // 查询
  2. request.source().query(QueryBuilders.matchAllQuery());
  3. // 分页
  4. request.source().from.size(5);
  5. //价格排序
  6. request.source().sort("price",SortOrder.ASC);

高亮

高亮API包括请求DSL构建和结果解析两部分. 我们先看请求的DSL构建:

  1. request.source().highlighter(new HighlightBuilder(
  2. .field("name")
  3. //是否需要与查询字段匹配
  4. .requireFieldMatch(false)
  5. ))

高亮结果解析

高亮的结果处理相对比较麻烦:

  1. // 获取source
  2. HotelDoc hotelDoc = JSOn.parseObject(hit.getSourceAsString(),HotelDoc.class);
  3. // 处理高亮
  4. Map<String,HighlightField> highlightFields = hit.getHighlightFields();
  5. if(!CollectionUtils.isEmpty(highlightFields)){
  6. //获取高亮字段结果
  7. HighlightField highlightField = highlightFields.get("name");
  8. if(highlightField != null){
  9. //取出高亮结果数组中的第一个, 就是酒店名称
  10. String name = highlightField.getFragments()[0].string();
  11. hotelDoc.setName(name);
  12. }
  13. }
  14. private void handleResponseHig(SearchResponse response) {
  15. //4. 解析响应
  16. SearchHits searchHits = response.getHits();
  17. //4.1 获取总条数
  18. long total = searchHits.getTotalHits().value;
  19. System.out.println("共搜索到:" + total + "条");
  20. //4.2 文档数组
  21. SearchHit[] hits = searchHits.getHits();
  22. //4.3 遍历
  23. for (SearchHit hit : hits) {
  24. String json = hit.getSourceAsString();
  25. // 反序列化
  26. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  27. // 获取高亮结果
  28. Map<String, HighlightField> highlightFields = hit.getHighlightFields();
  29. if (!CollectionUtils.isEmpty(highlightFields)){
  30. // 根据字段名获取高亮结果
  31. HighlightField highlightField = highlightFields.get("name");
  32. if (highlightField != null){
  33. // 获取高亮值
  34. String name = highlightField.getFragments()[0].string();
  35. // 覆盖高亮结果
  36. hotelDoc.setName(name);
  37. }
  38. }
  39. System.out.println("hotelDoc = " + hotelDoc);
  40. }
  41. }

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

黑马旅游案例 —基本搜索和分页

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

我们课前提供的hotel-demo项目中, 自带了前端页面, 启动后可以看到:

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

  1. 定义实体类, 接收前端请求
  2. 定义controller接口, 接收页面请求, 调用IHotelService的search方法
  3. 定义IHotelService中的search方法, 利用match查询实现根据关键字搜索酒店信息

步骤 步骤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接口, 接收前端请求

定义一个HotelController, 声明查询接口, 满足下列请求:

  • 请求方式: Post
  • 请求路径: /hotel/list
  • 请求参数: 对象, 类型为RequestParam
  • 返回值: PageResult, 包含两个属性
    • Long total: 总条数
    • Listhotels: 酒店数据

案例 案例2: 添加品牌, 城市, 星际, 价格等过滤器功能

需要效果如图:

步骤:

  1. 修改RequestParams类, 添加brand,city,starName,minPrice,maxPrice等参数
  2. 修改search方法的实现, 在关键字搜索时,如歌brand等参数存在, 对其做过滤

步骤 步骤一: 拓展IUserService的search方法的参数列表

修改RequestParams类, 接收所有参数:

  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 brand;
  8. private String starName;
  9. private String city;
  10. private Integer minPrice;
  11. private Integer maxPrice;
  12. }

步骤 步骤二: 修改search方法, 在match查询基础上添加过滤条件

过滤条件包括:

  • city精确匹配
  • brand精确匹配
  • starName精确匹配
  • price范围过滤

注意事项:

  • 多个条件之间是AND关系, 组合多条件用BooleanQuery
  • 参数存在需要过滤, 做好非空判断

案例 案例三: 我附近的酒店

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

我们要根据这个坐标, 将酒店结果按照到这个点的距离升序排序

实现思路如下:

  • 修改RequestParams参数, 接收location字段
  • 修改search方法业务逻辑, 如果location有值, 添加根据geo_distance排序的功能

距离排序

距离排序与普通字段排序有所差异, API如下:

  1. //价格排序
  2. request.source().sort("price",SortOrder.ASC);
  3. //距离排序
  4. request.source().sortSortBuilders
  5. .geoDistanceSort("location",new GeoPoint("31.21, 121.5"))
  6. .order(SortOrder.ASC)
  7. .Unit(DistanceUnit.KILOMETERS)
  8. );

按照距离排序后, 还需要具体的距离值:

发起距离排序请求时, 会返回 sort 响应值, 距离 就在sort:

案例 案例四: 让指定的酒店在搜索结果中排名指定

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

实现步骤分析:

  1. 给HotelDoc类添加isAD字段, Boolean类型
  2. 挑选几个你喜欢的酒店, 给他的文档数据添加isAD字段, 值为true
  3. 修改search方法, 添加function score功能, 给isAD值为true的酒店增加权重

组合查询-function score

Function Score查询可以控制文档的相关性算分, 使用方式如下:

  1. //7. function score
  2. FunctionScoreQueryBuilder functionScoreQueryBuilder =
  3. QueryBUilders.functionScoreQuery(
  4. QueryBuilders.matchQuery("name","外滩"),
  5. new FunctionScoreQueryBuilder.FilterFunctionBuider(
  6. QueryBuilders.termQuery("brand","如家"),
  7. ScoreFunctionBuilders.weightFactorFunction(5)
  8. )
  9. );
  10. sourceBuilder.query(functionScoreQueryBuilder);
  1. FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{});

思考

酒店竞价排名中, 我们是给带isAD标记的酒店提高分数, 从而排名靠前, 但是排名不分先后.

我希望让出广告费不同的酒店, 排名高低不同, 又该怎么实现那?

数据聚合

聚合的分类

官方文档 : https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html

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

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

总结

什么是聚合?

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

聚合的常见种类有哪些?

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

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

  • keyword
  • 数值
  • 日期
  • 布尔

DSL实现Bucket聚合

现在, 我们要统计所有数据中的酒店品牌有几种, 此时可以根据酒店品牌的名称做聚合.

类型为term类型, DSL示例:

  1. GET /hotel/_search
  2. {
  3. "size" : 0, //设置size为0 , 结果中包含
  4. "aggs" : {//定义聚合
  5. "brandAgg": { //给聚合起个名字
  6. "terms" : { //聚合的类型 , 按照品牌值聚合 , 所以选择term
  7. "field" : "brand", //参与聚合的字段
  8. "size": 20 //希望获取聚合结果数量
  9. }
  10. }
  11. }
  12. }

Bucket聚合-聚合结果排序

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

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

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

Bucket聚合-限定聚合范围

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

  1. GET /hotel/_search
  2. {
  3. "query":{
  4. "range":{
  5. "price":{
  6. "lte": 200 //只对200元一下的文档聚合
  7. }
  8. }
  9. },
  10. "size": 0,
  11. "aggs":{
  12. "brandAgg":{
  13. "terms":{
  14. "field":"brand",
  15. "size" : 20
  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. "BrandAgg": {
  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实现聚合

我们以品牌聚合为例, 演示下Java的RestClient使用, 先看请求组装:

  1. request.source().size(0);
  2. request.source().aggregation(
  3. AggregationBuilders
  4. .terms("brand_agg")
  5. .field("brand")
  6. .size(20)
  7. );

再看下聚合结果解析

  1. // 解析聚合结果
  2. Aggregations aggregations = response.getAggregations();
  3. // 根据名称获取聚合结果
  4. Terms brandTerms = aggregation.get("brand_agg");
  5. // 获取桶
  6. List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
  7. // 遍历
  8. for (Terms.Bucket buckets){
  9. //获取key, 也就是品牌名
  10. String brandName = bucket.getKeyAsString();
  11. sout(brandName);
  12. }

聚合 - RestClient

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

需求: 搜索页面的品牌, 城市等信息不应该是在页面写死, 而是通过聚合索引库中的酒店数据得来的:

对接前端接口

前端页面会向服务端发起请求, 查询品牌, 城市, 星级等字段的聚合结果:

可以看出请求参数与之前search时的RequestParam完全一致, 这是在限定聚合时的文档范围.

例如: 用户搜索”外滩”, 价格在300~600, 那聚合必须是在这个搜索条件基础上完成.

因此我们需要:

  1. 编写controller 接口, 接收该请求.
  2. 修改IUserService#getFilters()方法, 添加Requestparam参数
  3. 修改getFilters方法的业务, 聚合时添加query条件

自动补全

使用拼音分词器

要实现根据字母做补全, 就必须对文档按照拼音分词. 在GitHub上恰好有elasticsearch的拼音分词插件. 地址:

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

  1. docker restart es 重启es

自定义分词器

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

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

我们可以在创建索引库时, 通过settings来配置自定义的analyzer(分词器):

  1. PUT /test
  2. {
  3. "settings":{
  4. "analyzer":{
  5. "analyzer":{// 自定义分词器
  6. "my_analyzer":{//分词器名称
  7. "tokenizer":"ik_max_word",
  8. "filter":"pinyin"
  9. }
  10. }
  11. }
  12. }
  13. }
  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":{// 自定义tokenizer filter
  12. "py":{//过滤器名称
  13. "type":"pinyin", //过滤器类型, 这里是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_tokenize":false
  20. }
  21. }
  22. }
  23. }
  24. }

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

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

  1. PUT /test
  2. {
  3. "settings":{
  4. "analysis":{
  5. "analyzer":{
  6. "my_analyzer":{
  7. "tokenizer":"ik_max_word","filter":"py"
  8. }
  9. },
  10. "filter":{
  11. "py":{...}
  12. }
  13. }
  14. },
  15. "mappings":{
  16. "properties":{
  17. "name":{
  18. "type":"text",
  19. "analyzer":"my_analyzer",
  20. "search_analyzer":"ik_smart" //****
  21. }
  22. }
  23. }
  24. }

总结

如何使用拼音分词器?

  1. 下载pinyin分词器
  2. 解压并放到elasticsearch的plugin目录
  3. 重启即可

如何自定义分词器?

  1. 创建索引库时, 在settings中配置, 可以包含三部分
  2. character filter
  3. tokenizer
  4. filter

拼音分词器注意事项?

  • 为了避免搜索到同音字, 搜索时不要使用拼音分词器

completion suggester 查询

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

  • 参与补全查询的字段必须是completion类型.
  1. //创建索引库
  2. PUT /test
  3. {
  4. "mappings":{
  5. "properties":{
  6. "title":{
  7. "type":"completion"
  8. }
  9. }
  10. }
  11. }
  • 字段的内容一般是用来补全的多个词条形成的数组.
  1. //示例数据
  2. POST /test/_doc
  3. {
  4. "title":["Sony","WH-1000XM3"]
  5. }
  6. POST /test/_doc
  7. {
  8. "title":["SK-II","PITERA"]
  9. }
  10. POST /test/_doc{
  11. "title":["Nintendo","switch"]
  12. }

查询语法如下:

  1. GET /test/_seach
  2. {
  3. "suggest":{
  4. "title_suggest":{
  5. "text":"s", //关键字
  6. "completion":{
  7. "field":"title", //补全查询的字段
  8. "skip_duplicates": true, //跳过重复的
  9. "size": 10 //获取前10条结果
  10. }
  11. }
  12. }
  13. }

总结

自动补全对字段的要求:

  • 类型是completion类型
  • 字段值是多词条的数组

酒店数据自动补全

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

实现思虑如下:

  1. 修改hotel索引库结构, 设置自定义拼音分词器
  2. 修改索引库的name, all字段, 使用自定义分词器
  3. 索引库添加一个新字段suggestion, 类型为completion类型, 使用自定义的分词器
  4. 给HotelDoc类添加suggestion字段, 内容包含brand, business
  5. 重新导入数据到hotel库
  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. public HotelDoc(Hotel hotel) {
  19. this.id = hotel.getId();
  20. this.name = hotel.getName();
  21. this.address = hotel.getAddress();
  22. this.price = hotel.getPrice();
  23. this.score = hotel.getScore();
  24. this.brand = hotel.getBrand();
  25. this.city = hotel.getCity();
  26. this.starName = hotel.getStarName();
  27. this.business = hotel.getBusiness();
  28. this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
  29. this.pic = hotel.getPic();
  30. if (this.business.contains("、")){
  31. // business 有多个值, 需要切割
  32. String[] arr = this.business.split("、");
  33. // 添加元素
  34. this.suggestion = new ArrayList<>();
  35. this.suggestion.add(this.brand);
  36. Collections.addAll(this.suggestion, arr);
  37. }else {
  38. this.suggestion = Arrays.asList(this.brand, this.business);
  39. }
  40. }
  41. }

RestAPI实现自动补全

先看请求参数结构的API:

  1. // 1. 准备请求
  2. SearchRequest request = new SearchRequest("hotel");
  3. request.source()
  4. .suggest(new SuggestBuilder().addSuggestion(
  5. "mySuggestion",
  6. SuggestBuilders
  7. .completionSuggestion("title")
  8. .prefix("h")
  9. .skipDuplicates(true)
  10. .size(10)
  11. ));
  12. // 3. 发送请求
  13. client.search(request, RequestOptions.DEFAULT);

再来看结果解析:

  1. // 4. 处理结果
  2. Suggest suggest = response.getSuggest();
  3. // 4.1. 根据名称获取补全结果
  4. CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggestion");
  5. // 4.2. 获取options并遍历
  6. for (CompletionSuggestion.Entry.Option option : suggestion.getOptions){
  7. //4.3. 获取option中的text, 也就是补全的词条
  8. String text = option.getText().string();
  9. System.out.println(text);
  10. }

案例 实现酒店搜索页面输入框的自动补全

查看前端页面, 可以发现当我们在输入框键入时, 前端会发起ajax请求:

在服务端写接口, 接收该请求, 返回补全结果的集合, 类型为List

数据同步*

数据同步问题分析

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中数据
  • 启动并测试数据同步功能

ES集群

ES集群结构

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

  • 海量数据存储问题: 将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
  • 单点故障问题: 将分片数据在不同节点备份(replica)

搭建ES集群

我们计划利用3个docker容器模拟3个es的节点. 具体步骤参考elasticsearch第一天课前资料:

ES集群的节点角色

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

节点类型 配置参数 默认值 节点职责
master eligible node.master true 备选主节点:主节点可以管理和记录集群状态,决定分片在那个节点,处理创建和删除索引库的请求
data node.data true 数据节点:存储数据,搜索,聚合,CRUD
ingest node.ingest true 数据储存之前的预处理
coordinating 上面3个参数都为false则为coordinating节点 路由请求到其他节点合并其他节点处理的结果,返回给用户

ES集群的分布式查询

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

ES集群的脑裂

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

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

总结

master eligible节点的作用是什么?

  • 参与集群选主
  • 主节点可以管理集群状态, 管理分片信息, 处理创建和删除索引库的请求

data节点的作用是什么?

  • 数据的CRUD

coordinator节点的作用是什么?

  • 路由请求到其他节点
  • 合并查询到的结果,返回给用户

ES集群的分布式储存

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

elasticsearch会通过hash算法来计算文档应该存储到那个分片: