单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:
缺点:
分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。
优点:
分布式架构的要考虑的问题:
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。
微服务技术对比
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud
SpringCloud集成了各种微服务功能组件,并基于Spring Boot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
在order-service的OrderApplication中注册RestTemplate
修改order-service中的OrderServicel的queryOrderByld方法
微服务调用方式
服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
服务既可以是服务提供者,也可以是服务消费者。
服务调用关系
在Eureka架构中,微服务角色有两类:
搭建EurekaServer服务步骤如下:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
将user-service服务注册到EurekaServer步骤如下:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
注册user-service
另外,我们可以将user-service多次启动,模拟多实例部署,但为了避免端口冲突,需要修改端口设置:
通过Copy Configuration…中的VM options: -Dserver.port=8082
服务注册
无论是消费者还是提供者,引入eureka-client依赖, 知道eureka地址后,都可以完成服务注册
在order-service完成服务拉取
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
通过定义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,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过配置开启饥饿加载
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。默认端口 8848
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud.nacos}
pom
import
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
spring:
cloud:
nacos:
server-addr: localhost:8848
spring:
cloud:
nacos:
server-addr: localhost:8848 #nacos的服务地址
discovery:
cluster-name: TJ #集群名称
修改 application.yml 文件,添加 spring.cloud.nacos.discovery.cluster-name 即可
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: TJ
userservice: #在服务消费者配置服务提供者的服务名
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
# Nacos优先访问本地集群,在本地集群内部采用随机的负载均衡规则
NacosRule负载均衡策略
实际部署中会出现这样的场景:
步骤:
实例的权重控制
Namespace -> Group -> Service (Data) -> 集群 -> 实例
步骤:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: TJ
namespace: 473adcbe-4c0f-4225-8189-44e195901c13 # dev环境的命名空间的 ID
Nacos环境隔离
服务注册到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的相同点和不同点
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
spring:
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 # nacos 地址
config:
file-extension: yaml # 文件后缀名
将配置交给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读取多个配置文件:
[spring.application.name]-[spring-profiles.active]yaml,例如:userservice-dev.yaml
[spring.application.name].yaml,例如:userservice.yaml
无论profiles如何变化,[spring.application.name],yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件
多种配置的优先级:
[服务名]-[spring.profile.active].yaml, 环境配置
[服务名].yaml, 默认配置,多环境共享~
[服务名]-[环境]yaml > [服务名].yaml > 本地配置
Nacos生产环境下一定要部署为集群状态 nacos集群搭建.md
集群搭建步骤:
进入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
修改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地址
先来看我们以前利用RestTemplate发起远程调用的代码:
String url "http://userservice/user/"+order.getUserId();
Useruser restTemplate.getForObject(url,User.class);
存在下面的问题:
Feign是一个声明式的http客户端,官方地址:
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
定义和使用Feign客户端
使用Feign的步骤如下:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
使用注解 @EnableFeignClients
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
4.用Feign客户端代替RestTemplate
主要是基于SpringMVC的注解来声明远程调用的信息,比如:
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的性能主要包括:
Feign添加HttpClient的支持:
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
feign:
httpclient:
enabled: true # 支持httpclient开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径的最大连接数
Feign的优化:
1.日志级别尽量用basic
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
Feign的最佳实践:
①让controller和FeignClient继承同一接口
②将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用
实现最佳实践方式二的步骤如下:
当定义的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,建议使用以下方法解决:
@Autowired
@Qualifier("beanName")
private MyBean myBean;
这样Spring就会根据"beanName"找到唯一要注入的Bean。
@Bean
@Primary
public MyBean myBean() {
return new MyBean();
}
这里加上了@Primary注解表示该Bean是默认Bean,当有多个同类型的Bean时,Spring默认使用加上了@Primary注解的Bean。
@Autowired
private List myBeans;
这样Spring会把所有同类型的Bean注入到这个List中,你可以遍历这个List去使用你需要的Bean。
不同包的FeignClient的导入有两种方式:
① 在 @EnableFeignClients 注解中添加basePackages, 指定 FeignClient 所在的包
② 在 @EnableFeignClients 注解中添加clients, 指定具体 FeignClient 的字节码
网关功能:
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
在 SpringCloud 中网关的实现包括两种:
Zuul是基于 Servlet 的实现,属于阻塞式编程。而 SpringCloudGateway 则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
网关的作用:
·对用户请求做身份认证、权限校验
·将用户请求路由到微服务,并实现负载均衡
·对用户请求做限流
搭建网关服务的步骤:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
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/开头就符合要求
网关搭建步骤:
路由配置包括:
网关路由可以配置的内容包括:
我们在配置文件中写的断言规则只是字符串,这些字符串会被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是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
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的作用是什么?
① 对所有路由都生效的过滤器
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与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,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
过滤器的执行顺序
详细内容,可以查看源码:
// 方法是先加载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 (自定义过滤器)
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域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跨域要配置的参数包括哪几个?
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Liux内核,因此可以在任意Linux操作系统上运行
Docker是一个快速交付应用、运行应用的技术:
虚拟机 是在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在Windows系统里面运行Ubuntu系统,这样就可以运行任意的Ubuntu应用了。
Docker和虚拟机的差异:
docker是一个系统进程;虚拟机是在操作系统中的操作系统
docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
容器(Container.):镜像中的应用程序运行后形成的进程就是容器,只是 Docker 会给容器做隔离,对外不可见。
DockerHub : DockerHub是一个Docker镜像的托管平台。这样的平台称为Docker Registry。
国内也有类似于DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等。
Docker 是一个CS架构的程序,由两部分组成:
服务端(server) Docker守护进程,负责处理Docker指令,管理镜像、容器等
客户端(client) 通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
镜像: 将应用程序及其依赖、环境、配置打包在一起
容器:镜像运行起来就是容器,一个镜像可以运行多个容器
Docker 结构:
服务端:接收命令或远程请求,操作镜像或容器
客户端:发送命令或者请求到Docker服务端
DockerHub:
一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
企业部署一般都是采用Linux操作系统,而其中又数CentOS发行版占比最多,因此我们在CentOS下安装 Docker
/var/run/yum.pid 已被锁定,PID 为 2985 的另一个程序正在运行
这个错误表示 Yum 已经在运行并且已使用linux系统的锁,以避免出现竞争条件。如果尝试启动另一个 Yum 进程,将会引发此类冲突,并且该进程将无法运行。
为了解决这个问题,您可以尝试以下几个步骤:
错误:软件包:docker-ce-rootless-extras-23.0.5-1.el7.x86_64 (docker-ce-stable) 需要:fuse-overlayfs >= 0.7
这个问题表明您的系统缺少所需的软件包或软件包版本过低,因此Docker安装失败。为了解决这些错误,您可以尝试以下几个步骤:
镜像名称一般分两部分组成:[repository]:[tag]
在没有指定tag时,默认是latest,代表最新版本的镜像
镜像操作:
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服务
sudo vim /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
以上命令告诉 systemd 在启动 docker 服务时执行指定的命令,这里我们指定了 /usr/bin/dockerd 命令来启动 docker 服务。
sudo systemctl enable docker.service
1.去DockerHub搜索Redis镜像
2.查看Redis镜像的名称和版本
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
创建运行一个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!
\nWelcome 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
数据卷(volume) 是一个虚拟目录,指向宿主机文件系统中的某个目录。
操作数据卷
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 参数来挂载一个数据卷到某个容器目录
创建一个 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密码
镜像 是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
镜像是分层结构,每一层称为一个Layer
Baselmage层:包含基本的系统函数库、环境变量、文件系统
Entrypoint :入口,是镜像中应用启动的命令
其它:在Baselmage基础上添加依赖、安装程序、完成整个应用的安装和配置
Dockerfile 就是一个文本文件,其中包含一个个的指令(Instruction), 用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层 Layer
请参考官网文档:
# 构建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
什么是DockerCompose
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。
DockerCompose的详细语法参考官网:
“TCP connection reset by peer” 的错误消息通常表示在尝试连接到远程服务器时遇到了主机或防火墙的问题。一些可能导致该错误的原因包括:
为了解决此问题,您可以尝试以下几个步骤:
# 首先查看网络的连通性,没有问题
ping www.baidu.com
# 查看防火墙的状态,看一下是否在运行,
1.使用防火墙管理工具打开防火墙配置。 在大部分的Linux发行版中,都有一个默认的防火墙管理工具(如firewalld或iptables等)。您可以使用以下命令打开防火墙配置界面, 并记住开启前的配置:
复制代码sudo firewall-cmd --state #检查当前防火墙状态并确定防火墙所采用的管理器,不同管理器操作可参考相关文档
sudo firewall-cmd --list-all #列出当前所有的防火墙规则
复制代码sudo systemctl stop firewalld.service #火墙管理工具为systemd时
sudo service iptables stop #火墙管理工具为iptables时
复制代码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
私有镜像仓库地址 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
微服务间基于Feign的调用就属于同步方式,存在一些问题。
总结
同步调用的优点:
时效性较强,可以立即得到结果
同步调用的问题:
耦合度高
性能和吞吐能力下降
有额外的资源消耗
有级联失败问题
异步调用常见实现就是事件驱动模式
优势一:服务解耦
优势二:性能提升,吞吐量提高
优势三:服务没有强依赖,不担心级联失败问题
优势四:流量削峰
总结
异步通信的优点:
耦合度低
吞吐量提升
故障隔离
流量削峰
异步通信的缺点:
依赖于Broker的可靠性、安全性、吞吐能力
架构复杂了,业务没有明显的流程线,不好追踪管理
MQ(MessageQueue), 中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:Messaging that just works — RabbitMQ
RabbitMQ的结构和概念
RabbitMQ 中的几个概念:
Channel 操作MQ的工具
Exchange 路由消消
Queues 缓存消息
Virtual Hosts 虚拟主机,是对queue、exchange等资源的逻辑分组
MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:
基本消息队列(BasicQueue)
工作消息队列 (WorkQueue)
发布订阅 (Publish、Subscribe), 又根据交换机类型不同分为三种:
Fanout Exchange 广播
Direct Exchange 路由
Topic Exchange 主题
官方的 HelloWorld 是基于最基础的消息队列模型来实现的,只包括三个角色:
publisher 消息发布者,将消息发送到队列queue
queue 消息队列,负责接受并缓存消息
consumer 订阅队列,处理队列中的消息
总结
基本消息队列的消息发送流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
基本消息队列的消息接收流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消费行为handleDelivery()
5.利用channel将消费者与队列绑定
SpringAmqp的官方地址:
利用SpringAMQP实现HelloWorld中的基础消息队列功能
流程如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
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);
}
}
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如何发送消息?
SpringAMQP如何接收消息?
引入 amqp 的 starter 依赖
配置 RabbitMQ 地址
定义类,添加 @Component 注解
类中声明方法,添加 @RabbitListener 注解,方法参数就时消息
Work queue 工作队列,可以提高消息处理速度,避免队列消息堆积
模拟 WorkQueue , 实现一个队列绑定多个消费者
基本思路如下:
1.在oublisher服务中定义测试方法,每秒产生50条消息,发送到 simple.queu
2.在consumer服务中定义两个消息监听者,都监听simple.queue队列
3.消费者1每秒处理50条消息,消费者2每秒处理10条消息
Work模型的使用:
多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
通过设置 prefetch 来控制消费者预取的消息数量
发布订阅模式与之前案例的区别就是允许将同一 消息发送给多个消费者。实现方式是加入了exchange(交换机)。
常见exchange类型包括:
Fanout 广播
Direct 路由
Topic 话题
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的 queue
利用SpringAMQP演示FanoutExchange的使用
实现思路如下:
1.在consumer服务中,利用代码声明队列、交换机,并将两者绑定
2.在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
3.在oublisher中编写测试方法,向litcast.fanout发送消息
步骤1:在consumer服务声明Exchange、Queue、Binding
在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);
}
总结
接收 publisher 发送的消息
将消息按照规则路由到与之绑定的队列
不能缓存消息,路由失败,消息丢失
FanoutExchange 的会将消息路由到每个绑定的队列
在 spring-amqp 框架中,声明队列、交换机和绑定关系有两种方法:一种是直接使用 RabbitMQ 的 API 来进行声明,另一种是使用 Spring Boot 的 AMQP 自动配置功能来创建 Bean 对象实现声明。这些声明的 Bean 包含以下信息:
通过这些 Bean 对象,无需手动进行声明,Spring Boot 会自动将这些 Bean 注册到 RabbitMQ 中,并开始消息的传递和处理。
在配置声明队列、交换机和绑定关系的 Bean 时,需要注意队列和交换机名称的唯一性,否则可能会导致冲突和误操作。同时,建议按照业务类型对队列和交换机进行命名,以便在项目庞大之后进行统一管理和协调。
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue, 因此称为路由模式(routes)。
利用 SpringAMQP 演示 DirectExchange 的使用
实现思路如下:
步骤1:在 consumer 服务声明 Exchange、Queue
@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 与DirectExchange 类似,区别在于routing Key必须是多个单词的列表,并且以 . 分割。
Queue与Exchange指定BindingKey时可以使用通配符:
# 代指0个或多个单词
* 代指一个单词
利用 SpringAMQP 演示TopicExchange的使用
实现思路如下:
步骤1:在consumer服务声明Exchange、Queue
@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交换机的差异?
测试发送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是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch 结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。
elasticsearch 是elastic stack的核心,负责存储、搜索、分析数据。
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接口,可被任何语言调用
一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
是以elasticsearch为核心的技术栈,包括Beats、Logstash、kibana、elasticsearch
什么是Lucene ?
是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
elasticsearch采用倒排索引:
·文档(document):每条数据就是一个文档
·词条(term):文档按照语义分成的词语
·每一条数据就是一个文档
·对文档中的内容分词,得到的词语就是词条
基于文档创建索引。查询词条时必须先找到文档,而后判断是否包含词条
·对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。
索引
索引(index): 相同类型的文档的集合
映射(mapping): 索引中文档的字段约束信息,类似表的结构约束
架构
Mysql: 擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch: 擅长海量数据的搜索、分析、计算
文档:一条数据就是一个文档,es中是json格式
字段:Json文档中的字段
索引:同类型文档的集合
映射:索引中文档的约束,比如字段名称、类型
elasticsearch与数据库的关系:
数据库负责事务类型操作
elasticsearch: 负责海量数据的搜索、分析、计算
** 问题: 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"
}
创建倒排索引时对文档分词
用户搜索时,对输入的内容分词
ik_smart: 智能切分,粗粒度
ik_max_word:最细切分,细粒度
利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
在词典中添加拓展词条或者停用词条
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
创建、查询、删除、修改索引库
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})
RestClient操作es索引库_编程彦祖的博客-CSDN博客
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/quide/en/elasticsearch/client/index.html
利用 JavaRestClient 实现创建、删除索引库,判断索引库是否存在
根据酒店数据创建索引库,索引库名为hotel,mapping属性根据数据库结构定义。
基本步骤如下:
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
dependency>
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.12.1elasticsearch.version>
properties>
RestHighLevelclient client = new RestHighLevelclient(
Restclient.builder(HttpHost.create("http://192,168,147.146:9200"))
);
@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);
}
@Test
void testDeleteHotelIndex() throws IOException{
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发起请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
@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 不存在");
}
索引库操作的基本步骤:
·初始化RestHighLevelClient
·创建XxxIndexRequest XXX是CREATE、Get、Delete
·准备DSL(CREATE时需要)
·发送请求。调用RestHighLevelClient#indices().Xxx)方法
Xxx是create、exists、delete
// 新增文档
@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);
}
文档操作的基本步骤:
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), 返回结果时按照分值降序排列。
使用function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。
复合查询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":{
"查询类型":{
"查询条件": "条件值"
}
}
}
match 根据一个字段查询
multi_match 根据多个字段查询,参与查询字段越多,查询性能越差
term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
range查询:根据数值范国查询,可以是数值、日期的范国
TF-IDF 在elasticsearch5.0之前,会随着词频增加而越来越大
8M25 在elasticsearch5.0之后,会题着词频增加而增大,但增长曲线会趋于水平
过滤条件:哪些文档要加分
算分函数:如问计算function score
加权方式:function score与query score如何运算
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 的数据:
针对深度分页,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"
}
}
}
}
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);
}
查询的基本步骤是:
QueryBuilders 来构建查询条件
传入Request.source() 的 query() 方法
全文检素的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结果,逐层解析
先实现其中的关键字搜索功能,实现步骤如下:
//查询所有
@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);
}
我们要根据这个坐标,将酒店结果按照到这个点的距离升序排序。
实现思路如下:
//周边查询(地理查询)
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);
聚合(aggregations) 可以实现对文档数据的统计、分析、运算。聚合常见的有三类:
聚合是对文档数据的统计、分析、计算
Bucket:对文档数据分组,并统计每组数量
Metric:对文档数据做计算,例如avg
Pipeline:基于具它聚合结果再做聚合
keyword
数值
日期
布尔聚合的种类
默认情况下,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 //希望获取的聚合结果数量
}
}
}
}
总结
例如,我们要求获取每个品牌的用户评分的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"
}
}
}
}
}
}
@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输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
拼音分词器适合在创建倒排素引的时候使用,但不能在搜索的时候使用。
因此字段在创建倒排索引时应该用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": "掉入狮子笼咋办"
}
}
}
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 )的业务可能在两个不同的微服务上,数据同步该如何实现呢?
方案一:同步调用
方案二:异步通知
方案三:监听binlog
方式一:同步调用
·优点:实现简单,粗暴
·缺点:业务精合度高
方式二:异步通知
·优点:低偶合,实现提度一般
·缺点:依赖mq的可靠性
方式三:监听binlog
·优点:完全解除服务间属合
·缺点:开启binlog增加数据库负担、实现复杂度高
利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增、别、改时,要求对
elasticsearch中致据也要完成相同操作。
单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。
海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
单点故障问题:将分片数据在不同节点备份(replica)
ES集群的节点角色
elasticsearch中集群节点有不同的职责划分:
ES集群的分布式查询
elasticsearch中的每个节点角色都有自己不同的职责,因此建议集群部署时,每个节点都有独立的角色。
ES集群的脑裂
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。
为了避免脑裂,需要要求选票超过 (eligible节点数量+1)/2 才能当选为主,因此 eligible 节点数量最好是奇数。对应配置项是 discovery.zen.minimum_master_nodes 在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题
集群的 master 节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到具它节点,确保数据安全,这个叫做故障转移。
故障转移:
ES集群的分布式存储
当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到哪个分片呢?
elasticsearch会通过hash算法来计算文档应该存储到哪个分片:
说明:
_routing默认是文档的id
算法与分片数量有关,因此索引库一旦创建。分片数量不能修改!
elasticsearch的查询分成两个阶段:
scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
gather phase:聚集阶段,coordinating node汇总data node的搜索结集,并处理为最终结果集返回给用户
分布式新增如何确定分片?
coordinating node 根锯id做hash运算,得到结果对shard数量取余,余数就是对应的分片
分布式查询:
分散阶段:coordinating node将查询请求分发给不同分片
收集阶段:将查询结果汇总到coordinating node, 整理并返回给用户