SpringCloud
1.名词解释
什么是集群?
集群就是相当于将一个服务复制多份,他们每一个节点都是独立的
集群可以解决什么问题?
使用单个服务时,因为服务并发量有限,当并发量过大时会导致服务器宕机,使用集群就可以解决因为并发量过大的问题,因为集群的每个节点都有一个服务器,所以会将人流量均摊到每个服务,就可以做到降低并发量高的时候单个服务器的压力
保证服务的高可用,如果是单个服务,那么当服务出现故障那么这个项目就完全瘫痪了,如果是一个集群,因为他有多个节点,所以一个节点不能用了就可以使用另外一个节点,这样保证了服务的不会因为一个故障导致整个项目出现故障
什么是负载均衡? 为什么需要负载均衡?
1:负载均衡可以将我们请求的路径根据负载均衡策略去匹配集群中某个节点的地址,
负载均衡策略常用的有 轮询,随机
2:当我们是一个集群,并且我们每个节点的地址都已经注册到注册中心了,假如我们的服务器名称为:orderservice,并且集群有三个节点8081,8082,8083,这时候我们需要访问这个集群的节点,怎么写?肯定是不能写死地址例如:127.0.0.1:8081,这样的话就会将地址写死,又成了一个单个服务干活了,我们集群的目的就是多个节点一起干活,这时候就需要我们穿过去一个地址,然后根据这个地址去自动给我们分配我们应该用那个节点,这就是负载均衡干的活,例子:我们使用一个 127.0.0.1:orderservice 这时候就会负载均衡到我们这三个节点地址根据我们的负载均衡策略去分配一个节点给这个请求使用。
3:当我们有一个微服务,如果某一个节点需要更新,那么如果要暂时停掉这个功能,那么我们就可以设置负载均衡策略,将流量都弄到40%的节点,然后另外60%的节点进行更新,当更新完再设置负载均衡策略把人都弄到更新过的,然后另外40%的节点去更新。这样就做到了在客户眼里的不停机更新
什么是分布式? 为什么需要分布式?
分布式是根据不同业务来区分不同的模块,比如service mapper controller层都是根据不同的任务来区分不同的层级,分布式个人理解就是这样的思想,一个模块代表一类的功能模块,每个功能模块相互联合起来就是一个整的项目,这样每个模块都可以专注于做一件事情,就跟电子厂流水线一样,每个站只专注做一个事情,他们连起来就是一个完整的生产线
什么是反向代理? 为什么需要反向代理?
反向代理就是相当于负载均衡那样,当我们请求一个链接是,并不知道请求的是集群中的具体什么地址,这时候就有Ribbon替我们去找,也就是说我们并不知道我们真正需要访问的是什么链接但是能得到我们想要的数据
什么是正向代理? 为什么需要正向代理?
???????
2.什么是单体架构?
请描述你对单体架构的理解
将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:
架构简单
部署成本低
缺点:
耦合度高(维护困难、升级困难)
技术栈受限
3.什么是微服务架构?
请描述你对微服务架构的理解,以及为什么需要使用微服务架构?
将每个功能都拆分,按照不同的功能进行拆分,这个才分的功能是独立的,他们联合起来就是一个整的项目,这些拆分出来的功能可以进行集群,这样可以高并发和单点故障
4.微服务架构的优缺点有哪些?
优点:
降低服务耦合,不会因为某一个服务之间出了问题导致整个项目出现问题,并且如果某一个功能需要重新上线时,那么也不用停掉整个服务
有利于服务升级和拓展
技术栈不受限,每个服务都是相互独立的,并且都会开放出来接口让其他服务调用,这样我们就可以使用不同的业务,比如:支付使用Java写,订单使用c++写,只要让我们订单服务提交订单时候去调用支付的服务就行,不用管他是Java写的还是c++写的,但是单体架构就不行了。
缺点:
服务调用关系错综复杂
5.什么叫做微服务治理?
首先就是为了解决让我们的请求可以得到我们请求某一个服务的全部地址。适用于实现各个微服务之间的自动注册和发现,利用nacos可以实现注册微服务。这样客户端都注册到了nacos,然后nacos就会有一个心跳机制去检测每个注册进来的服务心跳,每个服务每隔一段时间就会给nacos发送一个请求告诉nacos还活着,如果有一段时间发现已经死了那么就会剔除
6.微服务拆分原则有哪些?
不同微服务,不要重复开发相同业务
微服务数据独立,不要访问其它微服务的数据库
微服务可以将自己的业务暴露为接口,供其它微服务调用
7.微服务常用的组件有哪些
1.服务注册与发现
Rureka、Zookeeper、Consul、Nacos、Eureka
2.服务负载与调用
Ribbon、LoadBalancer、Feign、OpenFeign
3.服务熔断降级
Hystrix、resilence4j、sentienl
4.服务网关
Zuul、Gateway
5.服务分布式配置
Config、Nacos
6.服务总线
Bus、Nacos
8.Eureka组件
1 Eureka组件的作用
是负载均衡,主要有两个策略,轮询和随机 当请求发送过来,Ribbon就会拦截链接,然后根据url获取服务器名称去eureka注册中心拉对应地址,然后跟据负载均衡策略来确定具体地址
2 Eureka组件的应用场景
当某一个功能服务是一个集群,那么我们在调用的时候链接会写死,只能访问单一的一隔节点,这样集群特性就没用了,那么这时候可以使用eureka来将每个节点的地址注册到eureka,然后请求过来根据名称得到全部的地址再通过底层的Ribbon来进行负载均衡策略来确定本次请求具体访问那个地址
3 Eureka组件的使用步骤
第一步,引入服务端的依赖,服务端包含了springBoot的启动各类了所以不用再引用SpringBoot启动类:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
第二部写启动类:
给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能:
例如:
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
第三步编写配置文件:
server:
port: 10086
spring:
application:
name: eureka-server
# Eureka服务端本身就是一个雌雄同体的,意思就是eureka服务端本身就是一个既可以是服务端又可以是客户端,
# 因为eureka本身就是一个集群,这样eureka就充当客户端又是服务端
# 当请求过来需要访问注册中心,然后eureka服务端就会充当客户端和他的集群联系然后备份给其他集群,
# 然后再充当服务端给这个请求的对应url的客户端的地址,这样做的好处就是当一个eureka节点挂了,
# 那么因为是集群又是每次都使用了备份,这样就可以直接使用下一个eureka节点。 正是由于这个原因,在eureka服务端的配置文件中配置#时就需要
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
register-with-eureka: false # 不注册自己
fetch-registry: false #不拉取服务本eureka服务中的服务信息
第四步配置客户端:
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件:
修改application.yml文件,添加服务名称、eureka地址:
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance: # 在Eureka中显示服务的ip地址
ip-address: 127.0.0.1 # 配置服务器ip地址
prefer-ip-address: true # 更倾向于使用ip,而不是host名
instance-id: ${eureka.instance.ip-address}:${server.port} # 自定义实例的id
4 Eureka组件是如何进行服务状态监控的
客户都会隔30s就给服务端发送心跳,每隔一分钟服务端会检查客户端,如果超过三次没有发送心跳,那么服务端会认为这个服务已经死了,那么就会剔除,但是剔除最大剔除总服务器数量的的15%,如果已经死了的服务器又没有被剔除,然后又活了,那么依然可以使用
5 Eureka组件服务剔除的时机
每过一分钟就会检查一次,如果超过三次没有发送心态就会剔除,但是剔除不会超过15%,当剔除了15%还有需要剔除的,eureka也不会剔除,当死掉的没有被剔除的活过来了那么就会继续发送心跳继续用
9.Ribbon组件
1 Ribbon组件的作用
是负载均衡,主要有两个策略,轮询和随机 当请求发送过来,Ribbon就会拦截链接,然后根据url获取服务器名称去eureka注册中心拉对应地址,然后跟据负载均衡策略来确定具体地址,eureka底层的负载均衡就是它做的
2 Ribbon组件的使用步骤
添加@LoadBalanced到制作RestTemplate bean对象的方法上,默认是轮询,也可以自己设置策论:
第一种:
@Bean
public IRule randomRule(){
return new RandomRule();
}
第二种:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
饥饿加载:因为Ribbon默认是懒加载模式,也就是第一次访问才会去创建LoadBalanceClient,这样会造成第一次请求时间过长
在配置类中写下边的代码可以改为饥饿加载,也就是启动服务就会创建LoadBalanceClient
ribbon:
eager-load:
enabled: true
#写下边一行就代表只有这个userservice名称的是饥饿加载,不写的话就代表全部都是饥饿加载
clients: userservice
10.Nacos组件
Nacos组件-注册中心
a.作用?
这个组件能做什么? 能够解决什么问题?
管理服务提供者的地址信息
监控服务提供者的健康状态
Nacos注册中心,它可以替代Eureka,也是一个服务中心,但是她更好用,因为他是一个可视化界面,并且可以支持热更新,以及properties的配置也可以给这上边配置。所以nacos既是配置中心又是注册中心
b.问题的由来?
如何管理服务提供者的地址
多个服务提供者的地址如何管理
如何监控服务提供者的健康状态
c.实现步骤? 技术的使用方式?
启动nacos服务
nacos是一个外部独立的服务,直接解压启动即可.
单机模式:
cmd 进入到bin目录下
start.cmd -m standalone
默认端口:8848
登录名:nacos
密码:nacos
集群模式: 是三个nocas都需要配置,例如第一个配置文件写
① 修改配置文件
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
修改如下:
127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847
② 连接数据库 三个nacos都要这样操作
【1】初始化数据库==执行conf目录下的mysql文件
【2】修改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=root
③ 修改端口
进入每个的application.properties文件,修改端口如下:
server.port=8845 第一个nocas写这个
server.port=8846 第二个nocas写这个
server.port=8847 第三个nocas写这个
消费者方引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--想要使用Alibaba的组件必须先导入Alibaba的环境依赖-->
添加配置信息
spring:
cloud:
nacos:
server-addr: localhost:8848
使用集群首先设置集群名称
discovery:
cluster-name: HZ # 集群名称
启动微服务
Nacos组件-配置中心
a.作用?
这个组件能做什么? 能够解决什么问题?
将配置文件从微服务中分离,存放在nacos中,微服务启动时先从nacos中获取配置信息,在加载微服务程序
当我们的服务器集群实例很多时,我们肯定是不能通过修改我们本地的配置文件来修改一些东西,因为都是一个集群,所以重复的配置我们可以使用一个公共的配置文件,这时候nacos就可以提供一个配置中心,但是我们在本地想要使用nacos的话,需要在bootstrap中配置,因为我们的application.yml的配置文件加载完毕就会加载bean工厂了,所以我们需要使用application这个配置文件之前加载配置中心的配置文件,这时候有一个bootstrap.yml可以在初始化时就会去加载外部的配置文件。bootstrap.yml优先级低于application.yml,如果bootstrap.yml有和application.yml同样的标签,那么使用application.yml里的标签内容
b.问题的由来?
在集群模式下,每一个微服务中存在相同的配置信息,维护起来不方便
c.实现步骤? 技术的使用方式?
启动nacos服务
nacos是一个外部独立的服务,直接解压启动即可.
在nacos中编写外部配置
消费者方引入依赖
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
添加配置信息
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
namespace: 当前项目所属组的id
11.OpenFeign组件
1 Open组件作用
在我们使用之前的RestTemplate远程调用的时候,是直接将url卸载代码中,他的问题就是:
•代码可读性差,编程体验不统一
•参数复杂URL难以维护
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
可以想我们调用数据库那样调用一层一层的调用,然后feign自己去组装地址
例如:
访问的地址就是:http://userservice/user/id
也就是说:@FeignClient("userservice")+GetMapping("/user/{id}")里面的内容拼成的一个链接
然后我们再使用的时候就可以直接注入这个类调用这一层的方法就可以完成远程调用了
2 OpenFeign组件的应用场景
远程调用时可以使用Feign
3 OpenFeign组件的使用步骤
第一步引入nacos-config依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:在启动类上添加注解开启Feign功能,直接添加一个注解:@EnableFeignClients
第三步:创建Feign的客户端:
@FeignClient("userservice")
//也就是代表访问的地址就是:http://userservice/user/id
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryById(@PathVariable("id") Long id);
}
然后我们直接调用就可以得到这个链接远程调用的返回值了,例如:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@Autowired
private UserFeignClient userFeignClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.远程调用获取对应的用户信息
// String url = "http://127.0.0.1:8081/user/" + order.getUserId();
// String url = "http://userservice/user/" + order.getUserId();
// User user = restTemplate.getForObject(url, User.class);
User user = userFeignClient.queryById(order.getUserId());
// 3.将结果封装
order.setUser(user);
// 4.返回
return order;
}
}
4 使用优化-连接池
① 导入依赖
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
② 配置连接池
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
注意:我们在导入了HttpClient的依赖以后就会直接将默认的给替换掉,即使不配置那么也会提换掉,个人认为只要导入了HttpClient依赖bean的工厂就有这个实例,那么Feign就会发现有这个实例就会优先调用HttpClirnt,如果有连接词的这个两种都没有导入依赖那么就是没有实例,Feign发现没有实例就会使用默认的
最佳实现-打包引入
① 打包-导入依赖 下边这个只是例子,具体导入的其实就是我们feign这个服务的jar包
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
② 解决扫描包问题 不然报错
【方式一:指定Feign应该扫描的包】
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
【方式二:指定需要加载的Client接口】
@EnableFeignClients(clients = {UserClient.class})
12.Gateway组件
1 Gateway组件的作用
Gateway网关是我们服务的守门神,所有微服务的统一入口。
网关的核心功能特性:
请求路由
权限控制
限流
也就是说我们的集群全部交给网关调用,我们只需要访问网关就行,网关给我们负载均衡,替我们访问
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大
2 为什么需要使用Gateway组件
作为我们的安全门户,可以进行拦截和路由
3 Gateway组件的使用步骤
1:
引入依赖:
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2:
编写路由规则:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:80 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
filters:
- AddRequestHeader=Truth, Itcast is freaking awesome! # 给这个请求添加请求头后面是信息,请求头的标签为Truth 内容为:Itcast is freaking awesome!
- id: order-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://orderservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件 断言有多种,可以去查看
- Path= /** # 这个是按照路径匹配,这个是匹配全部
default-filters: # 默认过滤项
#也就是说,比如上边的id为user-service他有指定的请求头,但是他们的请求头的key是一样的,那么就是用他自己的,但是下边的order-service没有指定,那么就用默认的
- AddRequestHeader=Truth, Itcast is freaking awesomedefault!
# 。。。
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 # 这次跨域检测的有效期
注意:
上边有两个路由:
一个是
- Path=/user/**
还有一个是
- Path= /**
当第二个的放在第一个的位置时,那么我们不管访问什么都会出现一个问题,就是第二个无论什么路径都会满足,这时候就不会进入第一个- Path=/user/**,即使携带user的也会被拦击,这个如果有共同路径时要考虑先后顺序
4 Gateway路由规则的配置方式
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
filters:
- AddRequestHeader=Truth, Itcast is freaking awesome! # 给这个请求添加请求头后面是信息,请求头的标签为Truth 内容为:Itcast is freaking awesome!
- id: order-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://orderservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件 断言有多种,可以去查看
- Path= /** # 这个是按照路径匹配,只要以/order/开头就符合要求
5 Gateway跨域配置设置
协议,域名,端口三者中任意一者不同即为跨域
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
域名相同,端口不同:localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看跨域资源共享 CORS 详解 https://www.ruanyifeng.com/blog/2016/04/cors.html
解决跨域:在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 # 这次跨域检测的有效期
6 Gateway默认过滤器
过滤器的作用是什么?
① 对路由的请求或响应做加工处理,比如添加请求头
② 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
① 对所有路由都生效的过滤器
7 Gateway全局过滤器
网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口。
全局过滤器实现:
@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();
}
}
注意:
其中那个order是我们过滤器的在过滤链中的执行先后顺序,order越小那么执行的越靠前,如果没指定那么就是按照代码顺序从上往下的顺序
过滤器执行顺序:
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
排序的规则是什么呢?
每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
RabbitMQ
1.什么是同步请求?优缺点?
同步请求就是我们一次请求会将所有的都执行完才会结束程序
同步调用虽然时效性较强,可以立即得到结果
但是同步调用存在以下问题:
1.耦合度高
2.性能和吞吐能力下降
3.有额外的资源消耗
4.有级联失败问题,也就是雪崩,当请求下游服务时,下游服务出现问题,导致请求堆积,将上游服务原本没问题的服务给拖垮了
2.什么是异步请求?优缺点?
异步请求就是并发进行,当我们一个请求过来时,我们可以几个调用同时进行
【优点】
1.调用间没有阻塞,不会造成无效的资源占用
2.耦合度极低,每个服务都可以灵活插拔,可替换
3.流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
【缺点】
1.架构复杂了,业务没有明显的流程线,不好管理
2.需要依赖于Broker(MQ)的可靠、安全、性能
3.同步请求和异步请求区别?
同步请求可以请求以后直接得到结果,知道处理这个请求完毕以后才会结束
异步请求就是一次请求可以调用消费者,通过给MQ的队列中传值让消费者监听得到值然后并行运行我们的其他调用代码
4.RabbitMQ通讯模型架构是什么?
????
5.名词解释
Borker是什么:
Virtual Host是什么:
Exchange 是什么什么作用:
Queue是什么什么作用:
6.RabbitMQ常见的5种工作模式
Helloworld: 入门/基本模式
单生产 ---> 队列(只有一个队列时,交换机省去不写,默认的交换机与队列名称一致) ---> 单消费
work: 工作模式
单生成 ---> 队列 ---> 多消费
发布/订阅:
广播:
路由:
主题:
7.简述RabbitMQ发送消息时的链路
生产者发送消息流程】
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
8.简述RabbitMQ接收消息时的链路
【消费者接收消息流程】
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
6、ack回复
9.SpringAMQP使用步骤
1、导入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置
spring:
rabbitmq:
host: 192.168.248.222 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
10.常见的5种工作模式示例代码
BasicQueue 简单队列模型
生产者对应一个队列,这样的话就不需要我们设置交换机,会有一个默认的交换机
【publisher】
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
// 第一个参数:想要去存储数据到的队列名称
// 第一个参数:存储到队列中的消息
rabbitTemplate.convertAndSend(queueName, message);
}
【consumer】
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
WorkQueue 工作模式
工作模式代码如下:
【publisher】
/**
* workQueue
* 向队列中不停发送消息,模拟消息堆积。
*/
@Test
public void testWorkQueue() throws InterruptedException {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, message_";
for (int i = 0; i < 50; i++) {
// 发送消息
// 第一个参数:想要去存储数据到的队列名称
// 第一个参数:存储到队列中的消息
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
【consumer】
/**
* 这个的消费者中和下边消费者消费的是同一个队列,那么他们就是工作模式,因为工作模式就是:一个链接,一个生产者多个消费者
* @param msg
* @throws InterruptedException
* 睡眠是为了实现能者多老,这样第一个就是慢的那个,第二个就是快的那个,快的那个就应该能者多劳
*/
@RabbitListener(queues = "fanout.queue251")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("第一个的:"+msg);
// 睡眠是为了实现能者多老,这样第一个就是慢的那个,第二个就是快的那个,快的那个就应该能者多劳
Thread.sleep(200);
}
@RabbitListener(queues = "fanout.queue252")
public void listenSimpleQueueMessage1(String msg) {
System.out.println("第二个的:"+msg);
try {
// 睡眠是为了实现能者多老,这样第一个就是慢的那个,第二个就是快的那个,快的那个就应该能者多劳
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这个需要设置一个yml配置
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
这样的话我们只需要设置两个消费者都去指向这个管道就行,其实这样是不需要交换机的,因为就一个管道,有一个默认的交换机
但是工作模式他有一个机制是“预取”机制,就是我们的消息弄到队列中去之后,消费者消费的时候,他是优先把队列的信息分配好之后,然后消费者再去消费,这样是不好的,因为比如上图的consumer2是效率不太行的,那么他执行的量和consumer1是一样的,这样不如按劳分配,能干的多干,不能干的少干,所以我们可以配置一个东西,让他们每次都只拿一个,拿完一个消费完了再去通道中拿。
发布/订阅模式
① Fanout-扇出/广播模式
先说fanout模式,也叫广播模式,生产者给交换机信息,交换机会将信息分为两份分别这两个管道,这样两个管道拿到的信息都是一样的:
首先他是一个交换机对应两个管道,这是就需要我们创建交换机,然后将创建好的管道绑定到交换机中昂,那么我们在创建交换机的时候就可以绑定我们的管道,这样生产者在生产信息给交换机,交换机就会给管道分配信息
fanout代码实现,有两种方式,一种是在config中配置,一种是在注解中写:
@Configuration
public class FanoutConfig {
// 声明交换机 Fanout类型交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
// 第1个队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
// 绑定队列和交换机
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
// 第1个队列
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
// 绑定队列和交换机
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
第二种注解:
【publisher】
// fanout
@Test
public void testFanoutExchange() {
// 队列名称
String exchangeName = "itcast.fanout";
// 消息
// 第一个参数:想要去存储数据到的队列名称
// 第一个参数:存储到队列中的消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
【consumer】
// fanout
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
② Direct-路由模式
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
【publisher】
// direct
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
// 发送消息
/**
* 参数一: 交换机名称
* 参数二: 路由信息
* 参数三: 消息内容
*/
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
【consumer】
/**
* 使用注解来创建交换机和队列和指定路由
*value = @Queue(name = "direct.queue1"), 队列名称
*exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), 交换机和交换机的类型
*key = {"red", "blue"} 这个队列规定key也就是路由
* @param msg
*/
// direct
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) {
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
③ Topic-主题模式
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
【publisher】
// topic
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "itcast.topic";
// 消息
String message = "喜报!孙悟空大战哥斯拉,胜!";
// 发送消息
/**
* 参数一: 交换机名称
* 参数二: 路由信息 因为这里是传的有news后缀,所以在下边消费者中会路由到
* listenTopicQueue2方法中
* 参数三: 消息内容
*/
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
【consumer】
/**
* 使用注解来创建交换机和队列和指定路由
*value = @Queue(name = "topic.queue2"), 队列名称
*exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
*交换机和交换机的类型
*key = "#.news" 这个队列规定key也就是路由,这里意思就是如果后缀满足 .news 为结 *尾,那么就会路由到这个队列中
* @param msg
*/
// topic
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg) {
System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg) {
System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
11.SpringAMQP消息转换器的作用和使用步骤
① 消息是对象
问题:当消息传递是对象时,会调用jdk的序列化,内存空间占用大
【publisher】
// 测试jdk序列化
@Test
public void testSendMap() throws InterruptedException {
// 准备消息
Map<String,Object> msg = new HashMap<>();
msg.put("name", "Jack");
msg.put("age", 21);
// 发送消息
// messageConverter.toMessage(msg, msg);
rabbitTemplate.convertAndSend("simple.queue","", msg);
}
【consumer】
// 测试jdk序列化
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(Map msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
② 解决
<!--json转换-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
12.SpringAMQP创建交换机和队列的两种方式
1注解:
/**
* 使用注解来创建交换机和队列和指定路由
*value = @Queue(name = "direct.queue1"), 队列名称
*exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), 交换机和交换机的类型
*key = {"red", "blue"} 这个队列规定key也就是路由
* @param msg
*/
// direct
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) {
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
2配置
@Configuration
public class FanoutConfig {
// 声明交换机 Fanout类型交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
// 第1个队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
// 绑定队列和交换机
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
// 第1个队列
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
// 绑定队列和交换机
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
13 RabbitMQ注意事项
注意:RabbitMQ是可以支持多个交换机绑定一个队列的,也就是说比如test1交换机可以绑定通道aa,然后test2可是可以绑定通道aa,但是如果通道aa在第一次链接被建立的是Topic类型的,那么交换机想要链接也要是Topic类型的,如下图
ElasticSearch1
1.ES是什么?有什么作用?
【elasticsearch】
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容.
ElasticSearch: 灵活的搜索
分类: 非关系型数据库
概述: ES专门用于对海量数据的搜索
存储介质: 内存
优点: 搜索效率高
2.ES应用场景
在模糊查询中,由于索引失效,mysql的查询效率太过低下。
3.倒排索引过程?
倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
优点:
根据词条搜索、模糊搜索时,速度非常快
缺点:
只能给词条创建索引,而不是字段
无法根据字段做排序=
4.名词解释
索引库: index 就是相同类型的文档的集合。类似于mysql中的表
mapping映射: mapping,是索引中文档的字段约束信息,类似mysql表的结构约束。
文档: Document 就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
词条:Term 对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。
5.Kibana作用?
可视化工具,在浏览器输入DSL语句即可查看ES中的内容。
6.索引库操作
1.Java代码
添加索引库:
修改索引库:
@Test
public void createIndex() throws IOException {
// 1.创建请求语义对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 添加 source
request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
// 2.发送请求
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// 3.解析响应
boolean flag = response.isAcknowledged();
// 打印结果
System.out.println(flag);
}
删除索引库:
@Test
public void deleteIndex() throws IOException {
// 1.创建请求语义对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 3.解析响应
boolean flag = response.isAcknowledged();
// 打印结果
System.out.println(flag);
}
查询索引库:
@Test
public void existsIndex() throws IOException {
// 1.创建请求语义对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean flag = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.解析响应
// 打印结果
System.out.println(flag);
}
2.DSL语句
创建索引库:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
}
}
}
}
查看索引库:
GET /索引库名
修改索引库:
PUT /索引库名/_mapping
例子:
PUT /索引库名/_mapping
{
"properties":{
"新字段":{
"type":"数据类型(integer)"
}
}
}
删除索引库:
DELETE /索引库名
7.文档操作
DSL操作文档
添加文档数据:
POST /索引库名/_doc/文档id(不给会随机生成id)
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
}
}
修改文档数据全量修改:
PUT /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2"
}
修改文档数据追加修改:
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新的值"
}
}
删除文档数据:
DELETE /索引库名/_doc/文档id
查询文档数据:
GET /索引库名/_doc/文档id
9.java代码操作文档
添加文档数据:
@Test
public void add() throws IOException {
// 1.根据id获取数据
Hotel hotel = hotelService.getById(36934L);
// 2.转换成 ES 对应的数据
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.转换成JSON
String hotelDocJson = JSON.toJSONString(hotelDoc);
// 4.创建请求语义对象 这个语义对象就是相当于es中的put
// 所以如果我们es库中有这个字段的数据那么就删除再新增就完成了修改,如果没有这个数据
// 那么就会直接新增 就是全量修改
IndexRequest request = new IndexRequest("hotel");
request.id(hotel.getId() + "");
request.source(hotelDocJson, XContentType.JSON);
// 5.发送请求
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 6.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
修改文档数据:
@Test
public void update() throws IOException {
// 1.创建请求语义对象
UpdateRequest request = new UpdateRequest("hotel","36934");
request.doc(
"name", "7天连锁酒店(上海宝山路地铁站店)"
);
// 2.发送请求
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
// 3.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
删除文档数据:
@Test
public void delete() throws IOException {
// 1.创建请求语义对象
DeleteRequest request = new DeleteRequest("hotel","36934");
// 2.发送请求
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
// 3.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
查看文档数据:
// 查询文档数据
@Test
public void select() throws IOException {
// 1.创建请求语义对象
GetRequest request = new GetRequest("hotel","36934");
// 2.发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
批量添加文档数据
@Autowired
private IHotelService hotelService;
// 批量添加文档数据
@Test
public void add() throws IOException {
// 1.批量查询酒店数据
List<Hotel> hotels = hotelService.list();
// 2.创建请求语义对象
BulkRequest request = new BulkRequest();
for (Hotel hotel : hotels) {
// 转换格式
HotelDoc hotelDoc = new HotelDoc(hotel);
String hotelDocJson = JSON.toJSONString(hotelDoc);
IndexRequest indexRequest = new IndexRequest("hotel");
indexRequest.id(hotel.getId() + "");
indexRequest.source(hotelDocJson, XContentType.JSON);
request.add(indexRequest);
}
// 5.发送请求
BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
// 6.解析响应结果
RestStatus status = response.status();
System.out.println(status);
}
8.mapping映射创建索引库
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
}
}
}
}
9.mysql,redis和ElasticSearch对比
Mysql:
分类: 关系型数据库
概述: mysql在存储数据时,数据和数据之间有一定的关联关系
存储介质: 存放在硬盘上
优点: 不会导致数据丢失
缺点: 执行效率低
硬盘 ---> 内存 ---> CPU
事务控制,这也是他没被替换掉的原因
redis:
分类: 非关系型数据库
概述: 数据在存储时,数据和数据之间没有关联关系
存储介质: 内存
优点: 执行效率高
缺点: 可能会导致数据丢失
ElasticSearch: 灵活的搜索
分类: 非关系型数据库
概述: ES专门用于对海量数据的搜索
存储介质: 内存
优点: 搜索效率高
10.java代码操作-索引库&文档步骤
【导入依赖】
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
【与版本对应】在这里是因为我们使用了elasticsearch 7.12.1所以导入的依赖弄成7.12.1
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
【初始化连接&关闭连接】
private RestHighLevelClient client;
// 创建连接
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.248.222:9200")
));
}
// 关闭连接
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
也可以直接使用bean工厂将RestHighLevelClient注入,然后使用的时候直接自动注入
@Bean
public RestHighLevelClient restHighLevelClient(){
return newRestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.248.222:9200")))
}
ElasticSearch2
1.ES 5种检索类型
查询所有:
GET /索引库名/_search
{
"query": {
"match_all": {
}
}
}
全文检索:
1【多字段】
GET /索引库名/_search
{
"query": {
"multi_match": {
"query": "内容",
"fields": ["字段1", "字段2"]
}
}
}
2【单字段】
GET /hotel/_search
{
"query": {
"match": {
"字段": "值"
}
}
}
精确查询:
【精确查询】
【term查询】
GET /索引库名/_search
{
"query": {
"term": {
"字段": {
"value": "值"
}
}
}
}
【range查询】
GET /索引库名/_search
{
"query": {
"range": {
"字段": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}
}
}
}
地理坐标:
【矩形范围查询】
GET /索引库名/_search
{
"query": {
"geo_bounding_box": {
"字段": {
"top_left": { // 左上点
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下点
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
【附近/距离查询】
GET /索引库名/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"字段": "31.21,121.5" // 圆心
}
}
}
复合查询:
【算分函数查询】
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
// 原始查询条件 根据BM25算法进行算分
"match": {
"all": "外滩"
}
},
"functions": [
{
// 过滤条件 符合条件重新算分
"filter": {
"term": {
"brand": "如家"
}
},
//算分函数 weight是常量值
"weight": 10
}
],
//加权模式 默认是multiply
"boost_mode": "sum"
}
}
}
【布尔查询】
GET /hotel/_search
{
"query": {
"bool": {
//必须匹配
"must": [
{
"match": {
"all": "上海"
}
}
],
//选择匹配
"should": [
{
"term": {
"brand": {
"value": "皇冠假日"
}
}
},
{
"term": {
"brand": "华美达"
}
}
],
//必须不匹配 不参与算分
"must_not": [
{
"range": {
"price": {
"lte": 400
}
}
}
],
//必须匹配 不参与算分
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
2.ES 5种检索类型请求方式
GET /索引库/_search
{
"query":{
"查询类型":{}
}
}
3.ES查询结果处理-排序
1【普通字段】
GET /索引库名/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"字段": "desc" // 排序字段、排序方式ASC、DESC
}
]
}
2【地理坐标】
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"地理坐标相关字段": {
"lat": 31.21,
"lon": 121.5
},
"order":"asc",
//单位
"unit":"km"
}
}
]
}
4.ES查询结果处理-分页
1【基本分页】
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
5.ES查询结果处理-高亮
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": {
"pre_tags": "<em>", // 用来标记高亮字段的前置标签
"post_tags": "</em>", // 用来标记高亮字段的后置标签
"require_field_match": "false"
}
}
}
}
6.ES相关的RESTClient代码实现
1【查询所有】
// 查询所有--match all
@Test
public void matchAll() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 DSL 参数
request.source()
.query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应结果
handleResponse(response);
}
2【全文检索查询】
// 全文检索查询--match
@Test
public void match() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 DSL 参数
request.source()
.query(QueryBuilders.matchQuery("all","如家"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应结果
handleResponse(response);
}
3【精确查询】
1【term查询】
// 精确查询--term
@Test
public void term() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 DSL 参数
request.source()
.query(QueryBuilders.termQuery("city","杭州"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应结果
handleResponse(response);
}
2【range查询】
// 范围查询--range
@Test
public void range() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 DSL 参数
request.source()
.query(QueryBuilders.rangeQuery("price").gte(500).lte(1000));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应结果
handleResponse(response);
}
【复合查询】
1【布尔查询】
// 布尔查询--bool
@Test
public void bool() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 bool 查询参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
boolQuery.filter(QueryBuilders.rangeQuery("price").lt(1000));
// 3.组织 DSL 参数
request.source()
.query(boolQuery);
// 4.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 5.解析响应结果
handleResponse(response);
}
【排序分页】
// 排序、分页--sort from size
@Test
public void sortAndPage() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 DSL 参数
// 查询所有
request.source().query(QueryBuilders.matchAllQuery());
// 前 20条
request.source().from(0).size(20);
// 按价格升序
request.source().sort("price", SortOrder.ASC);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应结果
handleResponse(response);
}
【高亮】
// 高亮--highlights
@Test
public void highlights() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest request = new SearchRequest("hotel");
// 2.组织 DSL 参数
// 查询所有带有如家
request.source().query(QueryBuilders.matchQuery("all","如家"));
// 高亮处理
request.source().highlighter(new HighlightBuilder()
.field("name")
.requireFieldMatch(false));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应结果 -- 高亮特殊处理
handleResponse(response);
}
【响应结果解析】
// 响应结果解析
private void handleResponse(SearchResponse response) {
// 4.解析响应结果
SearchHits searchHits = response.getHits();
// 查询总条数
long total = searchHits.getTotalHits().value;
System.out.println("查询总条数:" + total);
// 查询结果数组
SearchHit[] hits = searchHits.getHits();
// 遍历
for (SearchHit hit : hits) {
// 得到json
String hotelDocJson = hit.getSourceAsString();
// 转换成Java对象
HotelDoc hotelDoc = JSON.parseObject(hotelDocJson, HotelDoc.class);
// 高亮特殊处理
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
// 高亮字段结果
HighlightField highlightField = highlightFields.get("name");
if (highlightField!=null) {
// 不为空时的第一个就是酒店名称
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
}
// 打印结果
System.out.println(hotelDoc);
}
}
ES聚合操作
/**
* 根据品牌分桶
* 对应es语句:
* GET /hotel/_search
* {
* "aggs":{
* "brandAgg":{
* "terms":{
* "field": "brand",
* "size": 4
* }
* }
* }
*
* }
*/
@Test
public void testAgg1() throws IOException {
//创建语义对象
SearchRequest request = new SearchRequest("hotel");
//准备DEL语句,这句话是使用AggregationBuilders创建一个名称为brandAgg的桶,按照brand字段分捅,查询出来前四个
request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(4));
//发送给ES请求得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//根据响应得到桶指定brandAgg捅的信息,等号前边表示使用什么格式的查询就是用什么接收,上边是使用了terms所以这里就是用Terms接收
Terms brandAgg = response.getAggregations().get("brandAgg");
//得到里面捅的集合
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
for (Terms.Bucket bucket : buckets) {
//得到key和doc_count
System.out.println(bucket.getKeyAsString()+" : "+bucket.getDocCount());
}
}
private void handleResponse(SearchResponse response) {
// 4.解析聚合响应结果
Aggregations aggregations = response.getAggregations();
// 根据名称获得聚合结果
Terms brandTerms = aggregations.get("brand_agg");
// 获取桶
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 遍历
for (Terms.Bucket bucket : buckets) {
// 获取品牌信息
String brandName = bucket.getKeyAsString();
long count = bucket.getDocCount();
System.out.println(brandName+":"+count);
}
}
7.练习
ElasticSearch3
1.数据聚合:
聚合分桶: 就相当于MySQL中的聚合函数,可以按照某一个字段进行分类并且计算最大值平均值这些
聚合度量: 就是计算平均值 最大值 最小值
管道:
2.数据聚合DSL格式
GET /hotel/_search
{
"query": {//限定聚合条件
"range": {
"price": {
"lte": 200 // 只对200元以下的文档聚合
}
}
},
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { //给聚合起个名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选择term
"field": "brand", // 参与聚合的字段
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 20 // 希望获取的聚合结果数量
},
"aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
"score_stats": { // 聚合名称
"stats": { // 聚合类型,这里stats可以计算min、max、avg等
"field": "score" // 聚合字段,这里是score
}
}
}
}
}
}
3.RestAPI实现数据聚合:
/**
* 根据品牌分桶
* 对应es语句:
* GET /hotel/_search
* {
* "aggs":{
* "brandAgg":{
* "terms":{
* "field": "brand",
* "size": 4
* }
* }
* }
*
* }
*/
@Test
public void testAgg1() throws IOException {
//创建语义对象
SearchRequest request = new SearchRequest("hotel");
//准备DEL语句,这句话是使用AggregationBuilders创建一个名称为brandAgg的桶,按照brand字段分捅,查询出来前四个
request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(4));
//发送给ES请求得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//根据响应得到桶指定brandAgg捅的信息,等号前边表示使用什么格式的查询就是用什么接收,上边是使用了terms所以这里就是用Terms接收
Terms brandAgg = response.getAggregations().get("brandAgg");
//得到里面捅的集合
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
for (Terms.Bucket bucket : buckets) {
//得到key和doc_count
System.out.println(bucket.getKeyAsString()+" : "+bucket.getDocCount());
}
}
//解析结果
private void handleResponse(SearchResponse response) {
// 4.解析聚合响应结果
Aggregations aggregations = response.getAggregations();
// 根据名称获得聚合结果
Terms brandTerms = aggregations.get("brand_agg");
// 获取桶
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 遍历
for (Terms.Bucket bucket : buckets) {
// 获取品牌信息
String brandName = bucket.getKeyAsString();
long count = bucket.getDocCount();
System.out.println(brandName+":"+count);
}
}
4.什么是自动补全?
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
参与补全查询的字段必须是completion类型。
字段的内容一般是用来补全的多个词条形成的数组。
completion: 被其修饰的字段可以自动补全
自动补全示例:
# 自动补全查询
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "n", // 关键字
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
这样上边查出来的就是和n有关的分词信息
字段补全例子
POST /_analyze
{
"text": "如家酒店还不错",
"analyzer": "pinyin"
}
5.拼音分词器作用?
默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
elasticsearch中分词器(analyzer)的组成包含三部分:
character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
6拼音分词器分词流程
如上图,也就是说我们会先过滤,将一些符号什么的替换,然后再对过滤后的内容进行分词,分完词以后再对我们分词后的词语进行拼音分词,这样我们就可以得到中文的分词和拼音的分词
但是如果我们在使用的时候在搜索的时候用拼音+中文分词,那么就会出现如下一个例子:
当我们文档中有 文档id为1:name=“狮子” 文档2的id为2:name=“虱子”
当我们搜索时搜索 name=“狮子” 这时候我们的分词就是 狮子 sz shizi 当通过狮子查的时候查出来了文档1,但是接下来通过sz和shizi查那么就是文档2也就满足了,这样就导致了我们命名查询的狮子但是虱子也被查出来了。这是不好的,那么我们就设置当我们查询的时候就不使用拼音分词器,直接使用 ik 分词器,这样在输入狮子那么就没用拼音分词了,当我们输入的拼音,那么直接根据我们输入的拼音去匹配拼音分词
7.如何使用拼音分词器?
如何使用拼音分词器?
①下载pinyin分词器
②解压并放到elasticsearch的plugin目录
③重启即可
如何自定义分词器?
①创建索引库时,在settings中配置,可以包含三部分
②character filter
③tokenizer
④filter
拼音分词器注意事项?
为了避免搜索到同音字,搜索时不要使用拼音分词器
8.DSL如何实现自动补全功能?
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
参与补全查询的字段必须是completion类型。
字段的内容一般是用来补全的多个词条形成的数组。
completion: 被其修饰的字段可以自动补全
自动补全例子::
# 自动补全查询
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "n", // 关键字
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
//查出来的都是分词以后然后拼音分词中有n的分词的信息
9.学习案例Java自动补全代码示例
/**
* 通过搜索得到联想次词
*
* @param key 搜索框的内容
* @return
*/
@Override
public List<String> getSuggest(String key) {
//设置我们的自动补全函数名称
String suggestName = "getSuggest";
SearchRequest request = new SearchRequest("hotel");
//这个请求的条件,把我们的自动补全字段添加上,然后是前缀为传过来的key的就查出来
request.source().suggest(new SuggestBuilder().addSuggestion(
suggestName, SuggestBuilders.completionSuggestion("suggestion")
.prefix(key)
.skipDuplicates(true)
.size(10)));
//发送请求
SearchResponse response = null;
try {
response = client.search(request, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
//拆关键集合用来收集并且传给前端
List<String> result = new ArrayList();
if (!Objects.isNull(response)) {
//解析获取数据 suggestName是我们的函数名称在上边写了 String suggestName = "getSuggest";
CompletionSuggestion suggestion = response.getSuggest().getSuggestion(suggestName);
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
result = options.stream().map(value -> value.getText().string()).collect(Collectors.toList());
}
return result;
}
对应上边Java的ES的代码如下:
GET /hotel/_search
{
"suggest": {
"sugg_test": {
"text": "ru",
"completion": {
"field": "suggestion"
}
}
}
}
结果:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"sugg_test" : [
{
"text" : "ru",
"offset" : 0,
"length" : 2,
"options" : [
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "415600",
"_score" : 1.0,
"_source" : {
"address" : "三间房乡褡裢坡村青年沟西侧558号",
"brand" : "如家",
"business" : "传媒大学/管庄",
"city" : "北京",
"id" : 415600,
"location" : "39.923212, 116.560023",
"name" : "如家酒店(北京朝阳北路传媒大学褡裢坡地铁站店)",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/3NezpxNZWQMdNXibwbMkQuAZjDyJ_w200_h200_c1_t0.jpg",
"price" : 259,
"score" : 47,
"starName" : "二钻",
"suggestion" : [
"如家",
"传媒大学",
"管庄"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "416121",
"_score" : 1.0,
"_source" : {
"address" : "莲花池东路120-2号6层",
"brand" : "如家",
"business" : "北京西站/丽泽商务区",
"city" : "北京",
"id" : 416121,
"location" : "39.896449, 116.317382",
"name" : "如家酒店(北京西客站北广场店)",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/42DTRnKbiYoiGFVzrV9ZJUxNbvRo_w200_h200_c1_t0.jpg",
"price" : 275,
"score" : 43,
"starName" : "二钻",
"suggestion" : [
"如家",
"北京西站",
"丽泽商务区"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "433576",
"_score" : 1.0,
"_source" : {
"address" : "南京东路480号保安坊内",
"brand" : "如家",
"business" : "人民广场",
"city" : "上海",
"id" : 433576,
"location" : "31.236454, 121.480948",
"name" : "如家酒店(上海南京路步行街店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/BA/Cii-U13eXVaIQmdaAAWxgzdXXxEAAGRrgNIOkoABbGb143_w200_h200_c1_t0.jpg",
"price" : 379,
"score" : 44,
"starName" : "二钻",
"suggestion" : [
"如家",
"人民广场"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "434082",
"_score" : 1.0,
"_source" : {
"address" : "复兴东路260号",
"brand" : "如家",
"business" : "豫园",
"city" : "上海",
"id" : 434082,
"location" : "31.220706, 121.498769",
"name" : "如家酒店·neo(上海外滩城隍庙小南门地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/B6/Cii-U13eXLGIdHFzAAIG-5cEwDEAAGRfQNNIV0AAgcT627_w200_h200_c1_t0.jpg",
"price" : 392,
"score" : 44,
"starName" : "二钻",
"suggestion" : [
"如家",
"豫园"
]
}
},
{
"text" : "如家",
"_index" : "hotel",
"_type" : "_doc",
"_id" : "441836",
"_score" : 1.0,
"_source" : {
"address" : "西坝河东里36号",
"brand" : "如家",
"business" : "国展中心",
"city" : "北京",
"id" : 441836,
"location" : "39.966238, 116.450142",
"name" : "如家酒店(北京国展三元桥店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/39/Cii-TF3eRTGITp1UAAYIilRD7skAAGLngIuAnQABgii479_w200_h200_c1_t0.png",
"price" : 458,
"score" : 47,
"starName" : "二钻",
"suggestion" : [
"如家",
"国展中心"
]
}
}
]
}
]
}
}
10.什么是ES的数据同步?
Elasticsearch的数据来自Mysql数据库中,所以当我们的MySQL发生改变时,Elasticsearch也要跟着改变,这时候我们的es的数据就要和mysql同步了
9.ES数据同步过程?
常见的数据同步方案有三种:
同步调用
异步通知
监听binlog
10.ES数据同步过程三种方案
ES数据同步过程方案1
hotel-admin是后台,可以对酒店的价格,地址,酒店名称信息等等进行修改
hotel-demo是酒店展示给客户的页面,展示酒店的价格,位置,名称信息等等
hotel-demo对外提供接口,用来修改elasticsearch中的数据
酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口,
也就是说MySQL修改完去修改es的数据
优点:实现简单,粗暴
缺点:业务耦合度高
ES数据同步过程方案2
hotel-admin是后台,可以对酒店的价格,地址,酒店名称信息等等进行修改
hotel-demo是酒店展示给客户的页面,展示酒店的价格,位置,名称信息等等
hotel-admin对mysql数据库数据完成增、删、改后,发送MQ消息
hotel-demo监听MQ,接收到消息后完成elasticsearch数据修改
优点:低耦合,实现难度一般
缺点:依赖mq的可靠性
这个实现方式也就是使用mq进行操纵,当我们修改MySQL的服务器修改完以后会将信息发送给MQ,然后修改ES的会进行监听,当监听到了以后就进行修改es的操作
ES数据同步过程方案3
给mysql开启binlog功能
mysql完成增、删、改操作都会记录在binlog中
hotel-demo基于canal监听binlog变化,实时更新elasticsearch中的内容
也就是监听mysql,如果MySQL的数据有变化那么就直接去改变es的数据
优点:完全解除服务间耦合
缺点:开启binlog增加数据库负担、实现复杂度高
在这里使用的是第二种实现方案:使用MQ来写
ES数据同步过程方案2代码实现
用来操控ES的代码(负责监听MQ队列):
/**
* 监听增加和修改的队列
* 因为我们的ES中可以进行全量修改,当有这个id的数据的时候那么就先删除再新增,没有这个数据那么就直接新增
* 所以队列过来的id不管是新增还是修改es都可以判断如果有这个数据id那么就先删除再新增,如果没有这个数据就直接新增,所以新增和修改他俩用一个方法就行了
*
* @param id 队列中需要进行操作的id
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = MqConstants.HOTEL_INSERT_QUEUE),
exchange = @Exchange(name = MqConstants.HOTEL_EXCHANGE, type = ExchangeTypes.DIRECT),
key = MqConstants.HOTEL_INSERT_KEY
))
public void insertAndUpdate(Long id) {
if (id == null) {
return;
}
log.info("入参:{}", id);
//监听到以后拿到id去数据库查询整个数据
Hotel hotel = iHotelService.getById(id);
//因为查的mysql数据和es的数据有些不一样所以需要做转换
HotelDoc hotelDoc = new HotelDoc(hotel);
//转换为json
String hotelDocJson = JSON.toJSONString(hotelDoc);
System.out.println("hotelDocJson = " + hotelDocJson);
//发送到ES中,因为我们的ES中可以进行全量修改,当有这个id的数据的时候那么就先删除再新增,没有这个数据那么就直接新增
//创建请求语义对象 添加文档数据
IndexRequest request = new IndexRequest("hotel");
//这个新增就是PUT在es中
request.id(hotel.getId().toString()).source(hotelDocJson, XContentType.JSON);
//发送请求
try {
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
RestStatus status = response.status();
log.info("响应结果为:{}", status);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 监听删除队列
*
* @param id 队列中需要进行操作的id
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = MqConstants.HOTEL_DELETE_QUEUE),
exchange = @Exchange(name = MqConstants.HOTEL_EXCHANGE, type = ExchangeTypes.DIRECT),
key = MqConstants.HOTEL_DELETE_KEY
))
public void deleteByMqId(Long id) {
if (id == null) {
return;
}
log.info("入参:{}", id);
//先创建语义对象,直接就可以给里面写id的字段
DeleteRequest request = new DeleteRequest("hotel", id.toString());
//发送请求
try {
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
RestStatus status = response.status();
log.info("响应结果为:{}", status);
} catch (IOException e) {
e.printStackTrace();
}
用来操作MySQL的代码:
@RestController
@RequestMapping("hotel")
public class HotelController {
//注入和RabbitMQ链接
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private IHotelService hotelService;
@GetMapping("/{id}")
public Hotel queryById(@PathVariable("id") Long id) {
return hotelService.getById(id);
}
@GetMapping("/list")
public PageResult hotelList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "1") Integer size
) {
Page<Hotel> result = hotelService.page(new Page<>(page, size));
return new PageResult(result.getTotal(), result.getRecords());
}
@PostMapping
public void saveHotel(@RequestBody Hotel hotel) {
hotelService.save(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
@PutMapping()
public void updateById(@RequestBody Hotel hotel) {
if (hotel.getId() == null) {
throw new InvalidParameterException("id不能为空");
}
hotelService.updateById(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
hotelService.removeById(id);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_DELETE_KEY, id);
}
}
11.ES集群搭建过程?
12.为什么搭建集群
1:在我们es中,如果只用一台es服务的话,那么当我们同时点击量很高时会造成单点问题。
2:在我们数据量太多时, 一台es执行起来效率会变低,所以es也就设置了我们但是用from和size进行分页时,from+size<=10000 这是硬性规定
总结上边:
解决的问题是:当我们数据量太多放到一个库中就会造成es查找性能的下降和点击量太多时造成单点问题
海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点(node-集群下的一台服务器)
单点故障问题:将分片数据在不同节点备份(replica )
11.ES集群中各个节点的角色?
默认情况下,集群中的任何一个节点都同时具备上述四种角色。
但是真实的集群一定要将集群职责分离:
master节点:对CPU要求高,但是内存要求低
data节点:对CPU和内存要求都高
coordinating节点:对网络带宽、CPU要求高
master eligible节点的作用是什么?
参与集群选主
主节点可以管理集群状态、管理分片信息、处理创建和删除索引库的请求
data节点的作用是什么?
数据的CRUD
coordinator节点的作用是什么?
路由请求到其它节点
合并查询到的结果,返回给用户议,转载请附上原文出处链接及本声明。
12.什么是ES的脑裂问题?
脑裂问题:我们的节点都有机会当上主节点的,他们内部会进行算法托票,我们图中是投的es01作为主节点,当es01这个主节点死掉了,那么还有两个节点他们会再进行投票选出来一个主节点,但是这时候如果我们的es01又活了,因为他之前就是一个主节点,但是现在我们已经又选出来一个主节点了,这时候就存在了两个主节点,导致我们其他节点不知道应该听他们两个谁的
13.如何解决脑裂问题?
要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题 例如:3个节点形成的集群,选票必须超过 (3 + 1) / 2 ,也就是2票。node3得到node2和node3的选票,当选为主。node1只有自己1票,没有当选。集群中依然只有1个主节点,没有出现脑裂。
14.ES分布式集群环境下,添加文档数据的原理?
es是根据hash算法进行计算hash值来计算应该存储到什么分片中
默认是使用文档中的id
_routing默认是文档的id
算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!
例子:
1)新增一个id=1的文档
2)对id做hash运算,假如得到的是2,则应该存储到shard-2
3)shard-2的主分片在node3节点,将数据路由到node3
4)保存文档
5)同步给shard-2的副本replica-2,在node2节点
6)返回结果给coordinating-node节点
15.ES分布式集群环境下,查询文档数据的原理?
elasticsearch的查询分成两个阶段:
scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
ES分布式集群环境下,查询文档数据的原理图
Sentinel
1.Sentinel是什么?能做什么?
Sentinel是阿里巴巴开源的一款微服务流量控制组件。
下边的特征都是sentinel能做的
Sentinel 具有以下特征:
•丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
•完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
•广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
•完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等
2.描述雪崩问题?解决方案有哪些?
雪崩问题是当我们的上游访问下游的服务器时,下游的服务器出现了问题,那么上游的请求就会一直等待,导致请求过来以后在上游一直堆积,这样就会因为下游出了故障把原本没有问题的上游给拖垮了
解决方案,一共四种:
第一种超时处理:
设设置1s,如果c有1s没反应那么A就直接反应服务器异常,那么就不会让A一直等结果,这样就避免了牵连A错误
2熔断降级:
如果到达了这个阈值那么就会调用降级方法,这个降级方法自己写一个返回的数值例如:服务器忙。。。 阈值自己定义。 然后我们可以自己设置时间,熔断以后再隔一段时间就会去访问,当发现可以用了那么就解除熔断,这时候就继续使用
3舱壁模式:
一个tomcat两个服务,每个服务单独一个线程,当服务C出错了也只是他单独那个线程出了问题不会影响业务1的线程
4流量控制:
使用sentinel,请求过来以后设置一次请求几个,避免一次性多个照成把服务器冲垮
3.什么是Sentinel的资源?
4.流控模式有哪些?
直接: 当前资源请求数量达到阈值时,对当前资源做限流,默认也就是直接模式
关联: 统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
链路: 多条链路访问同一个资源,同一个资源达到阈值时,我们可以对其中的某条链路做限流
5.流控效果有哪些?
6.Feign整合Sentinel步骤?
第一步:修改application.yml文件,开启Feign的Sentinel功能
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
步骤二编写降级方法:
业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。
给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
我们使用的是当时二,代码如下:
package cn.itcast.order.fallback;
import cn.itcast.order.feign.UserFeignClient;
import cn.itcast.order.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
/**
* 说明:这个里类实现的FallbackFactory里面的泛型UserFeignClient就是我们的编写的feign的访问路径的接口,
* 这样设置的话,当我们访问到UserFeignClient这个接口中的路径出错需要熔断了,因为UserFeignClient加了fallbackFactory注解指定UserClientFallbackFactory类了
* 所以直接来这里去访问这个类中的实现重写了的访问的路径,也就是说访问UserFeignClient的queryById是出错了需要熔断,就会找UserClientFallbackFactory中
* 重写了queryById的方法
*/
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserFeignClient> {
@Override
public UserFeignClient create(Throwable throwable) {
return new UserFeignClient() {
@Override
public User queryById(Long id) {
log.info("查询用户异常");
User user = new User();
user.setUsername("查询用户异常");
System.out.println("user = " + user);
return user;
}
};
}
}
7.线程隔离的两种方案?
线程池隔离 具体解释参考第13条
信号量隔离(Sentinel默认采用) 具体解释参考第13条
8.熔断器的三种状态?
关闭 开启 半开
9.Sentinel支持的熔断规则有哪些?
10.授权规则是什么?如何设置授权规则?
11.自定义异常结果格式?
/**
* 自定义异常处理类:
*/
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
12.Sentinel规则管理模式有哪些?
13.线程池隔离和信号量隔离
线程池隔离:就是一个业务分配一定的线程池,但是要维护线程池,所以会增加服务的开销
信号量隔离:设置一个计数,比如我们设置一个计数器,我们设计计数器最大为10也就是最多允许是个访问同时进行,当进来一个线程那么这个技术就加一,当累计加到10以后那么就不允许后面再尽量访问
熔断:如果到达了这个阈值那么就会调用降级方法,这个降级方法自己写一个返回的数值例如:服务器忙。。。 阈值自己定义。 然后我们可以自己设置时间,熔断以后再隔一段时间就会去访问,当发现可以用了那么就解除熔断。sentinel是基于失败比例和慢调用比例。Hystrix是基于失败比例
Seata
1.CPA定理是什么?
2.Base理论是什么?
3.CP和AP指导思想是什么?
4.Seata结构?
TC
TM
RM
5.Seata搭建过程?
6.Seata的四种模式?
7.四种模式执行原理?
8.三种模式实现方式?
标签:服务,请求,hotel,request,public,问题,相关,response,id
From: https://www.cnblogs.com/-turing/p/17531834.html