认识微服务
SpringCloud和Dubbo是微服务方案的实现
微服务技术对比
SpringCloud 和SpringBoot版本兼容需要对应
(左侧是SpringCloud的版本,右侧SpringBoot版本。两者版本需要一一对应,否者可能出现兼容性问题)
(此笔记基于SpringCloud Hopxton.SR10和SpringBoot2.3.x进行记录)
- 微服务需要根据业务模块拆分,做到单一职责,不要重复开发相同业务
- 微服务可以将业务暴露为借口,供其它微服务使用
- 不同微服务都应该有自己独立的数据库
SpringCloud
SpringCloud快速项目搭建
父工程搭建
父工程负责控制所有微服务的统一版本依赖管理,不负责微服务业务的依赖。
-
新建一个空白maven项目(,删除src目录,因为用不上)
-
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xz.springcloud</groupId> <artifactId>springcloud002</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <!--统一版本管理--> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>2.3.9.RELEASE</spring.version> <spring-cloud.version>Hoxton.SR10</spring-cloud.version> <mysql.version>8.0.29</mysql.version> <lombok.version>1.18.24</lombok.version> </properties> <!--所有子项目再次引入此依赖jar包时则无需显式的列出版本号。 Maven会沿着父子层级向上寻找拥有dependencyManagement 元素的项目,然后使用它指定的版本号。--> <dependencyManagement> <dependencies> <!--spring boot 版本--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud 版本--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--mysql版本--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--lombok 版本--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> </dependencies> </dependencyManagement> <!--打包用的,这样每个子工程就不用写了--> <build> <!--app.jar 统一指定生成的名字,方便docker构建镜像--> <finalName>app</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <!--可以把依赖的包都打包到生成的Jar包中--> <!--不写这个生成的jar很小,且无法执行,缺少依赖--> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
Eureka
Eureka是SpringCloud的一个组件
注册中心
- Eureka分为Eureka-server服务端和Eureka-client客户端
- 每个微服务都是一个eureka-client客户端
- 每个服务启动时都会向注册中心进行注册服务,保存自己的信息
- 每个eureka客户端都会向注册中心发送心跳,感知提供者健康状况
- 消费者根据服务名称向eureka拉取提供者的信息
EurekaServer搭建
EurekaServer——注册中心
注册中心搭建
-
在父工程项目中,New——Module——新的maven项目
-
修在pom.xml配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud002</artifactId> <groupId>com.xz.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eureka-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <!--eureka-服务端--> <!--因为在父工程声明了版本,这里不用声明了--> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> </project>
-
编写Eureka服务端启动类(在eureka-server项目下编写)
package com.xz.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @author xzlyf * @date 2022/9/12 17:44 */ @EnableEurekaServer @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
-
编写Eureka服务端yml配置文件(在eureka-server项目下编写)
server: port: 8888 #服务端口 spring: application: name: eruekaserver #服务的名字(eureka本身也是一个服务,微服务之间通过名字相互调用) eureka: client: #服务的注册,指向注册中心地址,eureka本身也是一个服务,在启动时把自己也注册进注册中心 service-url: #eureka的地址信息 defaultZone: http://127.0.0.1:8888/eureka
EurekaClient搭建
使用EurekaClient把服务注册进注册中心,或者把已存在的服务进行注册
-
在父工程下搭建一个新的服务UserServer,以及编写springboot启动类和配置类等等。就是一个普通的springboot web工程。
这个启动类不需要加@EnableEurekaServer
-
pom配置文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud002</artifactId> <groupId>com.xz.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>user-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--spring boot web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--...其它业务相关依赖...--> <!--eureka client 客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> </project>
-
yml配置文件
server: port: 8080 # ... 其它业务相关配置 ... spring: application: name: userserver eureka: client: #服务的注册,指向注册中心地址 service-url: defaultZone: http://127.0.0.1:8888/eureka
-
重启服务后可以在注册中心看到服务已经注册进来了
远程调用微服务
eureka注册中心可以根据服务名拉取服务列表,然后在对服务列表做负载均衡。
也就是各个服务之间根据服务名进行调用API
每个微服务通过暴露接口来给消费者调用数据。
各个接口使用Resultful风格进行暴露
微服务之间使用RestTemplate工具进行调用外部接口
示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kBTqt7bi-1664193921860)(https://gitee.com/xzlyfcc/pic-cloudstack/raw/master/image-20220912194654739.png)]
在userserver业务中,使用RestTemplate工具远程调用order服务中的数据
使用服务名代替对方的ip端口进行请求
其中RestTemplate需要自行注入
@LoadBalanced 负载均衡,ribbon组件实现
@Bean
@LoadBalanced //开启负载均衡,如果该服务存在多台实例中,将会使用负载均衡。
public RestTemplate restTemplate() {
return new RestTemplate();
}
Ribbon
Ribbon是SpringCloud的一个组件,负责负载均衡
使用注解@LoadBalanced开启Ribbon负载均衡
当RestTemplate使用注解@LoadBalanced标记时,表明这个restTemplate发起的请求将被Ribbon拦截和处理。
@Bean
@LoadBalanced //开启负载均衡,如果该服务存在多台实例中,将会使用负载均衡。
public RestTemplate restTemplate() {
return new RestTemplate();
}
负载均衡策略
修改负载均衡策略
-
第一种方式:在微服务中注入IRule对象,并修改实现类(对应实现类查看上面负载均衡策略)
@Bean public IRule rule(){ return new RandomRule(); }
需要注意的是,这种配置方式是全局(仅这个微服务中),配置之后这个微服务下的RestTemplate负载均衡策略都会生效。
-
第二种方式:配置文件方式。指定某个微服务的负载均衡方式
例如在order-service服务中的yml配置文件,修改了user-service请求负载均衡策略
order-service.yml:# 请求userservice服务ribbon使用RandomRule策略 userservice: ribbon: NFLoadBalancerRuleClassName: com.netfilx.loadbalancer.RandomRule #负载均衡规则 # 请求cartService服务ribbon使用RandomRule策略 cartService: ribbon: NFLoadBalancerRuleClassName: com.netfilx.loadbalancer.RetryRule
饥饿加载
Ribbon默认采用懒加载,即第一次访问时才会去创建LoadBalanceClient,导致第一次请求时间过长。
而饥饿加载则会在项目启动时创建,降低第一次访问的消耗。
示例:
在需要开启饥饿加载的微服务中配置yml
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients:
- userservice #指定对xxxService(对方的服务,不是自己)这个服务饥饿加载
Nacos
SpringCloud一个组件,也是一个注册中心,但比Eureka功能更加丰富。
不仅可以作为注册中心,也可以作为配置中心
Nacos是阿里巴巴的产品
Nacos注册中心
安装指南
Nacos需要安装,并独立启动
1、Windows安装方式
前往Github下载
https://github.com/alibaba/nacos/releases
2、解压出来
3、启动
进入bin目录,执行命令
standalone 单机启动
startup.cmd -m standalone
也可以直接点击startup.cmd启动
4、启动成功
登录地址:http://192.168.7.1:8848/nacos/index.html#/login
Nacos默认端口为8848,可conf/application.properties中配置新的端口
默认登录密码都是nacos
注册服务
把服务注册进注册中心
示例:
1、在父工程的pom.xml加入依赖
加入Spring Cloud Alibaba依赖,因为Nacos是阿里巴巴的,Nacos的注册发现的依赖需要被它管理
<dependencyManagement>
<dependencies>
<!--spring boot 依赖版本管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud 依赖版本管理-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 依赖版本管理-->
<!--引入阿里巴巴的springcloud依赖即可使用nacos,因为nacos是阿里巴巴的-->
<!--因为阿里巴巴的组件是后来出的,并不在原生spring cloud dependencies依赖中,所以需要单独引入-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...其它业务代码...
</dependencies>
</dependencyManagement>
2、在需要注册的微服务pom.xml中配置nacos客户端
<dependencies>
<!--spring boot 业务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
...其它业务依赖...
</dependencies>
3、修改微服务yml配置文件
spring:
application:
name: orderservice #服务名
cloud:
nacos:
server-addr: localhost:8848 #指向nacos注册中心地址
4、前往Nacos注册中心管理面板可以发现服务已近被注册进来
5、远程调用与Eureka的一样,使用服务名和RestTemplate进行远程调用。
Eureka切换成Nacos非常简单,只需要在父工程加入springcloudAlibaba的管理依赖和在客户端注释掉Eureka的依赖即可,并且在客户端配置文件配置Nacos注册中心地址。
Nacos注册中心服务端并不需要创建一个子项目来启动,直接下载启动器,在/nacos/bin目录下独立启动Nacos注册中心即可。
集群配置
修改微服务之yml配置
spring:
cloud:
nacos:
service-addr: localhost:8848
discovery:
cluster-name: SZ #集群名字,可以自定义
集群名字可以自定义,相同名字表示微服务示例部署在同一个集群。
服务调用尽可能选择本地集群服务,跨集群调用延迟高
本地集群不可访问时,再访问其它集群。
负载均衡
配置集群后,不同地域可以通过配置Ribbon的IRule策略,让其优先访问本地集群。当本地集群出现异常时才切换至外域集群。
配置yml,与前面讲到的Ribbon相同,指定服务名并指定IRule策略
userservice:
ribbon:
NFLoadBalanceRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #这里改为NacosRule
NacosRule负载均衡策略
- 优先选择同集群服务实例列表
- 本地集群找不到提供者,才去其它集群,并且报警告
- 确定了可用实例后,再采取随机负载均衡挑选实例
权重配置
Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高
通过控制台修改权重
当权重等于0,流量将不会分配给该实例
环境隔离
- namespace用来做环境隔离
- 每个namespace都有唯一ID
- 不同namespace下的服务不可见
把服务加入命名空间,如果不加入,默认在public这个命名空间下
spring:
cloud:
nacos:
service-addr: lcoalhost:8848
discovery:
namespace: d43264f8-3bc9-4030-a280-65dd7ba97df3 #加入到自己创建的命名空间
不同命名空间的服务相互不可见,也就是说,不能相互调用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rbrznZRJ-1664193921862)(https://gitee.com/xzlyfcc/pic-cloudstack/raw/master/image-20220913174739620.png)]
临时实例
服务注册到Nacos时,可以选择注册为临时实例或非临时实例
默认所有服务注册都为临时实例
通过配置修改为非临时实例
spring:
cloud:
nacos:
discoery:
ephmeral: false #设置为非临时实例,默认为true(临时实例)
临时实例和非临时实例区别:
区别在于临时实例采用的心跳机制和eureka一致,由服务提供者向注册中心发出。
非临时实例心跳机制由注册中心发出,心跳间隔更短,但感知服务提供者挂掉了,会主动向服务消费者推送变更消息,更新服务列表缓存。
并且服务提供者挂掉后,并不会在注册中心剔除掉,转为一种等待保护状态,直至服务重启成功。在此期间该服务提供者不会收到任何流量。
Nacos注册中心与Eureka注册中心的主要区别
Nacos配置中心
Nacos不仅可以作为注册中心,还有配置中心的功能
配置发布
向微服务发布配置文件
配置内容不是填写一切内容,而是一些支持热更新的内容
读取配置
bootstrap.yml是一个引导文件,这个文件执行优先级高于application.yml
在这个文件里配置nacos地址和配置文件信息,从而在执行Application.yml前获取新的配置文件,然后进行配置文件合并
1、客户端需要配置Nacos配置管理依赖
<!-- Nacos 配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2、在服务中的resource目录下添加一个bootstrap.yml文件,用于引导
spring:
application:
name: userservice #服务名 [服务名]
profiles:
active: dev #开发环境[profile]
cloud:
nacos:
server-addr: localhost:8848
config:
file-extension: yaml #文件名后缀 [后缀名]
上面三个关键名对应统一配置发布中心的Data ID(即配置中心发布的配置文件名)
[服务名]-[profile].[后缀名]
userservice-dev.yaml
上面配置表示:从localhost:8080的nacos服务器获取名为 userservice-dev.yaml 的配置文件
3、当服务启动时,便会向Nacos读取配置文件,合并配置文件。
配置热更新
Nacos中配置文件变更后,服务无需重启服务器即可实现更新。
两种方式配置:
方式一、
在需要读取配置所在的类上加上注解:@RefreshScope
例如:
在UserController.java类上,通过@Value读取了配置的一个变量,
而此时可以通过在该变量所在的类上加上注解
而此时在配置中心发布新的配置信息可以马上更新到变量,无需重启
方式二、常用
使用注解**@ConfigurationProperties**
表示会热更新读取配置文件的 pattern.dateformat 属性,dateformat会自动注入数据
多配置优先级
微服务启动时,读取配置的优先级。
三种配置文件,优先级从高到低:
服务名-profile.yaml
服务名.yaml
本地配置.yaml
其中,服务名-profile.yaml和服务名.yaml存在Nacos配置中心,称为远端配置,
本地配置是指服务里面的application.yml配置文件。
当存在相同的配置属性时,本地配置优先级最低,远端配置(服务名-profile.yaml)优先级最高。
profile一般是指开发环境,即自定义标识,一般指dev、test、release等
示例:
当存在两个服务名一致的配置文件时,并且服务指定了profile,那就是userservice-dev.yaml的优先级最高
如果服务中的bootstarp.yml没有指定prifile,将会使用userservice.yaml这个配置,并不会读取userservice-dev.yaml
集成搭建
步骤:
- 搭建Mysql集群并初始化数据库表
- 下载解压nacos
- 修改集群配置(节点信息)、数据库配置
- 分别启动多个nacos节点
- nginx反向代理
Feign
http客户端feign,使用feign代替RestTemplate。
RestTemplate方式远程调用存在的问题:
- 代码可读性差
- 参数复杂难以维护
Feign是一个声明式的http客户端,其作用就是帮助我们优雅的实现http请求的发送。
并且Feign集成了负载均衡(Ribbon实现)的功能。
基本使用
使用步骤:
- 引入依赖
- 添加@EnableFeignClients注解
- 编写FeignClient接口
- 使用FeignClient中定义的方法代替RestTemplate
使用教程:
1、在服务中引入依赖
<!--Feign客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、在服务启动类上开启Feign功能
@EnableFeignClients //开启Feign功能
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
3、声明一个远程调用,创建远程调用客户端
定义一个接口,使用@FeignClient注解表示将调用orderservice服务中的接口,
@GetMapping是SpringMVC的注解,表示该接口使用Get方式请求,对应PostMapping POST请求等等。
Feign会自动封装返回的数据类型
package com.xz.userserver.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.Map;
@FeignClient("orderservice") //调用orderservice服务的接口,
public interface OrderClient {
//调用orderservice服务中的query接口
//使用RestFul风格进行传参
//Feign会自动封装返回数据的类型,数据字段一一对应就好了。
@GetMapping("/order/query/{id}")
Map<String,Object> query(@PathVariable("id") String id);
}
4、使用Feign客户端
在需要远程调用的业务上,直接注入OrderClient即可调用
@Service
public class UserService {
@Autowired
private OrderClient orderClient;
public Object query(String id){
return orderClient.query(id);
}
}
自定义配置
Feign运行自定义配置来覆盖默认配置
配置日志
日志级别:NONE、BASIC、HEADERS、FULL
方式一、配置文件修改
feign:
client:
config:
default: #default表示全局,指定服务名则是针对某个服务生效
loggerLevel: FULL #日志级别
方式二、配置类修改
定义一个配置类,但是不需要加@Configurable注解
public class FeignLoggerConfig{
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;//日志级别
}
}
如果全局生效,在@EnableFeignClients注解(启动类)上修改。
@EnableFeignClients(defaultConfiguration = FeignLoggerConfig.class)
如果要局部生效,在需要生效的FeignClient客户端注解上修改
@FeignClient(value="userservice",configuration = FeignLoggerConfig.class)
性能优化
日志级别最好使用NONE或BASIC,否则影响性能
Feign底层客户端默认是使用JDK自带的URLConnection实现,不支持连接池。
可以通过修改换成Apache HttpClient 或 OkHttp,这两者都支持连接池。
使用HttpClient作为底层请求
1、引入依赖
<!--httpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2、配置文件
feign:
client:
config:
default: #default表示全局,指定服务名则是针对某个服务生效
loggerLevel: FULL #日志级别
httpclient:
enable: true #开启feign对HttpClient的支持
max-connections: 200 #最大连接数
max-connections-per-route: 50 #每个路径最大连接数
网关Gateway
网关功能:
- 身份认证和权限验证
- 服务路由、负载均衡
- 请求限流
SpringCloud提供了两种网关实现,SpringCloud Gateway 和 Zuul
SpringCloud Gateway
是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
Zuul
是基于Servlet的实现,属于阻塞式编程。
SpringCloud Gateway
搭建网关
1、新建一个module,maven工程。用于搭建网关
2、引入网关相关依赖
网关也是作为一个服务,需要向Nacos注册中心进行注册,所以需要引入nacos 客户端
<dependencies>
<!-- 网关依赖-->
<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>
</dependencies>
3、编写启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4、配置yml
需要配置nacos注册中心的地址,以及配置网关路由
在网关路由里配置不同服务的路由
每一个服务都应该有一个id,并且唯一
并且需要配置路由规则predicates(断言)
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址,把网关注册进去
gateway:
routes: #网关配置
- id: user-service #路由id,自定义,唯一。指向某个服务
uri: lb://userservice #服务的地址(注册中心的服务名),使用lb开头表示开启负载均衡,也可以使用http指定服务地址
predicates: #路由断言,判断请求是否符合规则,符合就放行
- Path=/user/** #判断路径是以/user开头,符合就放行
- id: order-service #第二个服务,可以配置多个服务
uri: lb://orderservice
predicates:
- Path=/order/**
5、调用
客户端通过访问网关,不再让客户端直接访问微服务,并通过路径规则来访问不同的微服务
例如:http://localhost:10010/user/test
来访问userservice的服务中的/user/test接口
路由断言
Route Predicate Fatory 路由断言工厂
例如Path = /user/**是按照路径(Path)匹配
而Path就是其中一个断言工厂
断言工厂有很多个,默认就是使用Path断言工厂来判断
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UMC1OiMZ-1664193921864)(https://gitee.com/xzlyfcc/pic-cloudstack/raw/master/image-20220915155850650.png)]
示例:
gateway:
routes: #网关配置
- id: order-service #第二个服务
uri: lb://orderservice
predicates:
- Path=/order/**
- After=2031-01-20T17:42:47.789-07:00[Asia/Shanghai]
这个路由加了两个断言判断规则
一个是Path,表示请求路径,如果符合才能放行
另一个是After,表示是某个时间点之后,如果符合才能放行
要想访问这个服务,必须同时满足上面两个断言。
就算Path路径符合规则,但时间并不符合,也不能访问成功
断言规则可以加很多个
路由过滤器
GatewayFilter
是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
SpringCloud Gateway有31种路由过滤工厂
参看文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
示例1、
给某个服务的请求添加一个请求头,test=Helloworld
spring:
cloud:
gateway:
routes:
- id: order-service #第二个服务
uri: lb://orderservice
predicates:
- Path=/order/**
- After=2018-01-20T17:42:47.789-07:00[Asia/Shanghai]
filter:
- AddRequestHeader=test,Helloworld
其中AddRequestHeader就是一个过滤工厂,用于请求头修改。后面跟了两个参数,用逗号隔开,前面表示key,后面表示value。
示例2
上面的示例1配置是针对某个路由的,下面将演示针对所有路由
spring:
cloud:
gateway:
routes:
- id: order-service #第二个服务
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=test,Helloworld
这种配置方式可以对所有路由生效
全局过滤器
上面的过滤例子都是yml写死的,固定的。而通过全局过滤(GlobalFilter接口)来实现动态的逻辑变更
注意这种过滤方式会对所有路由进行过滤
示例
请求者用户身份验证
获取请求者的请求参数:author,判断其值是否等于admin。
1、在网关下新建一个AuthorFilter类,并实现GlobalFilter接口
@Order(1) //过滤器优先级,越小优先级越高。不写的话优先级非常低
@Component //把该类进行自动装配
public class AuthorFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求参数
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
//2.获取 “author”参数
String key = queryParams.getFirst("author");
//3.校验
if (key != null && key.equals("admin")) {
//4.放行,如果存在下一个过滤器,则跳转到下一个过滤器。根据@Order优先级
return chain.filter(exchange);
}
//5.拦截
//5.1设置返回状态码,这里UNAUTHORIZED就是401状态码,一般用于身份未验证的提示
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//5.2结束处理
return exchange.getResponse().setComplete();
}
}
2、测试
未加入author参数:http://localhost:10010/order/test
加入author参数:http://localhost:10010/order/test?author=admin
过滤器执行顺序
局部路由过滤器、defaultFilter、GlobalFilter
- order值越小,优先级越高
- 当order值一样时,顺序是 defaultFileter > 局部路由过滤器 > GlobalFilter
Zuul
Spring Cloud 提供了基于Netflix Zuul 实现的API网关组件 Spring Cloud Zuul
搭建网关
1、在父工程引入Netflix依赖管理
因为Zuul输入Netflix的组件,前面并没有引入Netflix依赖管理,现在需要引入
<!--spring cloud Netflix 版本依赖管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-dependencies</artifactId>
<version>${spring-cloud-netflix.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
2、新建一个module,作为zuul网关模块
3、引入spring cloud zuul依赖
<dependencies>
<!--zuul 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--nacos 客户端 把网关注册进注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
4、新建启动类
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
5、配置yml,配置路由规则和nacos服务注册等
server:
port: 10011 #网关地址
spring:
application:
name: zuul #服务名称
cloud:
nacos:
server-addr: localhost:8848 #注册中心地址,把该服务注册进注册中心
## 配置zuul的路由规则
## 访问路由网关/api-a/**,则将请求分发到对应的serviceId: userservice
## 访问路由网关/api-v/**,则将请求分发到对应的serviceId: orderservice
zuul:
routes:
user-service: #路由名字,自定义
path: /api-a/** #路由路径 /api-a是自定义的,后面**是匹配规则
serviceId: userservice #指向的服务名
order-service:
path: /api-v/**
serviceId: orderservice
通过访问:http://localhost:10011/api-v/order/test 可以访问到orderservice服务中的/order/test接口
路由配置方式
1、上面已经提到一种配置方式,同时指定未付的serviceId和路径
zuul:
routes:
user-service: #路由名字,自定义
path: /api-a/** #路由路径 /api-a是自定义的,后面**是匹配规则
serviceId: userservice #指向的服务名
2、指向单实例服务URL和路径
这种方式不用指定服务名,但是指向固定的服务地址
访问url为:http://localhost:10011/api-a/user/test
zuul:
routes:
user-service:
url: http://localhost:8080
path: /api-a/**
3、简洁配置
这里userservice指向微服务的名称,/user/**表示访问路径
访问url为:
http://localhost:10011/user/user/test
http://localhost:10011/api-b/test
zuul:
routes:
userservice: /user/**
orderservice: /api-b/**
4、路由前缀配置
这种配置会给所有路由加上前缀/api
访问url为:http://localhost:10011/api/api-v/
zuul:
prefix: /api
routes:
userservice: /api-a/**
5、本地跳转
例如在zuul网关项目中存在一个接口/local,通过forward进行跳至zuul网关项目中进行处理
下面例子表示处理路径/api-b/**的路径跳转至本地的local方法
访问url为:http://localhost:10010/api-b/local/test
zuul:
routes:
custom-name:
path: /api-b/**
url:forward: /local
@RestController
public class GateWayTestController {
@RequestMapping("/local/test")
public String serverInfo() {
return "这里是zuul-gateway";
}
}
忽略服务
在默认不配置任何路由的情况下, zuul都可以通过***(网关:端口/服务名/接口)来访问服务。
这可能会使得服务不那么安全,所有需要通过配置忽略服务*
例如:在没有配置userservice服务和没有忽略该服务时,可以通过http://localhost:10011/userservice/user/test来访问到该服务
1、忽略所有服务(一般用这个用得多)
使用ignored-services :*来忽略所有服务,并指定一个路由跳转值orderservice
这样除了orderservice都不能通过网关来访问了
访问orderservice的url为:http://localhost:10011/api-v/order/test
zuul:
ignored-services: '*' # 使用'*'可忽略所有微服务
routes:
orderservice: /api-v/**
2、忽略单个或多个服务
zuul:
ignored-services: userservice,orderservice,...
过滤器
Zuul允许开发者在API 网关上通过定义过滤器来实现对请求的拦截与过滤,实现方法比较简单,只需要继承ZuulFilter抽象类并实现抽象方法即可
1、编写过滤器
在zuul项目下新建一个类,集成ZuulFilter实现抽象方法
方法介绍:
- filterType() 过滤器的类型,决定过滤器什么时候执行
- filterOrder() 相同过滤器类型执行的优先级,越小越优
- shouldFilter() 该过滤器的开关,决定该过滤器是否执行
- run() 过滤器业务逻辑处理
package com.xz.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
/**
* @author xzlyf
* @date 2022/9/16 17:07
*/
public class AuthorFilter extends ZuulFilter {
/**
* 返回该过滤器的类型,决定了该过滤器什么时候执行
*
* @return pre - 前置过滤器,在请求被路由前执行,通常用于处理身份认证,日志记录等;
* route - 在路由执行后,服务调用前被调用;
* error - 任意一个filter发生异常的时候执行或远程服务调用没有反馈的时候执行(超时),通常用于处理异常;
* post - 在route或error执行后被调用,一般用于收集服务信息,统计服务性能指标等,也可以对response结果做特殊处理
*/
@Override
public String filterType() {
return "pre";
}
/**
* 同类型过滤器执行优先级
* 返回值越小,执行顺序越优先
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 返回true执行,返回false 不执行。
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器的具体业务逻辑。
*/
@Override
public Object run() throws ZuulException {
//1、获取请求体
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//2、获取请求参数author
String author = request.getParameter("author");
//3、身份校验
if (author != null && author.equals("admin")) {
//4、通过
return null;
}
//5、失败,拦截
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
return null;
}
}
2、把过滤器注入到Spring容器,交给spring管理即可
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
/**
* 注入过滤器到容器
*/
@Bean
public AuthorFilter authorFilter() {
return new AuthorFilter();
}
}
3、测试
当开启过滤器后,需要携带参数author=admin才能正常访问到服务,注意该过滤器会对所有服务生效
访问url为:http://localhost:10011/api-v/order/test?author=admin
若不携带身份参数,便会返回http 401错误代码
Docker
Docker学习文档参考:https://blog.csdn.net/weixin_43418331/article/details/125352253
构建JDK环境镜像
基于Centos7系统,构建一个jdk8环境的镜像
准备一个jdk8安装包:https://www.oracle.com/java/technologies/downloads/#java8
1、将jdk8.tar.gz上传至docekr环境服务器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DfQQy7ga-1664193921866)(https://gitee.com/xzlyfcc/pic-cloudstack/raw/master/image-20220917152752604.png)]
2、编写dockerfile文件
# 指定基础镜像
FROM centos:7
# 配置环境变量,jdk安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk安装包
COPY ./jdk-8u341-linux-x64.tar.gz $JAVA_DIR
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk-8u341-linux-x64.tar.gz \
&& mv ./jdk1.8.0_341 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
3、构建镜像
docker build -f dockerfile -t jdk8:1.0 .
# 构建成功
Successfully built 0cdc681f0691
Successfully tagged jdk8:1.0
4、启动容器
# 前台启动
docker run -it --name java8 jdk8:1.0 /bin/bash
构建SpringBoot镜像
选择官方的java:8作为基础镜像,当然也可以选择上面自己创建jdk8镜像来作为底包
1、打包一个可以执行的springboot项目包
2、编写dockerfile文件
# 指定刚才生成的镜像作为底包
# 使用官方镜像java8作为底包
FROM java:8
# 复制项目jar到/tmp目录下,名为app.jar
COPY ./test-service-1.0-SNAPSHOT.jar /tmp/app.jar
# 暴露项目端口8080
EXPOSE 8080
# 启动容器时同时启动项目
ENTRYPOINT ["java","-jar","/tmp/app.jar"]
3、构建项目
docker build -f dockerfile -t app:1.0 .
# 构建成功
Successfully built 904ef8269735
Successfully tagged app:1.0
4、启动项目
# 前台启动容器
docker run -it -p 8080:8080 --name app app:1.0 /bin/bash
5、通过访问宿主机8080端口进行访问项目
Docker Compose
Docker Compose 可以基于Compose文件帮我们快速的部署分布式应用,无需手动一个个创建和运行容器
Compose文件是一个yml格式的文件
安装
1、前往docker github 下载linux 版本的compose文件
https://github.com/docker/compose#linux
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R64FVXH3-1664193921867)(https://gitee.com/xzlyfcc/pic-cloudstack/raw/master/image-20220917164255943.png)]
2、将compose文件上传至服务器
存放目录为:/usr/local/bin
3、修改文件名为docker-compose
mv docker-compose-linux-x86_64 docker-compose
4、给compose加上可执行权限
chmod +x docker-compose
5、测试是否可用
# 查看docker-compose 版本
[root@localhost bin]# ./docker-compose -v
Docker Compose version v2.10.2
6、加入环境变量,这样不用每次进入该目录执行命令
# 修改环境变量文件
vi ~/.bash_profile
# 加入
PATH=$PATH:$HOME/bin:$/usr/local/bin
# 是环境变量生效
source ~/.bash_profile
配置文件详解
Compose 文件格式有3个版本,分别为1,2.x,3.x,目前主流版本为3.x
docker-compose.yml示例参考
version: "3.2" #docker-compose版本
# 需要创建容器的列表
services:
nacos: # 创建一个名为nacos的容器
image: nacos/nacos-server #使用nacos/nacos-server:laster镜像创建
environment: #环境变量配置
Mode: standalone #nacos的环境变量,单机启动模式
ports: #端口的映射
- "8848:8848"
mysql: # 创建第二个容器,名为mysql
image: mysql:5.7.25 #使用mysql:5.7.25这个镜像创建
environment: #mysql的环境变量配置,root的密码配置
MYSQL_ROOT_PASSWORD: 123456
volumes: #数据卷映射,$PWD是当前compose文件执行的路径,当然也可以写死。
- "$PWD/mysql/data:/var/lib/mysql" #配置mysql数据的映射
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice: #创建第三个容器,自己写的微服务
build: ./user-service #build命令表示当前还没有镜像,需要根据./user-service/dockerfile 构建文件进行构建镜像。构建镜像完成后便自动创建容器
orderservice: #第四个容器...
build: ./order-service
gateway: #第五个容器
build: ./gateway
ports: #端口映射
- "10010:10010"
上面使用compose文件批量构建镜像和创建容器。
除了nacos和gateway容器暴露端口,其余微服务都是以内部网络进行访问,提高安全性
build命令可以根据dockerfile构建文件构建镜像,并自动创建容器启动。
有进行的服务直接通过image命令创建容器
加上dockerfile文件使用
FROM java:8
COPY ./app.jar /tmp/app.jar
ENTRYPOINT ["java","-jar","/tmp/app.jar"]
项目结构
每个微服务文件夹都包含了一个dockerfile和app.jar文件
通过父工程批量打包app.jar
在父工程的pom统一修改打包的名字
把项目上传至服务执行
进入到docker-compose文件所在的目录
# 开启编排启动
docker-compose up -d
MQ异步通信
同步调用
优点:
- 时效性强,可以立即得到结果
缺点:
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失败的问题
异步调用
优点:
- 耦合度低
- 吞吐量提升
- 故障隔离
- 流量削峰
缺点:
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
什么是MQ
MQ(MessageQueue),即消息队列。也就是事件驱动架构中的Broker。
MQ的实现方案
市面上MQ的实现方案有很多,下面列举了几个常用的方案,以及区别
RabbitMQ
RabbitMQ是基于Erlang语言开发的开源小兮通信中间件,官网:https://www.rabbitmq.com/
安装
Docker单机部署RabbitMQ
1、使用docker pull拉取rabbitmq官方镜像,以3.9版本为例
docker pull rabbitmq:3.9
2、启动容器
docker run \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3.9
#解释:
#RABBITMQ_DEFAULT_USER 后台登录用户名
#RABBITMQ_DEFAULT_PASS 后台登陆密码
#--name 容器名字
#--hostname mq1 用于集群配置的主机名
#-p 15672 rabbit后台管理页面的端口
#-p 5672 消息通信的端口
#-d 后台启动
3、正常进入15672后台管理页面
访问路径为:http://192.168.7.10:15672
常见问题处理
-
无法访问15672后台管理页面
#1、进入mq容器 docker exec -it mq /bin/bash #2、开启web界面管理插件命令 rabbitmq-plugins enable rabbitmq_management
-
Stats in management UI are disabled on this node
#1、进入mq容器 docker exec -it mq /bin/bash #2、cd到路径 cd /etc/rabbitmq/conf.d/ #3、修改 management_agent.disable_metrics_collector = false echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf #4、退出容器 exit #5、重启rabbitmq容器 docker restart {rabbitmq容器id或容器名称}
RabbitMQ结构
publisher 消息发布者
consumer 消息消费者
exchange 路由消息到队列
queue 消息队列,接收路由的消息,用于缓存消息,
virtual host 虚拟主机,对exchange、queue等资源进行隔离分组
5种消息模型
官方案例:https://www.rabbitmq.com/getstarted.html
1、简单模式
基本消息模型就是:
一个生产者丶默认交换机丶一个队列丶一个消费者。
2、工作模式
work消息模型就是:
一个生产者丶默认交换机丶一个队列丶多个消费者。
3、发布订阅模式
fanout消息模型就是:
多个消费者,每一个消费这都有自己的队列,每个队列都绑定到交换机
生产者发送消息到交换机-交换机发送到哪个队列
4、 路由模式
Routing路由模式模型就是:
在某种场景下,我们希望不同的消息被不同的队列消费
这个时候我们就要用到direct类型的exchange
生产者向交换机发送消息—交换机根据路由key发送给队列-队列的消费者接收消息
5、Topics主题模式
它可以通过RoutingKey,将交换机和队列机进行模糊匹配
简单模式案例演示
1、项目结构
publisher 消息发布者
consumer 消息消费者
2、父工程引入rabbitMQ client 依赖
<!--dependencyManagement父工程对子工程依赖版本管理,子工程无须指定版本-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.16.0</version>
</dependency>
</dependencies>
</dependencyManagement>
3、publisher.Send 发布消息代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.nio.charset.StandardCharsets;
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
//1、建立连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、rabbitmq服务地址
factory.setHost("192.168.7.10");
//3、rabbitmq服务端口
factory.setPort(5672);
//4、使用的主机(主机隔离消息队列)
factory.setVirtualHost("testVM");
//5、操作主机的用户(该用户需要有testVM主机的操作权限)
factory.setUsername("test");
factory.setPassword("123456");
//6、开始连接
try (Connection connection = factory.newConnection();
//7、创建消息队列,队列名为hello
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
//8、发送消息至队列,结束业务
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
4、consumer.Recv 接收消息队列代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.nio.charset.StandardCharsets;
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
//1、建立连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、rabbitmq服务地址
factory.setHost("192.168.7.10");
//3、rabbitmq服务端口
factory.setPort(5672);
//4、使用的主机(主机隔离消息队列)
factory.setVirtualHost("testVM");
//5、操作主机的用户(该用户需要有testVM主机的操作权限)
factory.setUsername("test");
factory.setPassword("123456");
//6、开始连接
Connection connection = factory.newConnection();
//7、创建通道(如果通道不存在则执行创建,若存在则跳过
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//8、等待消息到达
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//9、接收消息
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
Spring AMQP
可以看到,RabbitMQ官方的API操作消息发送和接收非常繁琐。那么通过Spring AMQP 可以大大简化消息发送和接收的API
AMQP——Advanced Message Queueing Protocol。
是一种规范来的。用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关。
Spring AMQP 是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。底层则是通过spring-rabbit来实现
简单模式案例演示
对比上面使用官方API发送消息和接收消息,代码简化了不少
1、项目结构
2、在父工程引入amqp的依赖,这样子工程就不用引入了
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.3</version>
</dependency>
<!--AMQP的依赖,springboot 自动装配-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.3</version>
</dependency>
</dependencies>
3、application.yml配置rabbitmq通信地址(两个子项目都要配置)
spring:
rabbitmq:
host: 192.168.7.10 # RabbitMQ 服务地址
port: 5672 # RabbitMQ 服务端口
virtual-host: testVM # 主机,隔离不同消息队列
username: test #主机操作用户
password: 123456
4、Publisher.Send代码
rabbitTemplate.convertAndSend(queueName, message);这一行代码是关键,它可以自动把要发送的消息转换成字节码,发送到指定的消息队列。使用前只要注入RabbitTemplate工具即可
package com.xz.publisher.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SendController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/")
public Object index() {
String queueName = "hello";
String message = "hello:" + System.currentTimeMillis() + "";
rabbitTemplate.convertAndSend(queueName, message);
return "Ok";
}
}
5、Consumer.Recv代码
通过使用rabbitTemplate.receiveAndConvert(queueName);来接收消息队列的数据,并自动转换
注意:这里演示的手动接收消息,实际开发中一般使用监听,监听到消息自动处理
package com.xz.consumer.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RecvController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/")
public Object recv() {
String queueName = "hello";
String recv = (String) rabbitTemplate.receiveAndConvert(queueName);
return recv;
}
}
可以看到,通过spring amqp来处理发送消息和接收消息非常的简单。不用手动创建连接工厂,通过配置yml,让spring自动装配。
监听消息队列
实际开发种,一般使用监听消息来自动触发业务。
1、在项目中新建一个类,编写监听器
package com.xz.consumer.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component //注册为一个组件,归sping管理
public class RabbitQueueListener {
//queues 指定队列名
//String msg 发送消息时是什么格式,接收时也是什么格式
@RabbitListener(queues = "hello")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("接收到[hello]队列消息:" + msg);
}
}
这样不用手动接收了,当有队列里有消息,将会执行此方法。若队列存在多条消息,将按顺序一条条执行,直至处理完所有消息。
消费预取限制
当一个队列种存在多个消费者,就是5种消息模型种的工作模式。
rabbitMQ默认消费者可以预取很多消息,这会导致性能低的机器拿了很多消息在慢慢处理,性能高的机器处理完了所有消息在干等待。
解决这种问题就是限制预取
在yml种配置预取限制为1,表示每个消费者只能预取1个消息。
spring:
rabbitmq:
host: xxx
port: 5672
virtual-host: /
username: xxx
password: xxx
listener:
simple:
prefetch: 1 #每次只能取一条消息,处理完才能获取下一个消息。
发布、订阅
上面的例子都是发布者直接发送消息到消息队列。
下面的是发布者发送消息到exchange(交换机),由交换机决定消息发送给哪个消息队列。
注意:交换机负责消息路由,而不是存储,路由失败则消息丢失。
常见的exchange类型:
- Fanout:广播
- Direct:路由
- Topic:话题
Fanout Exchange
会将接收到的消息,路由给每一个跟其绑定的queue
演示:
1、在consumer消费者的项目种加入一个配置类,用于配置FanoutExchagne交换机
下面演示了声明一个fanout交换机,和两个queue队列。
两个队列绑定同一个交换机。
绑定工作都交给sping管理
这是其中一种声明交换机和队列的方式,还有一种方式是通过@RabbitListener来声明
配置式声明交换机和队列
package com.xz.consumer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
@Configuration
public class FanoutExConfig {
/*声明一个fanout exchange 交换机,名为:hello.exchange*/
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("hello.exchange");
}
/*声明一个队列,名为hello.queue1*/
@Bean
public Queue queue1() {
return new Queue("hello.queue1");
}
/*将队列1绑定到交换机,spring会将刚才创建的交换机和队列绑定
@Bean
public Binding bindingQueue1(FanoutExchange fanoutExchange, Queue queue1) {
return BindingBuilder.bind(queue1).to(fanoutExchange);
}
/*声明第二个队列,名为hello.queue2*/
@Bean
public Queue queue2() {
return new Queue("hello.queue2");
}
/*将队列2绑定到交换机,它们绑定的是同一个交换机*/
@Bean
public Binding bindingQueue2(FanoutExchange fanoutExchange, Queue queue2) {
return BindingBuilder.bind(queue2).to(fanoutExchange);
}
}
2、在consumer消费者中监听两个队列测试
package com.xz.consumer.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitQueueListener {
/*监听第一个队列*/
@RabbitListener(queues = "hello.queue1")
public void listenerFanoutQueue1(String msg) throws InterruptedException{
System.out.println("在queue1队列中收到消息:"+msg);
}
/*监听第二个队列*/
@RabbitListener(queues = "hello.queue2")
public void listenerFanoutQueue2(String msg) throws InterruptedException{
System.out.println("在queue2队列中收到消息:"+msg);
}
}
3、在publisher提供者中发送消息。
更以前的简单模式略有不同,这里是将消息发送至交换机,而不是队列了
@GetMapping("/fanout")
public Object sendFanout() {
// 交换机名称
String exchangeName = "hello.exchange";
// 消息
String msg = LocalTime.now().toString();
//发送消息。交换机名称、RoutingKey(暂空)、消息
rabbitTemplate.convertAndSend(exchangeName, "", msg);
return "Ok";
}
4、测试
# 结果
在queue2队列中收到消息:19:36:17.371
在queue1队列中收到消息:19:36:17.371
当消息提供者发送了一次消息,两个消息队列都能监听到消息,并接收。
两个消息队列都会读取到消息。
Direct Exchange
会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式
- 每一个Queue都与Exchang设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchang将消息路由到BindingKey与消息RoutingKey一致的队列
演示:
1、直接使用@RabbitListener监听以及声明交换机和队列
下面通过注解直接声明交换机和队列,不再通过配置声明,代码减少了
注解声明交换机和队列
package com.xz.consumer.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.security.Key;
@Component
public class RabbitQueueListener {
/**
* 声明监听一个队列
* bindings:绑定
* exchange:交换机,和交换机类型
* value:队列
* key:路由关键值,可以接收多个,交换机根据此关键字转发到此队列
*/
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = "hello.direct",type = ExchangeTypes.DIRECT),
value = @Queue("direct.queue1"),
key = {"keyA","keyB"}
)
)
public void listenerDirectQueue1(String msg){
System.out.println("接收到队列1的消息:"+msg);
}
/**
* 声明监听第二个队列
*/
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = "hello.direct",type = ExchangeTypes.DIRECT),
value = @Queue("direct.queue2"),
key = {"keyA","keyC"}
)
)
public void listenerDirectQueue2(String msg){
System.out.println("接收到队列2的消息:"+msg);
}
}
2、publisher消息提供者
只需要指定路由key即可将消息发送到和绑定key一致的队列
@GetMapping("/direct")
public Object sendDirect(){
// 交换机名称
String exchangeName = "hello.direct";
// 消息
String msg = "1234567890---=";
//发送消息
rabbitTemplate.convertAndSend(exchangeName, "keyC", msg);
return "Ok";
}
3、测试
当路由key为keyB时,direct.queue1队列可以收到消息
当路由key为keyC时,direct.queue2队列可以收到消息
当路由key为keyA时,direct.queue1、direct.queue2都可以收到消息
Topic Exchange
与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并已 .(点)分割,
例如:
work.hot
work.hot.desc
work.hot.asce
使用通配符来决定单词数量:
每个单词使用 . 来分割
# : 表示0个或多个单词
* : 表示一个单词
示例:
new.#
可以匹配
new.a
new.rabbay.hot
new.rabbay.random
#.new
可以匹配
hot.new
desc.hot.new
new.*
可以匹配
new.a
*.new
可以匹配
hot.new
声明方法和使用和上面DirectExchange类似,只是routingKey值变了,这里就不掩饰了,参考上面例子。
消息序列化
SpringAMQP的消息默认是通过JDk的MessageConverter来实现序列化。也就是说,当消息提供者发送一个bean到消息队列时,它会转换成一串很长的乱码,难以阅读和维护,并且占用大量空间,和安全问题。
通过修改可以直接将bean序列化为json,大大减少空间。通过修改消息转换器为fastjson或其它第三方json序列化工具即可。这样消息消费者也可以直接获取到bean实体类了
示例
1、在父工程引入依赖,这样每个儿子都会拥有依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
2、在consumer消费者服务中,修改fastjson为转换器
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
3、发送bean实体的消息
public void testSend(){
//模拟bean实体消息
Map<String,Object> msg = new HashMap<>();
msg.put("name","abc");
msg.put("age",11);
rabbitTemplate.convertAndSend("queueName",msg);
}
4、接收bean实体消息
直接转换成bean实体
@RabbitListener(queues = "queueName")
public void listenerObjectQueue(Map<String,Object> msg ){
System.out.printLn(msg)
}
分布式搜索-ES
ES即是elasticsearech
这是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容
Elasticsearch 结合了 kibana、Logstash、Beats。它们结合起来统称elastic stack (ELK 技术栈)
其中,elasticsearch是elastic stack的核心,负责存储、搜索、分析数据
kibana负责数据可视化,logstash和beats负责数据抓取
Lucene
elasticsearch是基于Lucene作为底层开发的。
Lucene是Apache的一个开源类库,提供了搜索引擎的核心API。
正向索引
基于文档id创建索引,查询词条时必须先找到文档,而后判断是否包含词条
倒排索引
对文档内容分词,对词条分词创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档
文档和词条
每一条数据就是一个文档
对稳定中的内容分词(中文按语义分,英文按空格分),得到的词语就是词条
安装ES
1、在docker创建一个网络,让es和kibana容器可以互联
当然也可以使用docker compose集群部署,这样部署内部容器可以互联
docker network create es-net
2、拉取elseticsearch的镜像
镜像非常大,接近1G左右
基于elseticsearch:7.12.1版本讲解
docker pull elasticsearch:7.12.1
3、启动es
单点部署es
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
#参数解释:
#-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" :JVM虚拟机的内存,默认是1024m,最低不可低于512m
#-e "discovery.type=single-node" : 单点模式运行
#-v es-data : 数据挂载目录
#-v es-plugins : 插件挂载目录
#--privileged : 授予逻辑卷访问权限
#--network es-net : 使用自定义的网络,让它们容器之间网络互联
#-p 9200 : http调用端口
#-p 9300 : 各个容器互联的端口
4、测试启动是否成功
访问:http://192.168.7.10:9200/
返回:
安装kibana
kibana可以给我们提供一个elasticsearch的可视化界面
1、拉取kibana镜像
注意它们之间的版本要一致
docker pull kibana:7.12.1
2、启动kibana容器
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
#参数解释:
#-e "ELASTICSEARCH_HOSTS=http://es:9200" : 指向elasticsearch的服务地址,这里可以直接使用容器名,因为它们在同一个docker网络,直接使用容器名可以得到对方的ip
#--network es-ne : 加入到es-net网络
#-p 5601:5601 : 对外访问接口
3、进入后台页面
http://192.168.7.10:5601/
4、DevTools可以快速测试DSL语句
访问:http://192.168.7.10:5601/app/dev_tools#/console
分词器
es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入的内容进行分词。
但默认的分词规则对中文处理并不友好,会对中文文字每个切割,这样就达不到语义的效果。
可以选择使用第三方分词器:IK分词器。
项目地址:https://github.com/medcl/elasticsearch-analysis-ik
这输入一个插件,需要安装。
安装IK分词器演示:
1、离线下载插件,前往项目地址,获取最新版本插件下载
注意,版本必须和es一致
否则es会启动失败
2、将下载的插件包复制到服务器,
我们在启动elasticsearch容器时,挂载了一个es-plugins目录,该目录就是存放这种插件的
2.1、通过docker命令,到es-plugins的目录路径
docker volume inspect es-plugins
2.2、把插件包解压复制进去该目录
# 进入到 es-plugins挂载目录
[root@localhost ~]# cd /var/lib/docker/volumes/es-plugins/_data
[root@localhost _data]# ls
elasticsearch-analysis-ik-7.12.1.zip
# 解压压缩包,文件夹文ik
[root@localhost _data]# unzip elasticsearch-analysis-ik-7.12.1.zip -d ik/
# 删除压缩包
[root@localhost _data]# rm -rf elasticsearch-analysis-ik-7.12.1.zip
# 只剩下一个ik文件夹,这便是插件包了
[root@localhost _data]# ls
ik
3、重启es容器
docker restart es
4、测试分词器
进入kibana的Dev Tools页面,这是用于测试DSL语句的控制器
http://192.168.7.10:5601/app/dev_tools#/console
测试样例:
IK分词器包含两种模式
- ik_smart:最少切分
- ik_max_word :最细切分
测试结果:
可以看到,ik分词器可以语义的进行词语分割,默认的是每个汉字分割,看不出语义。
拓展分词器
如果想要的词语并没有分词成功,我们可以使用拓展分词器,把需要分词的词语加进去。
比如“奥里给”,默认ik分词器是不能切割出来的。
同样的,如果不想某些词被分出来,也可以使用停用词库的功能,比如“的,吖,呵"的语气词等,还有敏感词。把这写限制词语加入进停用词库,这样ik分词器就不会把这些切割出来。
1、修改ik分词器目录中的config目录中的IKAnalyzer.cfg.xml文件
(插件包)ik/config/IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopword.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
2、在IKAnalyzer.cfg.xml同级目录下新建ext.dic文件,用于存放拓展词库。当然你也可以指向绝对路径,指向别的文件。
ext.dic
奥里给
干了兄弟们
stopword.dic
可以看到默认stopword.dic以及存在一些英文的无意义单词
太阳
的
3、重启es容器生效
docker restart es
4、测试演示
结果:
可以看到,拓展分词器词典生效了。停用词库的”太阳“也生效了,没有显示出来。
DSL语法
创建索引库
ES中通过Restful请求操作索引、文档。
请求内容用DSL语句来表示。
使用PUT请求创建一个索引库
PUT /索引库名称
{
"mappings":{
"properties":{
"字段名":{
"type":"text"
"analyzer":"ik_smart"
},
"字段名2":{
"type":"keyword",
"index":"false"
},
"字段名3":{
"properties":{
"子字段":{
"type":"keyword"
}
}
}
//...略
}
}
}
解释:
type:字段数据类型。text(可分词的文本)、keyword(精确词语,不参与分割,如品牌名,国家,ip地址等)。还有常见的long、integer、double等数值。还有boolean布尔值。还有日期data。还有对象object。
PUT: Restful风格请求模式。获取则用GET。删除则用DELETE。
index:是否创建索引,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段
示例:
# 创建一个索引库
PUT /firstindex
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": "false"
},
"name": {
"properties": {
"firstname": {
"type": "keyword"
},
"lastname": {
"type": "keyword"
}
}
}
}
}
}
结果:
创建成功
创建一个索引库时,主要考虑字段的名字和字段的类型。然后考虑要不要分词,要就是type属性,不需要分词就是keyword属性。如果要分词就考虑分词器是什么。如果不参与搜索就用关闭索引index: false。如果想同时根据多个字段搜索就使用copy_to。
查询索引库
使用GET请求
GEt /索引库名
# 示例
GET /firstindex
删除索引库
使用DELETE请求
DELETE /索引库名
# 示例
DELETE /firstindex
修改索引库
一般索引库创建成功后不能修改,不能修改已经存在的字段。
但是可以新增字段
PUT /索引库名/_mappin
{
"properties":{
"新字段名":{
"type":类型
}
}
}
新增文档
使用POST请求
如果不指定文档id,es会随机生成一个
POST /索引库名/_doc/文档id
{
"字段1":"值1",
"字段2":"值2",
"字段3":{
"子属性1":"值1",
"子属下2":"值2"
}
}
查询文档
使用GET请求
GET /索引库名/_doc/文档id
# 批量查询文档
GET /索引库名/_search
删除文档
使用DELETE请求
DELETE /索引库名/_doc/文档id
修改文档
方法1:
全量修改。如果文档id存在,则修改;如果文档id不存在,则新增。
PUT /索引库名/_doc/文档id
{
"字段1":"值1",
"字段2":"值2"
}
方法2:
局部修改,只修改部分字段的值
POST /索引库名/_update/文档id
{
"name":{
"firstname":"haha"
}
}
RestClient
ES官方提供了各种不同语言的客户端,用来操作ES。
这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
1、导入依赖
注意:版本要和上面创建的ES版本保持一致
**注意2:**如果项目引入了spring-boot-dependencies依赖管理,那么es的版本需要强制指定,因为spring-boot-dependencies依赖管理里,默认指定了别的版本的es
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
强制指定es版本
强制指定后dependency里面就不用指定版本了
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
2、初始化RestClient对象
下面再单元测试里演示,操作都是基于RestHighLevelClient客户端实现
public class FandouIndexTest {
private RestHighLevelClient client;
/**
* 初始化操作,在启动单元测试前初始化该方法
*/
@BeforeEach
void init() {
client = new RestHighLevelClient(RestClient.builder(
//如果需要集群,继续往下写即可
HttpHost.create("http://192.168.7.10:9200")
//, HttpHost.create("http://192.168.7.11:9200")
//, HttpHost.create("http://192.168.7.12:9200")
));
}
/**
* 执行完单元测试后销毁client
* 注意client需要close
*/
@AfterEach
void end() throws IOException {
this.client.close();
}
}
创建索引库
注意:CreateIndexRequest,导包需要导这个:org.elasticsearch.client.indices.CreateIndexRequest
其中,indices()该方法负责操作索引库
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class FandouIndexTest {
//...略
/**
* 创建索引的dsl语句
*/
public static final String DSL_INDEX_CREATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"sex\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"degree\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"occupation\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"email\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"tel\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"university\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
@Test
void createIndex() throws IOException {
//1、创建request对象,并指定索引名
CreateIndexRequest request = new CreateIndexRequest("fandou");
//2、请求参数。传入DSl语句,内容类型是json
request.source(DSL_INDEX_CREATE, XContentType.JSON);
//3、发起请求
client.indices().create(request, RequestOptions.DEFAULT);
}
}
删除索引库
@Test
void deleteIndex() throws IOException {
//1、创建request对象,指定索引名
DeleteIndexRequest request = new DeleteIndexRequest("fandou");
//2、发起请求
client.indices().delete(request,RequestOptions.DEFAULT);
}
判断索引库存在
true表示存在,false表示不存在
@Test
void isExistsIndex() throws IOException {
//1、创建request对象,指定索引名
GetIndexRequest request = new GetIndexRequest("fandou");
//2、发起请求,接收返回值
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
新增文档
@Test
public void createDoc() throws IOException {
//从数据库获取bean对象
Tenant tenant =tenantService.findById(1L);
//1、准备request对象,传入索引库名,以及指定文档id
IndexRequest request = new IndexRequest("fandou").id(tenant.getId().toString());
//2、准备DSL语句,json格式的文档。序列化bean得到json
request.source(JSON.toJSONString(tenant), XContentType.JSON);
//3、发送请求
client.index(request, RequestOptions.DEFAULT);
}
查询文档
@Test
public void getDoc() throws IOException {
//1.准备request对象
GetRequest request = new GetRequest("fandou", "1");
//2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//3.解析响应结果
String json = response.getSourceAsString();
Tenant tenant = JSON.parseObject(json,Tenant.class);
System.out.println(tenant);
}
修改文档
与DSL语法一样,修改文档数据有两种方式:
全量更新:再次写入id一样的文档,就会删除就文档,添加新文档
局部更新:只更新部分字段
这里演示局部更新,注意参数之间时间逗号隔开,没有冒号
@Test
public void updateDoc() throws IOException {
//1.准备request对象
UpdateRequest request = new UpdateRequest("fandou","1");
//2.准备参数
request.doc(
"name","梁忆萍",
"degree","小学"
);
//3.发送请求
client.update(request,RequestOptions.DEFAULT);
}
删除文档
@Test
public void deleteDoc() throws IOException {
//1.准备request对象
DeleteRequest request = new DeleteRequest("fandou","1");
//2.发送请求
client.delete(request,RequestOptions.DEFAULT);
}
批量操作
通过使用BulkRequest来做批量操作
bulkRequest提供了一个add()方法,用来接收Index、delete、updateRequest的请求。
索引bulkRequest可以做批量删除、批量新增、批量更新等操作
下面演示批量新增文档到es
@Test
public void batch() throws IOException {
//1.创建bulk对象
BulkRequest request = new BulkRequest();
//2.添加批量请求。
request.add(new IndexRequest("fandou").id("").source("json", XContentType.JSON));
request.add(new IndexRequest("fandou").id("").source("json", XContentType.JSON));
request.add(new IndexRequest("fandou").id("").source("json", XContentType.JSON));
request.add(new IndexRequest("fandou").id("").source("json", XContentType.JSON));
//3.发起请求
client.bulk(request,RequestOptions.DEFAULT);
}
DSL高级语法
查询条件
- 查询所有:查询出所有数据,match_all
- 全文检索查询:利用分词器对用户输入的内容分词,然后去倒排索引库中匹配。
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型
- ids
- range
- tern
- 地理查询(geo):根据经纬度查询
- gep_distance
- geo_bounding_box
- 符合查询:符合查询可以将上诉各种查询条件组合起来,合并查询
- bool
- funcation_score
基本语法
GET /索引库名/_search
{
"query":{
"查询类型":{
"查询条件":"条件值“
}
}
}
全文检索查询
全文检索查询,**会对用户输入的内容分词,**常用于搜索框搜索
match查询:全文检索查询的一种,会对用户输入的内容分词,到后去索引库查询
语法:
GET /索引库名/_search
{
"query":{
"match":{
"FIELD":"text"
}
}
}
示例:
查询字段university,
GET /fandou/_search
{
"query":{
"match": {
"university": "深圳信息"
}
}
}
multi_match:与match查询类似,只不过允许同时查询多个字段
语法:
GET /索引库名/_search
{
"query":{
"multi_match":{
"query":"text",
"fields":["FIELD1","FIELD2"]
}
}
}
示例:
查询多个字段,如果其中一个字段符合query,那就显示出来
GET /fandou/_search
{
"query":{
"multi_match":{
"query":"女性",
"fields":["name","occupation","university","sex"]
}
}
}
注意:参与搜索的字段越多,效率越低。应该把多个字段整合进一个字段all里面,进行搜索。例如在创建索引库时,使用copy_to功能,把多个字段复制进一个字段all里面,搜索的时候就直接使用all作为字段即可。
精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。
有两种查询模式:
- tern:根据词条精确查询
- range:根据值访问查询
tern精确查询演示:
GET /索引库名/_search
{
"query":{
"tern":{
"FIELD":{
"value":"VALEUE"
}
}
}
}
示例:
查询字段degree,必须等于”博士“这个值,否之不显示
GET /fandou/_search
{
"query": {
"term": {
"degree": {
"value": "博士"
}
}
}
}
range范围查询语法:
GET /索引库/_search
{
"query": {
"range": {
"FILED": {
"gte": 10,
"lte": 20
}
}
}
}
演示:
这里表示查询id字段,范围是10-20的数值
gte表示大于等于
gt表示大于
le表示小于
lte表示小于等于
GET /fandou/_search
{
"query": {
"range": {
"id": {
"gte": 10,
"lte": 20
}
}
}
}
地理查询
geo_bounding_box:查询geo_point值落在某个矩形范围的所有数据
语法:
GET /索引库/_search
{
"query":{
"geo_bounding_box":{
"FIELD":{
"top_left":{
"lat":31.1,
"lon":121.5
},
"bottom_right":{
"lat":30.9,
"lon":121.7
}
}
}
}
}
geo_distance:查询到指定中心点小于某个距离值的所有数据
常见应用常见:附近的人
语法:
这里表示以(31.21,121.5)为中心点查询范围15km的文档
GET /索引库/_search
{
"query":{
"geo_distance":{
"distance":"15km",
"FIELD":"31.21,121.5"
}
}
}
复合查询
可以将上诉的查询条件组合起来,实现更复杂的搜索逻辑
fucation score:算分函数查询,可以控制文档的分数(_score),控制文档排名
示例:
GET /hotel/_search
{
"query":{
"function_score":{
"query":{
"match":{"
"degree":"博士"
"}
},
"functions":[
{
"filter":{"term":{"id":"1"}},
"weight":10
}
],
"boost_mode":"multiply"
}
}
}
这个例子的意思是,先用match查询到dgree字段的文档,es会根据相关性自行排名一遍,然后再根据term精确匹配到id字段为1的文档,给它权重设置为10。这样id为1的文档会把原先的默认权重,乘上自定义的权重10,得到最终权重。而boost_mode表示加权模式,multiply表示两者相乘。这种应用场景常见与竞价排名
boolean query:布尔查询是一个或多个查询子句的组合。
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
示例:
可以结合多个查询条件
GET /索引库/_search
{
"query":{
"bool":{
"must":[
{"term":{"degree":"博士"}}
],
"should":[
{"term":{"sex":"女性"}}}
]
}
}
}
结果处理
排序
es支持对搜索结果排序,默认是根据相关度算分(_score)排序。可以排序字段有:keyword类型、数值类型、地理坐标类型、日期类型等。
排序方式:ASC(升序)、DESC(降序)
语法:
GET /索引库/_search
{
"query":{
"match_all":{}
},
"sort":[
{"FILED":"DESC"},
{"FILED2":"ASC"}
]
}
分页
es默认情况下只返回10条数据
自定义分页演示:(类似mysql的limit)
GET /索引库/_search
{
"query":{
“match_all":{}
},
"from":100, //分页开始的未知,默认为0
”size":10, //获取的文档总是,类似limit 100 0
}
高亮
就是在搜索结果中把搜索关键字突出显示
原理:
- 将搜索结果中的关键字用标签标记出来
- 在页面中给标片添加css样式
语法:
GET /索引名/_search
{
"query":{
"match":{
"FILED":"TEXt"
}
},
"highlight":{
"fields":{
"FIELD":{ //指定要高亮的字段,可以指定多个
"pre_tags":"<em>", //用来标记高亮字段的前置标签
"post_tags":"</em>" 、//后置标签
}
}
}
}
未完待续
https://www.bilibili.com/video/BV1LQ4y127n4/?p=111&spm_id_from=pageDriver&vd_source=f71bfe4ea9de7e1bb5d4e3832e91832c文章来源:https://www.toymoban.com/news/detail-479253.html
查询多个字段
语法:
GET /索引库名/_search
{
"query":{
"multi_match":{
"query":"text",
"fields":["FIELD1","FIELD2"]
}
}
}
示例:
查询多个字段,如果其中一个字段符合query,那就显示出来
GET /fandou/_search
{
"query":{
"multi_match":{
"query":"女性",
"fields":["name","occupation","university","sex"]
}
}
}
注意:参与搜索的字段越多,效率越低。应该把多个字段整合进一个字段all里面,进行搜索。例如在创建索引库时,使用copy_to功能,把多个字段复制进一个字段all里面,搜索的时候就直接使用all作为字段即可。
精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。
有两种查询模式:
- tern:根据词条精确查询
- range:根据值访问查询
tern精确查询演示:
GET /索引库名/_search
{
"query":{
"tern":{
"FIELD":{
"value":"VALEUE"
}
}
}
}
示例:
查询字段degree,必须等于”博士“这个值,否之不显示
GET /fandou/_search
{
"query": {
"term": {
"degree": {
"value": "博士"
}
}
}
}
range范围查询语法:
GET /索引库/_search
{
"query": {
"range": {
"FILED": {
"gte": 10,
"lte": 20
}
}
}
}
演示:
这里表示查询id字段,范围是10-20的数值
gte表示大于等于
gt表示大于
le表示小于
lte表示小于等于
GET /fandou/_search
{
"query": {
"range": {
"id": {
"gte": 10,
"lte": 20
}
}
}
}
地理查询
geo_bounding_box:查询geo_point值落在某个矩形范围的所有数据
语法:
GET /索引库/_search
{
"query":{
"geo_bounding_box":{
"FIELD":{
"top_left":{
"lat":31.1,
"lon":121.5
},
"bottom_right":{
"lat":30.9,
"lon":121.7
}
}
}
}
}
geo_distance:查询到指定中心点小于某个距离值的所有数据
常见应用常见:附近的人
语法:
这里表示以(31.21,121.5)为中心点查询范围15km的文档
GET /索引库/_search
{
"query":{
"geo_distance":{
"distance":"15km",
"FIELD":"31.21,121.5"
}
}
}
复合查询
可以将上诉的查询条件组合起来,实现更复杂的搜索逻辑
fucation score:算分函数查询,可以控制文档的分数(_score),控制文档排名
示例:
GET /hotel/_search
{
"query":{
"function_score":{
"query":{
"match":{"
"degree":"博士"
"}
},
"functions":[
{
"filter":{"term":{"id":"1"}},
"weight":10
}
],
"boost_mode":"multiply"
}
}
}
这个例子的意思是,先用match查询到dgree字段的文档,es会根据相关性自行排名一遍,然后再根据term精确匹配到id字段为1的文档,给它权重设置为10。这样id为1的文档会把原先的默认权重,乘上自定义的权重10,得到最终权重。而boost_mode表示加权模式,multiply表示两者相乘。这种应用场景常见与竞价排名
[外链图片转存中…(img-S9WSKR47-1664193921875)]
boolean query:布尔查询是一个或多个查询子句的组合。
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
示例:
可以结合多个查询条件
GET /索引库/_search
{
"query":{
"bool":{
"must":[
{"term":{"degree":"博士"}}
],
"should":[
{"term":{"sex":"女性"}}}
]
}
}
}
结果处理
排序
es支持对搜索结果排序,默认是根据相关度算分(_score)排序。可以排序字段有:keyword类型、数值类型、地理坐标类型、日期类型等。
排序方式:ASC(升序)、DESC(降序)
语法:
GET /索引库/_search
{
"query":{
"match_all":{}
},
"sort":[
{"FILED":"DESC"},
{"FILED2":"ASC"}
]
}
分页
es默认情况下只返回10条数据
自定义分页演示:(类似mysql的limit)
GET /索引库/_search
{
"query":{
“match_all":{}
},
"from":100, //分页开始的未知,默认为0
”size":10, //获取的文档总是,类似limit 100 0
}
高亮
就是在搜索结果中把搜索关键字突出显示
原理:
- 将搜索结果中的关键字用标签标记出来
- 在页面中给标片添加css样式
语法:
GET /索引名/_search
{
"query":{
"match":{
"FILED":"TEXt"
}
},
"highlight":{
"fields":{
"FIELD":{ //指定要高亮的字段,可以指定多个
"pre_tags":"<em>", //用来标记高亮字段的前置标签
"post_tags":"</em>" 、//后置标签
}
}
}
}
未完待续
https://www.bilibili.com/video/BV1LQ4y127n4/?p=111&spm_id_from=pageDriver&vd_source=f71bfe4ea9de7e1bb5d4e3832e91832c
P111未看文章来源地址https://www.toymoban.com/news/detail-479253.html
到了这里,关于微服务技术栈笔记从入门到跑路-SpringCloud+Gateway+Nacos+MQ+ES(保姆级)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!