入门
分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
分布式架构的优缺点:
优点:
-
降低服务耦合
-
有利于服务升级和拓展
缺点:
-
服务调用关系错综复杂
分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考:
-
服务拆分的粒度如何界定?
-
服务之间如何调用?
-
服务的调用关系如何管理?
人们需要制定一套行之有效的标准来约束分布式架构。
RestTemplate
如何让Controller像浏览器一样发送请求
1. 注册RestTemplate
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
2. 利用RestTemplate发送请求
String url = "http://localhost:8081/user/1";
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
Eureka注册中心
-
order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口?
-
有多个user-service实例地址,order-service调用时该如何选择?
-
order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
Eureka就可以解决这些问题
每一个服务启动时都会注册给Eureka
这样Eureka就能去管理这些服务,记录服务的功能 健康状态等
搭建注册中心
1. 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
2. 加配置
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:10086/eureka
registerWithEureka=false:
fetchRegistry=false:
3. 启动服务
服务注册
1. 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. 配置yml
server:
port: 8081
spring:
application:
name: user-server
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:10086/eureka
registerWithEureka=false:
fetchRegistry=false:
负载均衡
使用copy Configuration 对服务进行复制
为了防止端口占用 需要在VM加一个参数
-Dserver.port = 8082
实现负载均衡需要
1. 设置@LoadBalanced
在RestTemplante上
2. RestTemplate的请求地址进行修改
String url = "http://user-server/user/"+order.getUserId();
中间的位置填入注册服务的名称
Ribbon负载均衡
LoadBalancerIntercepor
拦截了用户的HttpRequest请求,然后做了几件事:
-
request.getURI()
:获取请求uri,本例中就是 http://user-service/user/8 -
originalUri.getHost()
:获取uri路径的主机名,其实就是服务id,user-service
-
this.loadBalancer.execute()
:处理服务id,和用户请求。
这里的this.loadBalancer
是LoadBalancerClient
类型,我们继续跟入。
LoadBalancerClient
-
getLoadBalancer(serviceId):根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。
-
getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个。本例中,可以看到获取了8082端口的服务
负载均衡策略IRule
总结
nacos
nacos注册中心
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
使用步骤
1. 引入依赖
版本管理依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
nacos依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2. 添加配置项
spring:
cloud:
nacos:
server-addr: localhost:8848
其他的都不用动,包括联合查询的url
nacos服务分级存储模型
一个**服务**可以有多个**实例**,例如我们的user-service,可以有:
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
假如这些实例分布于全国各地的不同机房,例如:
- 127.0.0.1:8081,在上海机房
- 127.0.0.1:8082,在上海机房
- 127.0.0.1:8083,在杭州机房
Nacos就将同一机房内的实例 划分为一个**集群*
简单来说一个服务存在多个实例,几个实例组成一个集群
cloud:
nacos:
server-addr: localhost:8848
discovery:
clusterName: HZ
将上述实例分配给HZ这个集群
nacos负载均衡策略
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
权重管理
nacos环境隔离
Nacos提供了namespace来实现环境隔离功能。
-
nacos中可以有多个namespace
-
namespace下可以有group、service等
-
不同namespace之间相互隔离,例如不同namespace的服务互相不可见
使用方法
1. 在nacos服务器创建命名空间
2. 在yml配置项中添加命名空间的id
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID
nacos细节
-
Nacos与eureka的共同点
-
都支持服务注册和服务拉取
-
都支持服务提供者心跳方式做健康检测
-
-
Nacos与Eureka的区别
-
Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
-
临时实例心跳不正常会被剔除,非临时实例则不会被剔除
-
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
-
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
-
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例
发现服务标红 表示服务挂掉了,但是因为是非临时实例 所以他不会被剔除
nacos配置
可以用nacos的配置中心 集中管理微服务的application.yml配置
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
在这里面进行配置更新
不要什么配置文件都往里写,主要写哪些经常变更的配置问及那
需要提前知道nacos地址,所以要在读取application.yml之前得到nacos地址
springboot提供了一个bootstrap.yml可以达到这个目的
使用步骤
1. 需要先引入依赖
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2. 添加bootstrap.yaml
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
注意一定看日志 (我在这卡了半天)
发现自己报错的时候看看bootstrap日志的路径对不对
nacos集群
1. 分别启动三个nacos
每个nacos需要单独重新配置端口号
每个nacos也需要对数据库进行统一设置
2. 初始化nacos数据库
Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。
这里我们以单点的数据库为例来讲解。
首先新建一个数据库,命名为nacos
3. 利用nginx做负载均衡
4. 配置upstream
三个地址均为nacos地址
5. 修改java代码的nacos端口号改为nginx端口
Feign
基础使用
优雅的实现http请求的发送
1. 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2. 开启Feign自动装配
3. 编写客户端声明
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
Feign使用优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
1)引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:
<!--httpClient的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
2)配置连接池
在order-service的application.yml中添加配置:
feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数
最佳实践
继承
可以把Feign客户端和Controller层都继承一个UserAPI的接口
抽取
Gateway网关
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Gateway网关是我们服务的守门神,所有微服务的统一入口。
网关的核心功能特性:
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
如何使用
1. 先写一个模块
因为路由也是一个微服务需要使用springboot进行启动
2. 写配置文件
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/开头就符合要求
告诉路由注册中心的地址
和一些路由相关的配置
然后可以做一下测试,发现网关配置是好用的
如果微服务请求微服务功能发生503错误
这个时候检查一下服务器是不是可用的
然后添加一个依赖
<!-- 需要添加负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2020年之后gateway删除了ribbon负载均衡器
所以需要我们在添加一个负载均衡器
断言工厂
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
名称 | 说明 | 示例 |
---|---|---|
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 | 权重处理 |
过滤器
拦截器
对用户的请求和微服务返回的数据做处理
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! # 添加请求头
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Itcast is freaking awesome!
全局过滤器
上面的拦截器
使用步骤
1. 先写接口
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
2. 写实现类
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}
过滤器执行顺序
跨域问题
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
Doker
Docker为了解决依赖的兼容问题的,采用了两个手段:
-
将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包
-
将每个应用放到一个隔离容器去运行,避免互相干扰
简单来说Docker的运行只需要内核就可以了 所以可以运行在不同的操作系统上面
镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器进程做隔离,对外不可见。
一切应用最终都是代码组成,都是硬盘中的一个个的字节形成的文件。只有运行时,才会加载到内存,形成进程。
而镜像,就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。
容器呢,就是将这些文件中编写的程序、函数加载到内存中允许,形成进程,只不过要隔离起来。因此一个镜像可以启动多次,形成多个容器进程。
DockerHub
开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像,例如Redis、MySQL镜像放到网络上,共享使用,就像GitHub的代码共享一样。
Docker架构
我们要使用Docker来操作镜像、容器,就必须要安装Docker。
Docker是一个CS架构的程序,由两部分组成:
-
服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
-
客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
安装docker
首先需要大家虚拟机联网,安装yum工具
yum install -y yum-utils \ device-mapper-persistent-data \ lvm2 --skip-broken
然后更新本地镜像源:
# 设置docker镜像源 yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo yum makecache fast
然后输入命令:
yum install -y docker-ce
docker-ce为社区免费版本。稍等片刻,docker即可安装成功。
配置镜像加速器
针对Docker客户端版本大于 1.10.0 的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://7ipb1ql6.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
docker命令
#info|version
docker info #显示docker的系统信息,包括镜像和容器的数量
docker version #显示docker的版本信息。
#帮助命令
docker 命令 --help #帮助命令
#镜像命令
docker images #查看所有本地主机上的镜像 可以使用docker image ls代替
docker search #搜索镜像
docker pull #下载镜像 docker image pull
docker rmi #删除镜像 docker image rm
#容器命令
docker run 镜像id #新建容器并启动
docker ps 列出所有运行的容器 docker container list
docker rm 容器id #删除指定容器
#删除所有容器
docker rm -f $(docker ps -aq) #删除所有的容器
docker ps -a -q|xargs docker rm #删除所有的容器
#启动和停止容器
docker start 容器id #启动容器
docker restart 容器id #重启容器
docker stop 容器id #停止当前正在运行的容器
docker kill 容器id #强制停止当前容器
#退出容器
exit #容器直接退出
ctrl +P +Q #容器不停止退出 ---注意:这个很有用的操作
#其他常用命令
docker run -d 镜像名 #后台启动命令
docker logs #查看日志
docker top 容器id #查看容器中进程信息ps
docker inspect 容器id #查看镜像的元数据
docker exec #进入当前容器后开启一个新的终端,可以在里面操作。(常用)
docker attach # 进入容器正在执行的终端
docker cp 容器id:容器内路径 主机目的路径 #从容器内拷贝到主机上
容器三种状态
运行镜像
创建并运行nginx容器的命令:
docker run --name containerName -p 80:80 -d nginx
命令解读:
-
docker run :创建并运行一个容器
-
--name : 给容器起一个名字,比如叫做mn
-
-p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口
-
-d:后台运行容器
-
nginx:镜像名称,例如nginx
这里的-p
参数,是将容器端口映射到宿主机端口。
默认情况下,容器是隔离环境,我们直接访问宿主机的80端口,肯定访问不到容器中的nginx。
现在,将容器的80与宿主机的80关联起来,当我们访问宿主机的80端口时,就会被映射到容器的80,这样就能访问到nginx了:
数据卷
docker修改配置文件存在一些问题
1. 修改比较麻烦
2. 无法同时配置多个容器
这时候可以使用数据卷来解决这个问题
数据卷就像一个分身一样,对数据卷的修改可以反馈到容器里面
数据卷的作用:
-
将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全
数据卷操作:
-
docker volume create:创建数据卷
-
docker volume ls:查看所有数据卷
-
docker volume inspect:查看数据卷详细信息,包括关联的宿主机目录位置
-
docker volume rm:删除指定数据卷
-
docker volume prune:删除所有未使用的数据卷
DockerFile
构建自定义的镜像时,并不需要一个个文件去拷贝,打包。
我们只需要告诉Docker,我们的镜像的组成,需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么,将来Docker会帮助我们构建镜像。
而描述上述信息的文件就是Dockerfile文件。
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。
# 指定基础镜像 FROM ubuntu:16.04 # 配置环境变量,JDK的安装目录 ENV JAVA_DIR=/usr/local # 拷贝jdk和java项目的包 COPY ./jdk8.tar.gz $JAVA_DIR/ COPY ./docker-demo.jar /tmp/app.jar # 安装JDK RUN cd $JAVA_DIR \ && tar -xf ./jdk8.tar.gz \ && mv ./jdk1.8.0_144 ./java8 # 配置环境变量 ENV JAVA_HOME=$JAVA_DIR/java8 ENV PATH=$PATH:$JAVA_HOME/bin # 暴露端口 EXPOSE 8090 # 入口,java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar
MQ
mq在微服务当中可以解决的问题
同步和异步通讯
微服务间通讯有同步和异步两种方式:
同步通讯:就像打电话,需要实时响应。
异步通讯:就像发邮件,不需要马上回复。
见另一篇文章
ES
Elasticsearch
mysql与elasticsearch
我们统一的把mysql与elasticsearch的概念做一下对比:
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 |
是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长支出:
-
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
-
Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
-
对安全性要求较高的写操作,使用mysql实现
-
对查询性能要求较高的搜索需求,使用elasticsearch实现
-
两者再基于某种方式,实现数据的同步,保证一致性
mapping是对索引库中文档的约束,常见的mapping属性包括:
-
type:字段数据类型,常见的简单类型有:
-
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
-
数值:long、integer、short、byte、double、float、
-
布尔:boolean
-
日期:date
-
对象:object
-
-
index:是否创建索引,默认为true
-
analyzer:使用哪种分词器
-
properties:该字段的子字段
创建索引库
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
PUT /heima
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "falsae"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
},
// ... 略
}
}
}
ES使用RESTful风格
基本语法:
-
请求方式:GET/PUT/DELETE
-
请求路径:/索引库名
-
请求参数:无
格式:
GET /索引库名
创建文档库
语法:
POST /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" }, // ... }
示例:
POST /heima/_doc/1 { "info": "黑马程序员Java讲师", "email": "zy@itcast.cn", "name": { "firstName": "云", "lastName": "赵" } }
修改文档
修改有两种方式:
-
全量修改:直接覆盖原来的文档 (相当于替换)
-
增量修改:修改文档中的部分字段
全量修改
语法:
PUT /{索引库名}/_doc/文档id { "字段1": "值1", "字段2": "值2", // ... 略 }
示例:
PUT /heima/_doc/1 { "info": "黑马程序员高级Java讲师", "email": "zy@itcast.cn", "name": { "firstName": "云", "lastName": "赵" } }
增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id { "doc": { "字段名": "新的值", } }
示例:
POST /heima/_update/1 { "doc": { "email": "ZhaoYun@itcast.cn" } }
索引操作总结
文档操作有哪些?
-
创建文档:POST /{索引库名}/_doc/文档id { json文档 }
-
查询文档:GET /{索引库名}/_doc/文档id
-
删除文档:DELETE /{索引库名}/_doc/文档id
-
修改文档:
-
全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
-
增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}
-
ES客户端
操纵索引
package cn.itcast.hotel;
import cn.itcast.hotel.constants.HotelConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
/**
* @projectName: hotel-demo
* @package: cn.itcast.hotel
* @className: HotelIndexTest
* @author: Eric
* @description: TODO
* @date: 8/1/2023 9:52 AM
* @version: 1.0
*/
@Slf4j
public class HotelIndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.232.146:9200")));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
@Test
void createHotelIndex() throws IOException {
//创建Request对象
CreateIndexRequest hotel = new CreateIndexRequest("hotel");
//准备请求的参数 DSL语句
hotel.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
//发送请求
client.indices().create(hotel, RequestOptions.DEFAULT);
}
@Test
void deleteHotelIndex() throws IOException {
//创建Request对象
DeleteIndexRequest hotel = new DeleteIndexRequest("hotel");
//发送请求
client.indices().delete(hotel, RequestOptions.DEFAULT);
}
@Test
void testExistHotelIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("hotel");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
}
使用客户端对索引进行操作
步骤可以抽取为以下几步
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:
-
初始化RestHighLevelClient
-
创建XxxIndexRequest。XXX是Create、Get、Delete
-
准备DSL( Create时需要,其它是无参)
-
发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
操纵文档
//Document
@Test
void testDocument() throws IOException {
Hotel hotel = hotelService.getById(38609L);
HotelDoc hotelDoc = new HotelDoc(hotel);
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
client.index(request,RequestOptions.DEFAULT);
}
@Test
void testGetDocumentById() throws IOException{
GetRequest request = new GetRequest("hotel","38609");
GetResponse response = client.get(request,RequestOptions.DEFAULT);
String sourceAsString = response.getSourceAsString();
HotelDoc jsonObject = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(jsonObject);
}
@Test
void testUpdateDocument() throws IOException {
UpdateRequest request = new UpdateRequest("hotel","38609");
//更新使用doc
request.doc("price","952");
UpdateResponse update = client.update(request,RequestOptions.DEFAULT);
}
@Test
void testDeleteDocument() throws IOException {
DeleteRequest request = new DeleteRequest("hotel","38609");
DeleteResponse delete = client.delete(request,RequestOptions.DEFAULT);
}
可以看到与创建索引库类似,同样是三步走:
-
1)创建Request对象
-
2)准备请求参数,也就是DSL中的JSON文档
-
3)发送请求
变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。
批量导入数据
在实际生产过程中我们不可能一条一条的添加数据所以这里使用ESclient的批量导入数据的功能从数据库里面导入数据
DSL查询
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
-
简单查询
查询所有
查询的语法基本一致:
GET /indexName/_search { "query": { "match_all": { } } }
一般就返回10条信息
全文检索查询
根据用户的分词对文档进行搜索
一个根据多字段来查询一个单字段查询
match查询语法如下:
GET /indexName/_search { "query": { "match": { "FIELD": "TEXT" } } }
mulit_match语法如下:
GET /indexName/_search { "query": { "multi_match": { "query": "TEXT", "fields": ["FIELD1", " FIELD12"] } } }
精准查询
查询的值必须和字段的值保持一致
-
term:根据词条精确值查询
-
range:根据值的范围查询
语法说明:
// term查询 GET /indexName/_search { "query": { "term": { "FIELD": { "value": "VALUE" } } } }
基本语法:
// range查询 GET /indexName/_search { "query": { "range": { "FIELD": { "gte": 10, // 这里的gte代表大于等于,gt则代表大于 "lte": 20 // lte代表小于等于,lt则代表小于 } } } }
范围查询
// geo_distance 查询
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"FIELD": "31.21,121.5" // 圆心
}
}
}
复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
-
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
-
bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
算分函数查询
后来升级为
但是和上面的那个差不多
布尔查询
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
-
must:必须匹配每个子查询,类似“与”
-
should:选择性匹配子查询,类似“或”
-
must_not:必须不匹配,不参与算分,类似“非”
-
filter:必须匹配,不参与算分
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "华美达" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }}
]
}
}
}
filter 和 must_not的区别
filter 必须参与匹配,但是不参与计算分数
must_not 必须不进行匹配,因为都没有进行匹配,所以更不需要计算分数了
搜索结果处理
排序
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
keyword、数值、日期类型排序的语法基本一致。
语法:
GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "FIELD": "desc" // 排序字段、排序方式ASC、DESC } ] }
排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
keyword、数值、日期类型排序的语法基本一致。
语法:
GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "FIELD": "desc" // 排序字段、排序方式ASC、DESC } ] }