微服务不等于springcloud,微服务是一种经过良好架构设计的分布架构方案,微服务架构特征:
微服务做的第一件事就是拆分,因为传统单体架构,所有的业务功能全部写在一起,随着业务越来越复杂,代码也耦合的越来越多,将来升级维护就会变得很困难,所以大型的互联网服务都会进行拆分。微服务再做拆分的时候,会根据业务功能模块把一个单体的项目拆分成许多的独立的项目,每个项目完成一部分业务功能,将来独立开发或部署,我们把这一个独立的项目称为服务,一个大型的互联网项目往往会包含数百上千的服务最终形成一个服务集群,而一个业务往往就需要多个服务共同完成。比如说一个请求来了,他可能先去调用服务A,而服务A可能又调用了服务B,而后又去调用了服务C。当业务越来越多也来越复杂的时候,这些服务的调用关系就会越来越复杂,这么复杂的调用关系要让人去记录和维护是不可能的。所以再微服务里会又一个组件叫注册中心,它可以去记录微服务里每个服务的ip、端口,以及他们的功能。当有一个服务需要调用另一个服务时它不需要自己去记录对方的ip,只需要去找注册中心就可以,从注册中心去拉取对应的服务信息。同时随着服务越来越多每个服务都有自己的配置文件,将来要更改配置我们要逐一去修改这样就太麻烦了,所以我们还会有一个配置中心。它可以去统一的管理整个服务群里成千上百个配置,如果以后又配置需要变更只需要去 找配置中心,他会通知相关的微服务实现配置的热更新当我们的微服务运行起来之后,用户就可以来访问我们了,这时候就还需要一个网关服务。那你这里有这么多微服务,用户怎么知道你要访问哪一个呢?而且也不是随便什么人都可以访问我们的服务,这就像是我们的小区,小区里往往有一个看门的大爷,不能什么人来了都可以进入。所以服务网关一部分是对用户身份进行校验,另一方面可以把用户的请求路由到具体的服务,当然再路由过程中也可以去做一些负载均衡。而这时候服务集群根据你的请求去处理业务,该访问数据库就访问数据库并把查询到的数据返回给用户。数据库肯定也是集群,不过集群在庞大也不会有用户数量多,所以数据库将来肯定无法抗住高并发,因此我们还会加入分布式缓存, 但是简单查询可以走缓存,一些海量数据的复杂的搜索统计和分析缓存也做不了,这时候就还需要用到分布式搜索功能。数据库将来的功能就只需要做一些写操作和一些事务类型的对数据安全较高的一些数据存储。最后再微服务里面还需要一种异步通讯的消息队列组件。为什么呢?其实对于分布式的服务或再微服务里面,他的业务往往会跨越多个服务,所以总时长就会等于每个服务执行时长之和,性能就会下降,而异步通讯的意思就是请求来了我调用服务A,而服务A不是去调用服务B和C而是通知他们,B和C去执行之后A就直接结束了,那么通信链路就变短了执行时长也就短了 。所以异步通讯可以大大提高我们的并发,再一些秒杀等高并发场景下就可以去利用了。当然我们如此庞大复杂的一个服务在运行的过程中如果出现什么问题也不好排查。所以在微服务运行中还会引入两个组件来解决这种异常定位。第一个是分布式日志服务,它可以去统计整个集群中成千上百个服务他们的运行日志,统一的去做一个存储,统计,分析。将来出现问题就比较好定位了。第二个叫做系统监控链路追踪,他可以去实时监控我们整个集群中每个服务节点的运行状态,Cpu的负载、内存的占用等等情况,一旦出现问题直接可以定位到具体的某一个方法和栈信息。那这么大的服务怎么部署呢?未来我们会做一些自动化的部署,持续集成:利用Jenkins这样的工具,它可以帮助我们对这些微服务项目进行自动化的编译,而基于Docker来进行一些打包形成镜像,再基于kubernetes或rancher这样的技术实现自动化的部署,这一套我们就称为叫持续集成。以上全部才叫做微服务技术栈
单体架构:将业务的所有功能集中在一个项目中开发,达成一个包部署。
优点:
缺点:
分布式架构:
分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。
分布式架构要考虑的问题:
如果服务A调用服务B,服务B调用了服务C,那么服务B是什么角色?
一个服务既可以是提供者也可以是消费者
在Eureka架构中,微服务角色有两类:
具体问题:
搭建EurekaServer服务步骤如下:
创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖
<dependency>
<groupId>org,springframework.cloudgroupId>
<artifacrId>spring-cloud-starter-netflix-eureka-serverartifacrId>
dependency>
编写启动类,添加@EnableEurekaServer注解
添加application.yml文件,编写下面的配置:
server: #端口
port: 8084
spring:
application: #服务名称
name: eurekaServer
eureka:
client:
service-url: #eureka的地址信息
defaultZone: http://127.0.0.1:8084/eureka
服务注册步骤
在要注册的服务下引入spring-cloud-starter-netflix-eureka-client
<dependency>
<groupId>org,springframework.cloudgroupId>
<artifacrId>spring-cloud-starter-netflix-eureka-clientartifacrId>
dependency>
在application.yml文件添加
server: #端口
port: 8081
spring:
application: #服务名称
name: UserService
eureka:
client:
service-url: #eureka的地址信息
defaultZone: http://127.0.0.1:8084/eureka
代码请求地址中将ip和端口号改为eureka的服务名,并给restTemplate添加@loadBanlanced开启负载均衡
ribbon通过获取主机名来对eureka进行获取服务地址,然后通过轮询或者随机来完成负载均衡
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。他是Ribbon默认的负载均很规则 |
AvailablitityFilteringRule | 对以下两种服务器进行忽略:(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”转台。短路状态将持续30秒,如果再次连接失败,短路的持续 时间就会几何级数的增加。(2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilterRule规则的客户端也会将其忽略。并发连接数的上线可以由客户端的.ActiveConnectionsLimit属性进行配置 |
weightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器 |
ZoneAvailableRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个几架等。而后再对Zone的多个服务做轮询 |
RandomRule | 随机选择一个可用的服务器 |
RetryRule | 重试机制的选择逻辑 |
通过定义IRule实现可以修改负载均衡规则,有两种方式:
1、代码方式在配置类中定义一个新的IRule:
@bean
public IRule randomRule(){
return new RandomRule();
}
1、配置文件方式:yml文件中添加新的配置也可以修改规则:
userservice:
ribbon:
NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RandomRule #负载均衡规则
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients:
-userservice #指定对userservice这个服务饥饿加载
-xxservice
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
在cloud-demo父工程中添加spring-cloud-alibaba的管理依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artfactId>spring-cloud-alibaba-dependenciesartfactId>
<version>2.2.5RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
注释掉order-service和user-service中原有的eureka依赖
添加nacos的客户端依赖:
<dependecy>
<groupId>com.alibaba.cloudgroupId>
<artfactId>spring-cloud-starter-alibaba-nacos-discoveryartfactId>
dependecy>
修改user-service和order-service中的application.yml文件,注释eureka地址,添加nacos地址:
spring:
cloud:
nacos:
server-addr: localhost:8848 #nacos 服务端地址
启动并测试
一个服务的多个实例分开放
修改application.yml文件,添加以下内容
spring:
cloud:
nacos:
server-addr: #nacos 服务端地址
discovery:
cluster-name: HZ #配置集群名称,也就是机房位置,例如:HZ,杭州
1、Nacos服务分级存储模型:
2、如何设置实例的集群属性
修改order-service中的application.yml,设置集群为HZ
spring:
cloud:
nacos:
server-addr: localhost:8848 #nacos服务器地址
discovery:
cluster-name: HZ #配置集群名称,也就是机房位置
然后再order-service中设置负载均衡的Rule为NacosRule,这个规则会优先寻找与自己同集群的服务:
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡策略
注意将user-service的权重都设置为1
实际部署中会出现这样的场景:
nacos提供了权重配置来控制访问频率,权重越大则访问频率越高
总结:
nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
步骤:
在nacos控制台中的命名空间添加命名空间
在要隔离的yml文件中添加namespace
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SH
namespace: 命名空间id
总结:
服务注册到nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置
spring:
cloud:
nacos:
discovery:
ephemeral: false #设置是否为临时实例
在nacos中添加配置文件
在微服务中引入nacos的config依赖
<!--导入nacos配置依赖-->
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
在微服务中添加bootstrap.yml(引导文件,优先级高于application.yml),配置nacos地址、当前环境、服务名称、文件后缀名。这些决定了程序启动时去nacos读取那个文件
spring:
application:
name: userservice #服务名称
profiles:
active: dev #开发环境
cloud:
nacos:
server-addr: localhost:8848
config:
file-extension: yaml #文件后缀名
@refreshScope注解 热更新自动刷新注解 在需要自动注入的地方使用
nacos配置更改后,微服务可以实现热更新,方式:
注意事项:
微服务启动时会从nacos读取多个配置文件:
无论profile如何变化,[spring.application.name.yaml].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件
多种配置的优先级:
nacos生产环境下一定要部署为集群状态
配置nacos
配置三份nacos节点
进入nacos的conf目录,修改配制文件cluster.conf.example,重命名为cluster.conf
添加内容:
127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847
将nacos文件夹复制三分,分别命名为:nacos1、nacos2、nacos3
然后分别修改三个文件夹中的application.properties
server.port=8845
server.port=8846
server.port=8847
分别启动三个nacos节点
nginx反向代理
修改conf/nginx.conf文件,配置如下:
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
将nacos中的连接数据库打开
spring.datasource.platform=mysql
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=rose
启动
先来看我们以前利用RestTemplate发起远程调用的代码:
String url =" http://userservice/user/"+order.getuserId();
User user = restTemplate.getForObject(url,User.class)
存在下面的问题:
Feign是一个声明式的http客户端,官方地址:http://github.com/OpenFeign/feign 其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题
使用步骤:
引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
在order-service的启动类添加注解开启Feign的功能
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args);
}
}
编写Feign客户端
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findUserById(@PathVariable("id") Long id);
}
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NON、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign.Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
一般我们需要配置的就是日志级别
自定义Feign的配置有两种方式:
方式一:配置文件方式
feign:
client:
config:
default: #这里用default就是全局配置
loggerLevel: Full #日志级别
feign:
client:
config:
userservice: #这里用userservice就是只有某个微服务配置
loggerLevel: Full #日志级别
方式二:Java代码方式,需要先声明一个bean
public class FeignClientConfiguration{
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;
}
}
而后如果是全局配置,则把它放到@EnableFeignClient这个注解中
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
如果是局部配置,则把他放到@FeignClient这个注解中
@feignClient(value = "userservice",configuration=FeignClientConfiguration.class)
Feign底层的客户端实现:
因此优化Feign的性能主要包括:
步骤:
引入依赖:
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
因为消费者的feignClient和提供者的controller的方法名和参数以及请求方式都相似,所以可以把他们抽取出来定义一个父类 让他们来继承 ,这样 他们就都不用写了。但这样他们的耦合性就高了
将FeignCilent抽取为独立模块,并且把接口端的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
因为服务的消费者用的userCilent有很多消费者在用,我们把userclient抽取出来(包括实体类,默认配置等),让消费者来医用依赖,然后远程调用服务的提供者。
当定义FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种解决方案:
方式一:指定FeignClient所在包
@EnableFeignClients(basePackages="cn.itcast.feign.clients")
方式二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
为什么需要网关?
微服务先注册到nacos,微服务与微服务之间通过feign来调用,用户需要操作时直接发请求到微服务,但这时候就存在一个问题,我们的微服务就在那里让所有人访问这是不安全的,因为有些业务属于公司内部的不需要面向大众,所以就需要对用户的身份进行验证。所以gateway网关就是专门做这个的。验证完后是不是需要放行到微服务去那?所以网关还需要将请求转发到微服务,也就是服务路由,并且网关还做到了负载均衡,因为一个查询请求对应的微服务可能偶多个实例。网关还可以请求限流
zuul是基于servlet的实现,属于阻塞式编程。而SpringcloudGateway是基于Spring5中提供的webFlux,属于响应式编程的实现,具备更好的性能。
步骤:
创建新的mouble,引入SpringCloudGateWay的依赖和nacos的服务发现依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
编写路由配置及nacos地址
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #网关路由配置
- id: user-service #路由id,自定义,只要唯一即可
uri: lb://userservice #路由的目标地址,lb就是负载均衡 后面跟着服务名
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** #这是按照路径匹配 也就是/user/开头就符合要求
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
server:
port: 10010 #网关端口
总结:
创建项目,引入nacos服务发现和gateway依赖
配置application.yml,包括服务基本信息、nacos地址、路由
路由配置包括:
spring提供了11种基本的predicate工厂:
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点之后的请求 | -Afer=2023-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | -Afer=2023-01-20T17:42:47.789-07:00[America/Denver] |
Between | 是某两个之间点之间的请求 | -Between=2023-01-20T17:42:47.789-07:00[America/Denver],023-01-20T17:42:47.789-07:00[America/Denver] |
Cookid | 请求必须包含某些cookie | -Cookid=chocolata,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必须是指定范围 | -ReomteAdr=192.168.1.1/24 |
Weight | 权重处理 |
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
spring提供了31种不同的路由过滤器工厂:看例如:
名称 | 说明 |
---|---|
AddrequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除一个响应头 |
RequestRateLimiter | 限制请求的流量 |
给所有进入userservice的请求添加一个请求头:Truth=rosevvi fucking awesome!
实现方式:在gateway中修改application.yml文件,给userserivice的路由添加过滤器:
routes: #网关路由配置
- id: user-service #路由id,自定义,只要唯一即可
uri: lb://userservice #路由的目标地址,lb就是负载均衡 后面跟着服务名
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** #这是按照路径匹配 也就是/user/开头就符合要求
filters:
- AddRequestHeader=Truth,rosevvi is fucking rwesome!!!
如果要对所有的路由都生效,则可以将过滤器工厂的default下。格式如下:
routes: #网关路由配置
- id: user-service #路由id,自定义,只要唯一即可
uri: lb://userservice #路由的目标地址,lb就是负载均衡 后面跟着服务名
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** #这是按照路径匹配 也就是/user/开头就符合要求
# filters:
# - AddRequestHeader=Truth,rosevvi is fucking rwesome!!!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Truth,rosevvi is fucking rwesome!!!
全局过滤器的作用也是处理一切网关的请求和微服务响应,与fatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现
定义方式是实现FlobalFilter接口
//设置优先级 或者实现Ordered接口
@Order(-1)
@Component
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1、获取请求参数
ServerHttpRequest request = exchange.getRequest();
//2、获取参数中的Authorization参数
String auth = request.getQueryParams().getFirst("Authorization");
//3、判断参数值是否为admin
if ("admin".equals(auth)){
return chain.filter(exchange);
}
//4、拦截 设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//5、拦截请求
return exchange.getResponse().setComplete();
}
}
跨域:域名不一致就是跨域,主要包括
跨域问题:浏览器禁止请求的发起者与服务端跨域ajax请求,请求被浏览器拦截问题。
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
gateway:
globalcors: #全局的跨域处理
add-to-simple-url-handler-mapping: true #解决options请求被拦截
cors-configurations:
'[/**]': #拦截那些请求
allowedOrigins: #允许那些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: #允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" #允许在请求头中携带的头信息
allowCredentials: true #是否允许携带cookid
maxAge: 360000 #这次跨域检测的有效期
项目部署的问题
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
Docker如何解决依赖的兼容问题的?
Docker如何解决不同系统环境的问题?
镜像(Image):Docker将应用程序以及其所需要的依赖、环境、配置等文件打包在一起,称为镜像。
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,使其互相对外不可见。
DockerHub:DockerHub是一个Docker镜像的托管平台。这样的怕平台称为DockerRegistry。
Docker是一个CS架构的程序,由两部分组成:
#镜像命令
# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
# 查看防火墙状态
systemctl status firewalld
# 启动Docker
systemctl start docker
# 查看docker版本
docker -v
# 停止docker 服务
systemctl stop docker
# 重启docker 服务
systemctl restart docker
# 查看本地镜像
docker images
# 拉取镜像 没有版本 默认最新版
docker pull nginx:latest
# 推送镜像
docker push
# 删除一个本地镜像
docker rmi nginx:latest
# 查看帮助
docker save --help
# 利用dockers save将nginx镜像导出到磁盘,然后再通过load加载回来 docker save [options] image [image]...
docker save -o nginx.tar nginx:latest
# 利用load加载镜像
docker load -i nginx.tar
# 构建镜像 自定义镜像 -t指定dockerfile所在目录 空格点.
docker build -t
# 容器常用命令
# 运行 --name:给容器起名字 -p:将宿主机端口与容器端口映射,冒号左边是宿主机端口,右侧是容器端口 80:80 -d:后台运行容器 nginx:基于nginx镜像创建的
docker run --name containerName -p 80:80 -d nginx
# 暂停
docker pause
# 开始
docker unpause
# 停止
docker stop
# 开始
docker start
# 查看所有运行的容器及状态 -a查看所有状态的容器
docker ps -a
# 查看容器运行日志 docker logs [name] -f -f:持续输出日志
docker logs mn
# 进入容器执行命令 要执行的命令通常为bash
docker exec -it [容器名][要执行的命令]
# 删除指定容器 -f强制删除运行中的容器
docker rm -f
#数据卷命令
#数据卷操作命令的基本语法
docker volume [COMMAND]
#创建一个volume
docker volume create
#显示一个或多个volume的信息
docker volume inspect
#列出所有的volume
docker volume ls
#删除未使用的volume
docker volume prune
#删除一个或多个指定的volume
docker volume rm
#数据卷挂载
docker run --name mn -v html:/root/html -p 8080:80 nginx
DockerCE支持64位版本的Centos7,并且要求内核版本不低于3.10,Centos7满足最低内核要求,所以我们在Centos7上安装Docker
如果之间安装过旧版本的Docker,可以使用下面命令卸载:
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest=logrotate \
docker-logroate \
docker-selinux \
docker-engine-selinux \
docker-enfine \
docker -ce
首先要联网虚拟机,安装yum工具
yum install -y yum-utils \
device=mapper-persistent-data \
lvn2 --skip-broken\
更新本地镜像源:
#设置Docker镜像源
yum-config-manger \
--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应用需要用到各种端口,逐一去修改防护墙设置非常麻烦,建议关闭防火墙
# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
# 查看防火墙状态
systemctl status firewalld
# 启动Docker
systemctl start docker
# 查看docker版本
docker -v
# 停止docker 服务
systemctl stop docker
# 重启docker 服务
systemctl restart docker
docker官方镜像仓库网速较差,我们需要设置国内镜像
参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://u2fv9qvc.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
进入nginx容器,修改html文件内容,添加“船只教育观影你”
进入容器。进入我们刚创建的nginx的容器命令为
docker exec -it mn bash
命令解读:
数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录(/var/lib/docker/volumes),创建一个数据卷html那么他就会创建/var/lib/docker/volumes/html
数据卷操作命令:
#数据卷操作命令的基本语法
docker volume [COMMAND]
#创建一个volume
docker volume create [name]
#显示一个或多个volume的信息
docker volume inspect
#列出所有的volume
docker volume ls
#删除未使用的volume
docker volume prune
#删除一个或多个指定的volume
docker volume rm
#-v 前半部分是数据卷,后半部分是容器内目录
docker run --name mn -v html:/root/html -p 8080:80 nginx
案例:
需求说明:上个案例中,我们进入nginx容器内部,已经知道nginx的html目录所在位置/usr/share/nginx/html,我们需要把这个目录挂载到html这个数据卷上,方便操作其中的内容。提示:运行容器时使用-v参数挂载数据卷
步骤:
创建容器并挂载数据卷到容器内的HTML目录、
#创建名为html的数据卷
docker valume html
#创建容器并挂载数据卷到容器内的html目录
docker run --name mn -p 80:80 -v
html:/usr/share/nginx/html -d nginx
进入html数据卷所在位置,并修改HTML内容
#查看数据卷的位置
docker volume inspect html
#计入该目录
cd
#修改文件
vi
宿主机目录可以直接挂载到容器
实现思路如下:
在将课前资料中的mysql.tar文件上传到虚拟机,通过load命令加载为镜像
创建目录/tmp/myql/data,
创建目录/tmp/myql/conf,将课前资料提供的hmy.cnf文件上传到/tmp/myql/conf4.
去DockerHub查阅资料,创建并运行MySQL容器,要求:
docker run \
--name mysql \
-e MYSAL_ROOT_PASSWORD=123 \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/musql
-d \
mysql:5.7.25
总结:
镜像是一个分层结构,每一层称为一个layer:
DockerFile就是一个文本文件,其中包含一个个的指令,用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层layer
指令 | 说明 | 示例 |
---|---|---|
FROM | 指定基础镜像 | FROM centos:6 |
ENV | 设置环境变量,可在后面指令使用 | ENV key value |
COPY | 拷贝本地文件到镜像的指定目录 | COPY ./mysql-2.7.rpm /tmp |
RUN | 执行linux的shell命令,一般是安装过程的命令 | RUN yum install gcc |
EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 |
ENTRYPOINT | 镜像中应用的启动命令,容器运行时调用 | ENTRYPOINT java -jar xx.jar |
案例:
基于Ubuntu镜像构建一个新镜像,运行一个java项目
步骤1∶新建一个空文件夹docker-demo
步骤2:拷贝课前资料中的docker-demo.jar文件到docker-demo这个目录
步骤3:拷贝课前资料中的jdk8.tar.gz文件到docker-demo这个目录
步骤4:拷贝课前资料提供的Dockerfile到docker-demo这个目录
步骤5:进入docker-demo
步骤6:运行命令:
DokerFile示例
#指定基础镜像
FROM centos:6
# 配置环境变量,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_DIR/bin
#暴露端口
EXPOSE 8090
# 入口 Java项目的启动命令
ENTRYPOINT java -jar .tmp/app.jar
现在有人把安装jdk的步骤构建成了镜像,我们可以利用java:8-alpine镜像来构建自己的镜像
# 指定基础镜像
FROM java:8-alpine
COPY ./docker-demo.jar /tmp/app.jar
#暴露端口
EXPOSE 8090
# 入口 Java项目的启动命令
ENTRYPOINT java -jar .tmp/app.jar
version: "3.8"
services:
mysql: #容器名称
image: mysql:5.7.25 #镜像名称
environment:
MYSQL_ROOT_PASSWORD:123 #设置密码
volumes: #数据卷
- /tmp/mysql/data:/var/lib/mysql
- /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf
web:
build: . #构建 . 代表从当前目录构建镜像
ports:
- 8090 : 8090
同步调用:你个妹子打视频电话
异步调用:你和19个妹子发微信消息
同步调用的问题:微服务之间基于Feign的调用就属于同步方式,存在一些问题
同步调用的优点:
同步调用的问题:
异步调用:异步调用常见实现就是事件驱动模式
异步通讯优势:
异步通信的缺点:
什么是MQ
MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
RabbitMQ | ACtiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP、XMPP、SMTP、STOMP | OpenWire,STOMP,REST,XNPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
下载镜像:
docker pull rabbitmq:3-management
执行下面命令来运行MQ容器
docker run \
-e RABBITMQ_DEFAULT_USER=rosevvi \ #设置环境变量
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq \ #起名
--hostname mq1 \ #配置主机名
-p 15672:15672 \ #端口映射 管理平台的端口
-p 5672:5672 \ #消息通信的端口
-d \
rabbitmq:3-management
RabbitMQ的几个概念:
MQ的官方文档中给出了5个MQ的demo示例,对应了集中不同的用法:
基本消息队列的消息发送流程:
基本消息队列的消息接收流程:
Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
SpringAMQP 是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amop是基础抽象,spring-rabbit是底层的默认实现。
案例:利用SpringAMQP实现helloworld中的基础消息队列功能
流程:
步骤一:
引入AMQP依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
步骤二:
在publisher服务中编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 43.143.237.123 #rabbitMQ的ip地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: rosevvi
password: 123321
在publisher服务中新建一个测试类,编写测试方法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class PublisherTest {
@Autowired
private RabbitTemplate rabbitTemplatel;
@Test
public void setRabbitTemplatel(){
String queuename = "simple.queue";
String message = "hello rosevvi";
rabbitTemplatel.convertAndSend(queuename,message);
}
}
步骤三:在consumer中编写消费逻辑,监听simple.queue
在consumer服务中编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 43.143.237.123 #rabbitMQ的ip地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: rosevvi
password: 123321
在consumer服务中新建一个类,编写消费逻辑
@Component
public class RabbitListener {
@org.springframework.amqp.rabbit.annotation.RabbitListener(queues = "simple.queue")
public void listenerSimpleQueueMessage(String msg){
System.out.println("消费者收到消息:{"+msg+"}");
}
}
Workqueue,工作队列,可以提高消息处理速度,避免队列消息堆积
案例:模拟WorkQueue,实现一个队列绑定多个消费者
基本思路:
消费预取限制:修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限.
spring:
rabbitmq:
host: 43.143.237.123 #rabbitMQ的ip地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: rosevvi
password: 123321
listener:
simple:
prefetch: 1
发布、订阅
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)
常见exchange类型包括:
FanoutExchange会将收到的消息路由到每一个跟其绑定的queue
案例:
步骤:
在consumer服务声明Exchange、Queue、Binding
@Configuration
public class FanoutConfig {
//定义交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("rosevvi.fanout");
}
//定义队列1
@Bean
public Queue queue1(){
return new Queue("rosevvi.queue1");
}
//绑定队列1和交换机
@Bean
public Binding bindingQueue1(Queue queue1,FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue1).to(fanoutExchange);
}
//定义队列2
@Bean
public Queue queue2(){
return new Queue("rosevvi.queue2");
}
//绑定队列2和交换机
@Bean
public Binding bindingQueue2(Queue queue2,FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue2).to(fanoutExchange);
}
}
在consumer服务声明两个消费者
@org.springframework.amqp.rabbit.annotation.RabbitListener(queues = "rosevvi.queue1")
public void listenerFanoutMessage1(String msg) throws InterruptedException {
System.out.println("消费者1收到Fanout消息:{"+msg+"}"+ LocalDateTime.now());
}
@org.springframework.amqp.rabbit.annotation.RabbitListener(queues = "rosevvi.queue2")
public void listenerFanoutMessage2(String msg) throws InterruptedException {
System.err.println("消费者2收到Fanout消息:{"+msg+"}"+LocalDateTime.now());
}
在publisher服务发送消息到FanoutExchange
@Test
public void fanOutExchangeTest(){
//交换机名字
String name= "rosevvi.fanout";
//消息
String message = "rosevvi,Hello";
rabbitTemplatel.convertAndSend(name,"",message);
}
交换机的作用是什么:
声明队列、交换机、绑定关系的bean是什么:
DirectExchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)
案例:利用SpringAMQP演示DirectExchange的使用
实现思路:
步骤1:在consumer服务声明Exchange、Queue
@RabbitListener(bindings = @QueueBinding(
value = @Queue("rosevvi.direct.queue2"),
exchange = @Exchange(value = "rosevvi.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenerDirectMessage2(String msg){
System.err.println("消费者2收到Direct消息:{"+msg+"}"+LocalDateTime.now());
}
总结:
描述下Direct交换机与Fanout交换机的差异?
基于@RabbitListener注解声明队列和交换机有那些常见注解?
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割
Queue与Exchange指定BindingKey时可以使用通配符:
案例:利用SpringAMQP演示TopicExchange的使用
实现思路如下:
步骤1:在xonsumer服务声明Exchange、Queue
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "rosevvi.topic.queue2"),
exchange = @Exchange(name = "rosevvi.topic",type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenerTopicMessage2(String msg){
System.err.println("消费者2收到Topic消息:{"+msg+"}"+LocalDateTime.now());
}
案例:测试发送Object类型消息
说明:在SpringAMQP的发送方法中,接收消息的类型时object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。
Spring的消息对象处理是由MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如我要修改只需要定义一个MessageConverter类型的Bean即可。推荐使用Json方式序列化,步骤如下:
在publisher服务引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
dependency>
在publisher服务声明MessageConverter
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
SpringAMQP中消息的序列化和反序列化是怎么实现的?
什么是elasticsearch
什么是Elastic Stack(ELK)?
什么是Lucene?
传统数据库(Mysql)采用正向索引,例如id自增利用id创建索引。
elasticsearch采用倒排索引:
比如:
id | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 299 |
词条(term) | 文档id |
---|---|
小米 | 1,3,4 |
手机 | 1,2 |
华为 | 2,3 |
充电器 | 3 |
手环 | 4 |
搜索时“华为手机”,会将华为手机进行分词,得到“华为”,“手机”两个词条,再利用词条去搜索得当文档id
总结:
什么是文档和词条?
什么是正向索引?
什么是倒排索引?
文档:elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储再elasticsearch中。
索引(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,实现FRUD |
架构
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
ElasticSearch:擅长海量数据的搜索、分析、计算
单点部署
创建网络
docker network create es-net
加载镜像
这里采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议自己pull,建议上传到虚拟机后,自行加载
docker load -i es.tar
同理kibana的tar包也需要这样做
运行:运行docker命令,部署单点es:
docker run -d \
--name es \
-e "ES_$JAVA_OPTS=-Xms512m -Xms512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/date \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \ #暴露再http协议的端口
-p 9300:9300 \ #es容器各个节点之间互联的端口
elasticsearch:7.12.1
命令解释:
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
es再创建倒排索引时需要对文档分词;再搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好,我们再kibnana的DevTools中测试:
POST /_analyze
{
"analyzer":"standard",
"text":"黑马程序员学习java太棒了"
}
语法说明:
在线:
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install
https://github.com/medcl/elasticsearch-analysis-ik/release/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
# 退出
exit
# 重启容器
docker restart elasticsearch
离线:
安装插件需要指导elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticearch的数据卷目录,通过下面命令查看:
docker volume inspect es-plugins
解压分词器安装包,重命名为ik
上传到es容器的插件数据卷中,也就是/var/lib/docker/volumes/es-plugins/_data;
重启容器
#重启容器
docker restart es
#查看es日志
docker logs -f es
测试:ik分词器包含两种模式
要扩展分词器的词库,只需要修改一个ik分词器目录中的config目录中的IKAnalyzer.cfg.xml文件:
<properties>
<commemt>Ik analyzer 扩展配置 comment>
<entry key="ext_dict">ext.didentry>
<entry key="ext_stopwords">stopwords.dicentry>
properties>
mapping属性时是对索引库中文档的约束,常见的mapping属性包括:
es中通过restful请求操作索引库、文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下:
PUT /rose
{
"mapping":{
"properties":{
"info":{
"tupe":"text",
"analyzer":"ik_smart"
},
"email":{
"type":"keyword",
"index":"false"
},
"name":{
"properties":{
"firstName":{
"type":"keyword"
}
}
}
}
}
}
查看索引库语法:
GET /索引库名
示例:
GET /rose
删除索引库的语法:
DELETE /索引库名
示例:
DELETE /rose
修改索引库:
索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下:
PUT /索引库名/_mapping
{
"properties":{
"新的字段名":{
"type":"integer"
}
}
}
#注意新的字段一定是 原本不存在的
添加文档: 新增文档的DSL语法如下:
POST
{
"字段1":"值1",
"字段2":"值2",
"字段3":{
"子属性1":"值3",
"子属性2":"值4",
}
}
查询文档语法:
GET /索引库名/_doc/文档id
示例:
GET /rose/_doc/1
删除索引库的语法:
DELETE /索引库名/_doc/文档id
示例:
DELETE /rose/——doc/
修改文档:
方式一:全量修改,回删除旧文档,添加新文档。 既能更新也能添加
PUT /索引库名/_doc/文档id
{
"字段1":"值1",
"字段2":"值2"
//。。。略
}
方式二:增量修改,修改指定字段值
POST /索引库名/_update/文档id
{
"doc":{
"字段名":"新的值"
}
}
什么是RestClient?
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过gttp请求发送给es。
案例:利用JavaRestClient实现创建、删除索引库,判断索引库是否存在
根据课前资料提供的九点数据创建索引库,索引库名为hotel,mapping属性根据数据库结构定义。
基本步骤如下:
Es中支持两种地理坐标数据类型:
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:
"all":{
"type":"text",
"analyzer":"ik_max_word"
},
"brand""{
"type":"keyword",
"copy_to":"all"
}
引入es的RestHighClient依赖:
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elaeticsearch-rest-high-level-clientartifactId>
dependency>
因为Springboot默认的es版本是7.6.2,所以我们需要覆盖默认的es版本:
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.12.1elasticsearch.version>
properties>
初始化RestHighLevelClient:
RestHighLevelClient client =new RestHighLevelClient(RestClient.builder(
this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http:43.143.237.123:9200")));
))
@Test
void testInit() throws IOException {
//1、创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
//2、请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引可的DSL语句
request.source(MAAPING_TEMPLATE, XContentType.JSON);
//3、发起请求
client.indices().create(request, RequestOptions.DEFAULT);
}
@Test
void testDelete() throws IOException {
//1、创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
//3、发起请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
@Test
void testExits() throws IOException {
//1、创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
//3、发起请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.err.println(exists?"索引库存在":"索引库不存在");
}
案例:利用JavaRestClient实现文档的CRUD
去数据库查询酒店数据,导入到hotel索引库,实现酒店数据的CRUD。
基本步骤如下:
新建一个测试类,实现文档相关操作,并且完成javarestClient的初始化
@SpringBootTest
public class HotelDocumentTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://43.143.237.123:9200")));
}
@AfterEach
void tearDown() throws IOException {
client.close();
}
}
先查询酒店数据,然后给这条数据创建倒排索引,即可完成添加:
@Autowired
private IHotelService iHotelService;
@Test
void testAddDocument() throws IOException {
//查询数据
Hotel hotel = iHotelService.getById(36934L);
//转换为文档类型
HotelDoc hotelDoc=new HotelDoc(hotel);
//1、准备request对象
IndexRequest indexRequest = new IndexRequest("hotel").id(hotel.getId().toString());
//2、准备json文档
indexRequest.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
//3、发送请求
client.index(indexRequest, RequestOptions.DEFAULT);
}
根据id查询到的文档数据是json,需要反序列化为java对象
@Test
void testGetDocument() throws IOException {
GetRequest request = new GetRequest("hotel").id("36934");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
@Test
void testUpdateDocument() throws IOException {
//1、创建request对象
UpdateRequest request =new UpdateRequest("hotel","36934");
//2、准备请求参数
request.doc(
"price","380",
"starName","新二钻"
);
//3、发送请求
client.update(request,RequestOptions.DEFAULT);
}
@Test
void testDeleteDocument() throws IOException {
//1、创建request对象
DeleteRequest request =new DeleteRequest("hotel","36934");
//3、发送请求
client.delete(request,RequestOptions.DEFAULT);
}
@Test
void testBulkDocument() throws IOException {
//批量查询酒店数据
List<Hotel> list = iHotelService.list();
//1、创建request对象
BulkRequest request =new BulkRequest();
//2、准备参数
for (Hotel hotel : list) {
HotelDoc hotelDoc=new HotelDoc(hotel);
request.add(new IndexRequest("hotel").id(
hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON)
);
}
//3、发送请求
client.bulk(request,RequestOptions.DEFAULT);
}
Elasticsearch提供了基于json的dsl来定义查询。常见的查询类型包括:
查询的基本语法如下:
GET /indexName/_search
{
"query":{
"查询类型":{
"查询条件":"条件值"
}
}
}
查询所有
GET /indexName/_search
{
"query":{
"match_all":{
}
}
}
全文检索查询,会对用户输入内容分词,常用于搜索框搜索:
matcha查询:全文检索查询的一种,会对用户输入的内容进行分词,然后去倒排索引库检索,语法:
GET /indexName/_search
{
"query":{
"match":{
"FIELD":"TEXT"
}
}
}
multi_match:与match查询相似,只不过允许同时查询多个字段,语法:
GET /indexName/_search
{
"query":{
"multi_match":{
"query":"TEXT",
"fields":["Field1","Field2"]
}
}
}
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
语法如下:
term:
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "上海"
}
}
}
}
range:
#精确查询 range
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 335,
"lte": 337
}
}
}
}
根据经纬度查询。常见的使用场景:
根据经纬度查询,官方文档。例如:
geo_bounding_box:查询geo_point值落在某个举行范围的所有文档
GET /indexName/_search
{
"query":{
"geo_bounding_box":{
"FIELD":{
"top_left":{
"lat":31.1,
"lon":121.5
},
"bottom_right":{
"lat":30.9,
"lon":121.7
}
}
}
}
}
geo_distance:查询到指定中心点小于莫格距离值的所有文档
GET /indexName/_search
{
"query":{
"geo_distance":{
"distance":"15km",
"FIELD":"31.21,121.5"
}
}
}
复合查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑,例如:
相关性算分
当我们利用match查询时,文档结果回根据与搜索词条的关联度打分,返回结果时按照分值降序排列。
使用FunctionScoreQuery,可以修改文档的相关性算分,根据新的到的算分排序。
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "上海"
}
},
"functions": [
{
"filter": {
"term": {
"id": "5873072"
}
},
"weight": 10
}
],
"boost_mode": "multiply"
}
}
}
布尔查询是一个或多个查询字句的组合,子查询的组合方式有:
案例:利用bool查询实现功能:
需求:搜索名字包含如家,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
#复合查询 BooleanQuery
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"score": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
elasticsearch支持对搜索结果排序,默认是根据相关度算分来排序。可以排序的字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
# 排序 标准
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
# 排序 地理坐标
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.034661,
"lon": 121.612282
},
"order": "asc",
"unit": "km"
}
}
]
}
elasticsearch默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
elastic search中通过修改from、size参数来控制要返回的分页结果:
#分页
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5
}
深度分页问题
ES是分布式的,所以回面临深度分页问题。例如按照price排序后,获取from=990,size=10的数据:
如果搜索页数过深,或者结果集(from+size)越大,对内存和cpu的消耗越高。因此es色号顶结果集查询上线是10000
深度分页解决方案
针对深度分页,es提供了两种解决方案
高亮:就是在搜索结果中把搜索关键字突出显示
原理:
#高亮
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false"
}
}
}
}
我们通过match_all来演示基本的api
RestAPI中其中构建DSL是通过HighLevelRestClient中的resource来实现的,其中包含了查询、排序、分页、高亮等所有功能。
RestAPI中其中构建条件的核心部分是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法
@Test
void testMatchAll() throws IOException {
//1、获取SearchRequest对象
SearchRequest request=new SearchRequest("hotel");
//2、设置参数
request.source().query(QueryBuilders.matchAllQuery());
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4、处理结果集
SearchHits hits = response.getHits();
TotalHits totalHits = hits.getTotalHits();
System.out.println(totalHits);
SearchHit[] searchHits = hits.getHits();
//5.遍历
for (SearchHit searchHit : searchHits) {
String source = searchHit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(source, HotelDoc.class);
System.out.println(hotelDoc);
}
}
match
@Test
void testMatch() throws IOException {
//1、获取SearchRequest对象
SearchRequest request=new SearchRequest("hotel");
//2、设置参数
request.source().query(QueryBuilders.matchQuery("name","如家"));
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handlerResponse(response);
}
boolean: term、range
@Test
void testBool() throws IOException {
//1、获取SearchRequest对象
SearchRequest request=new SearchRequest("hotel");
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//2、设置dsl传语句
boolQuery.must(QueryBuilders.termQuery("city","北京"));
boolQuery.mustNot(QueryBuilders.rangeQuery("price").gt(500));
//2、设置参数
request.source().query(boolQuery);
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handlerResponse(response);
}
@Test
void testPageAndSort() throws IOException {
int page = 2,size = 5;
//1、获取SearchRequest对象
SearchRequest request=new SearchRequest("hotel");
//2、设置参数
request.source().query(QueryBuilders.matchAllQuery());
request.source().from((page-1) * size).size(size);
request.source().sort("price", SortOrder.ASC);
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handlerResponse(response);
}
@Test
void testHighLight() throws IOException {
//1、获取SearchRequest对象
SearchRequest request=new SearchRequest("hotel");
//2、设置参数
request.source().query(QueryBuilders.matchQuery("name","北京"));
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handlerResponse(response);
}
private void handlerResponse(SearchResponse response) {
//4、处理结果集
SearchHits hits = response.getHits();
TotalHits totalHits = hits.getTotalHits();
System.out.println(totalHits);
SearchHit[] searchHits = hits.getHits();
//5.遍历
for (SearchHit searchHit : searchHits) {
String source = searchHit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(source, HotelDoc.class);
Map<String, HighlightField> map = searchHit.getHighlightFields();
if (!CollectionUtils.isEmpty(map)){
HighlightField highlightField = map.get("name");
if (highlightField!= null){
String string = highlightField.getFragments()[0].string();
hotelDoc.setName(string);
}
}
System.out.println(hotelDoc);
}
}
聚合的分类
聚合可以实现对文档数据的统计、分析、运算。聚合常见的有三大类:
聚合字段类型必须是:keyword、数值、日期、布尔。
现在我们要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合。类型为term
# 聚合查询
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 500
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
聚合结果排序
默认情况下,bucket聚合会统计Bucket内的文档数量,记为_count,并且按照 _count降序来排序,我们可以修改结果排序方式
# 聚合查询
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 500
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20,
"order": {
"_count": "desc"
}
}
}
}
}
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要做聚合的文档范围,只要添加query条件即可
例如我们要求获取每个品牌的用户评分的min、max、avg等值
# 数据聚合stats
GET /hotel/_search
{
"size": 0,
"query": {
"range": {
"price": {
"lte": 1000
}
}
},
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"scoreAgg.avg": "desc"
}
},
"aggs": {
"scoreAgg": {
"stats": {
"field": "score"
}
}
}
}
}
}
@Test
void testAggregation() throws IOException {
//1、获取SearchRequest对象
SearchRequest request = new SearchRequest("hotel");
//2、设置参数
request.source().size(0);
request.source().aggregation(AggregationBuilders.terms("branAgg").field("brand").size(5));
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Aggregations aggregations = response.getAggregations();
Terms brandTerms = aggregations.get("branAgg");
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println(key);
}
}
要实现根据字母做补全,就必须对文档按照拼音分词。在github上恰好有elasticsearch的拼音分词插件。
elasticsearch中分词器的组成包含三个部分:
我们可以在创建索引库时,通过settings来配置自定义的analyzer(分词器):
put /test
{
"settings":{
"analysis":{
"analyzer":{#自定义分词器
"my_analyzer":{ #自定义的分词器名
"tokenizer":"ik_max_word",
"filter":"pinyin"
}
}
}
}
}
// 自定义拼音分词器
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
}
}
}
}
}
自定义分词器适合在创建倒排索引的时候使用,但不能再搜索的时候使用。
因此字段在创建倒排索引时应该用my_analyzer分词器;在搜索时应使用ik_smart分词器
// 自定义拼音分词器
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
}
},
"mappngs":{
"properties":{
"name":{
"type":"text",
"analyzer":"my_analyzer",
"search_analyzer":"ik_smart"
}
}
}
}
}
}
elasticsearch提供了Completion suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询效率,对于文档中字段的类型有一些约束:
参与补全查询的字段必须是completion类型
put test
{
"mappings":{
"properties":{
"title":{
"type":"completion"
}
}
}
}
字段的内容一般是用来补全的多个词条形成的数组
查询语法如下:
GET /test/_search
{
"suggest":{
"title_seggest":{
"text":"s", #关键字
"completion":{
"field":"title", #补全查询的字段
"skip_duplicates":"true", #跳过重复的
"size":10 #获取前十条数据
}
}
}
}
@Test
void testSuggest() throws IOException {
//1、获取SearchRequest对象
SearchRequest request = new SearchRequest("hotel");
//2、设置参数
request.source().suggest(
new SuggestBuilder().addSuggestion(
"my_suggest",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("s")
.skipDuplicates(true)
.size(10)
)
);
//3、发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Suggest suggest = response.getSuggest();
CompletionSuggestion my_suggest = suggest.getSuggestion("my_suggest");
List<CompletionSuggestion.Entry.Option> options = my_suggest.getOptions();
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().string();
System.out.println(text);
}
}
Elasticsearch中的酒店数据来自于mysql数据库,因此,mysql数据库发生改变时,elasticsearch也必须跟着改变,这个就时elasticsearch与mysql之间的数据同步。
当酒店管理服务操作mysql数据库时,调用酒店搜索服务暴露的接口来修改Es
使用mq中间件来实现,当酒店管理服务操作mysql数据库时,利用MQ通知酒店搜索服务,让酒店搜索服务更新ES
使用mysql的binlog来实现,当酒店管理服务操作mysql数据库时,利用canal来监听mysql的binlog,当binlog改变就通知酒店搜索服务数据变更,酒店搜索服务来更新ES
总结:
利用MQ实现mysql与es数据同步
步骤:
声明exchange、queue、RoutingKey
@Configuration
public class MQConfig {
/**
* 交换机
* @return
*/
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(HotelMqConstants.EXCHANGE_NAME,true,false);
}
/**
* 插入和修改队列
* @return
*/
@Bean
public Queue insertQueue(){
return new Queue(HotelMqConstants.INSERT_QUEUE_NAME,true);
}
/**
* 插入和修改队列
* @return
*/
@Bean
public Queue deleteQueue(){
return new Queue(HotelMqConstants.DELETE_QUEUE_NAME,true);
}
/**
* 插入与修改绑定关系
* @return
*/
@Bean
public Binding insertBinding(){
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(HotelMqConstants.INSERT_KEY);
}
/**
* 删除绑定关系
* @return
*/
@Bean
public Binding deleteBinding(){
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(HotelMqConstants.DELETE_KEY);
}
}
@PostMapping
public void saveHotel(@RequestBody Hotel hotel){
// 新增酒店
hotelService.save(hotel);
// 发送MQ消息
rabbitTemplate.convertAndSend(HotelMqConstants.EXCHANGE_NAME, HotelMqConstants.INSERT_KEY, hotel.getId());
}
/**
* 监听插入和修改
* @param id
*/
@RabbitListener(queues = HotelMqConstants.INSERT_QUEUE_NAME)
public void listenHotelInsertOrUpdate(Long id){
iHotelService.insertById(id);
}
/**
* 监听删除
* @param id
*/
@RabbitListener(queues = HotelMqConstants.DELETE_QUEUE_NAME)
public void listenHotelDelete(Long id){
iHotelService.deleteById(id);
}
单机的es做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。
docker-compose
version: '2.2'
services:
es01:
image: elasticsearch:7.12.1
container_name: es01 #容器名
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03 #另外两个ip docker内可以用名
- cluster.initial_master_nodes=es01,es02,es03 #主节点选举
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data02:/usr/share/elasticsearch/data
ports:
- 9201:9200
networks:
- elastic
es03:
image: elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
ports:
- 9202:9200
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
es运行还需要修改一些linux的系统权限,修改/etc/sysctl.conf文件
vi /etc/sysctl.conf
#添加下面内容
vm.max_map_count=262144
#然后执行命令,让配置生效
sysctl -p
kibana监控不好用,用cerebro来监控集群状态,解压后直接用
创建索引库
利用kibana的devtools创建索引库
put /rose
{
"settings":{
"number_of_shards":3, #分片数量
"number_of_replicas":1 #副本数量
},
"mappings":{
}
}
节点类型 | 配置参数 | 默认值 | 节点职责 |
---|---|---|---|
master eligible | node.master | true | 备选主节点:主节点可以管理和记录集群状态、决定分片在那个节点、处理创建和删除索引库的请求 |
data | node.data | true | 数据节点:存储数据、搜索、聚合、CRUD |
ingest | node.ingest | true | 数据存储之前的预处理 |
coordingting | 上面三个都为false,则为coordingnating节点 | 五 | 路由请求到其他节点,合并其他节点处理的结果,返回给用户 |
es中的每个节点角色都有自己不同的职责,因此建议集群部署时,每个节点都有独立的角色。
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其他候选节点会选举一个称为新的主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。
为了避免脑裂问题,需要要求选票超过(eligible节点数量+1)/2才能当选为主节点,因此eligible节点数量最好是奇数。对应配置项时discover.zen.minimum_master_nodes,在es7之后,已经成为默认配置,因此一般不会发生脑裂问题。
当心怎文档时,应该保存到不同分片,保证数据均衡,那么coordingnating node如何确定数据改存储到那个分片呢?
Es会通过hash算法来计算文档应该存储到那个分片:
shard = hash(_routing) % number_of_shards
说明:
算法与分片数量有关,因此索引库一旦创建,分片数量不能修改
ES的查询分成两个阶段:
集群的master节点回监控集群中节点状态,如果发现有节点宕机,回立刻将宕机节点的分片数据迁移到其他节点,确保数据安全,这个叫做故障转移
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
解决雪崩问题的常见方式有四种: