第一天
1.跨域发送http请求通过RestTemplate接口来实现
微服务远程调用是利用RestTemplate跨域请求来实现的
package cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import cn.itcast.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("order")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final RestTemplate restTemplate;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
//1. 根据id查询订单并返回
Order order = orderService.queryOrderById(orderId);
//2. 通过restTemplate发起跨域http请求查询用户信息
//2.1 设置URL
String url = "http://localhost:8076/user/"+order.getUserId();
//2.2 发送请求
User forObject = restTemplate.getForObject(url, User.class);
order.setUser(forObject);
return order;
}
}
2.Eureka架构
2.1注册Eureka注册中心,将自己的Eureka服务也注册上去
- EurekaServer:服务端,注册中心
- 记录服务信息
- 心跳监控
- EurekaClient:客户端
- Provider:服务提供者,例如案例中的user-Service
- 注册自己的信息到EurekaServer
- 每隔30秒向EurekaServer发送心跳
- Consumer: 服务消费者,例如案例中的order-service
- 根据服务名称从EurekaServer拉去服务列表
- 基于服务列表做负载均衡(注解),选中一个微服务后发起远程调用
- Provider:服务提供者,例如案例中的user-Service
2.2将客户端注册到Eureka注册中心
spring:
application:
name: UserService //指定注册到eureka上面的服务名称
eureka:
client:
service-url: # eureka的地址信息,eureka本身自己也是一个微服务,所以把自己也注册到eureka上面
defaultZone: http://127.0.0.1:10086/eureka # 应该配置eureka集群的地址,就是多个eureka地址用,隔开
server:
peer-node-read-timeout-ms: 1000
# 默认是是200 设置大一点避免eureka系欸但占据所有的cpu时间出现socket read timeout exception
3.Ribbon负载均衡
4.Nacos注册中心
认识Nacos
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
第二天
1.Nacos配置管理
- 统一配置管理
- 配置热更新
- 配置共享
- 搭建Nacos集群
配置热更新
1.1.1 Nacos实现配置文件的热更新
1.1.2
1.1.3
总结:
配置当前微服务的热重载yaml文件
- 在pom文件中加入nocas的config依赖
- 在新建一个bootstrap.yml,这个文件是引导文件,优先级高于application.yml文件,配置nacos地址,当前环境,服务名称,文件名后缀。这些决定了程序启动时去哪个nacos读取文件
- 在nacos服务器当中创建一个热重载文件,注意nacos中的配置文件的后缀名一定要和bootstrap中的一样
bootstrap.yml配置文件
spring:
application:
name: UserService #服务名称
profiles:
active: dev #开发环境 测试
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
config:
file-extension: yaml #文件后缀名
nacos启动的配置文件
启动服务通过@value注入
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
@GetMapping("now")
public String now(){
System.out.println(dateformat);
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
通过配置类注解@ConfigurationProperties(prefix="pattern")做到,比上面的@Value的优点在,即使获取不到数据第一时间也不会报错终止程序
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
private String envSharedValue;
}
nacos配置共享
1.nacos中创建一个共享配置文件
2.每个服务启动的时候都会自动读取其中内容
多种服务共享配置的优先级
服务名-progile.yaml > 服务名称(环境共享).yaml > 本地配置
nacos集群搭建
步骤
1.搭建MySQL集群并初始化数据库表
2.下载解压nacos
3.修改集群配置(节点信息),数据库配置
4.分别启动多个nacos节点
5.nginx反向代理
2.http客户端Feign
- Feign替代RestTemplate
- 自定义配置
- Feign使用优化
- 最佳实践
2.1Feign替代RestTemplate
Feigin会自动帮我们实现负载均衡,他的核心依赖中有Ribbon依赖
1.消费端导入依赖
<!-- Feign依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2.在启动程序上添加注解
@EnableFeignClients3.配置跨域请求接口,其中@FeignClient注解中内容为需要调用的服务名称,
@FeignClient("UserService") public interface UserClient { @GetMapping("/user/{id}") User getUserById(@PathVariable("id")Long id); }
4.方法调用
@GetMapping("{orderId}") public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) { //1. 根据id查询订单并返回 Order order = orderService.queryOrderById(orderId); // 2. 通过restTemplate发起跨域http请求查询用户信息 2.1 设置URL //String url = "http://UserService/user/"+order.getUserId(); 2.2 发送请求 //User forObject = restTemplate.getForObject(url, User.class); //order.setUser(forObject); //User userById = userClient.getUserById(order.getUserId()); order.setUser(userClient.getUserById(order.getUserId())); return order; }
2.2Fegin实现自定义配置
2.3Feign客户端优化
1.导入依赖,配置连接池
<!-- httpClient的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
2.配置xml文件
feign: httpclient: enabled: true #支持HttpClient的开关,默认为true,但是不添加配置,httpclient不会生效 max-connections: 200 #最大连接数 max-connections-per-route: 50 #单个路径的最大连接数
2.4Feign的最佳实践
第二种方法
1.新建一个module包Feign-api
2.引入feign依赖
<!-- Feign依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
3.配置Feign跨域请求接口
@FeignClient("UserService") public interface UserClient { @GetMapping("/user/{id}") User getUserById(@PathVariable("id")Long id); }
4.再用到跨域请求的服务端映入Feign-api
<dependency> <groupId>com.example</groupId> <artifactId>feign-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
5获取feign请求的接口
@RestController @RequestMapping("order") @RequiredArgsConstructor public class OrderController { private final OrderService orderService; private final RestTemplate restTemplate; private final UserClient userClient; @GetMapping("{orderId}") public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) { //1. 根据id查询订单并返回 Order order = orderService.queryOrderById(orderId); order.setUser(userClient.getUserById(order.getUserId())); return order; } }
6.springboot启动类注解配置开启feign
@MapperScan("cn.itcast.order.mapper") @SpringBootApplication /** * 当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FiegnClient无法使用。有两种方式解决: * 方式一:指定Feign所在包 * @EnableFeignClients(basePackages="com.itcast.feign.client") */ /** * 方式二:指定FeignClient字节码 */ @EnableFeignClients(clients = {UserClient.class}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
3.统一网关GateWay
- 为什么需要网关
- gateway快速入门
- 断言工厂
- 过滤器工厂
- 全局过滤器
- 跨域问题
搭建网关服务 GateWay快速入门
1.创建新的module,引入SpringCloudGateWay的依赖和nacos的服务发现依赖:
<!-- nacose服务注册发现依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 网关gateway依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
2.第二部配置yml文件
- 配置网关端口
- 服务名称
- nacos地址
- 网关路由配置
- 路由id,自定义,只要唯一即可
- 路由的目标地址
- 路由的断言,也就是判断请求是否符合路由规则条件
server: port: 10010 # 网关端头 spring: application: name: GateWay # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service #路由id,自定义,只要唯一即可 #url: http://127.0.0.1:8081 # 路由的目标地址http就是固定地址 uri: lb://UserService # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则条件 - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求 - id: order-service uri: lb://OrderService predicates: - Path=/order/**
断言工厂
作用:读取用户定义的断言条件,对请求做出判断
路由过滤器GatwayFilter
GatewayFilter是网关中提供的一种过滤器,跨域对进入网关的请求和微服务返回的响应做处理:
Spring Cloud Gatewayhttps://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories过滤器举例:给指定url添加一个请求头
server: port: 10010 # 网关端头 spring: application: name: GateWay # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service #路由id,自定义,只要唯一即可 #url: http://127.0.0.1:8081 # 路由的目标地址http就是固定地址 uri: lb://UserService # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则条件 - Path=/user/** # 这个是按照路径皮皮额,只要以/user/开头就符合要求 filters: - AddRequestHeader=Truth,F The World!!! - id: order-service uri: lb://OrderService predicates: - Path=/order/**
controller层通过RequestHeader获取请求头,reqiured属性为不一定要获取到
@GetMapping("/{id}") public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth",required = false) String truth) { System.out.println(truth); return userService.queryById(id); }
默认过滤器举例:
如果要对所有的路由都生效,则可以将过滤器工厂写道default下。格式如下:
server: port: 10010 # 网关端头 spring: application: name: GateWay # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service #路由id,自定义,只要唯一即可 #url: http://127.0.0.1:8081 # 路由的目标地址http就是固定地址 uri: lb://UserService # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则条件 - Path=/user/** # 这个是按照路径皮皮额,只要以/user/开头就符合要求 filters: - AddRequestHeader=Truth,F The World!!! - id: order-service uri: lb://OrderService predicates: - Path=/order/** default-filters: #默认过滤器,会对所有的路由请求都生效 - AddRequestHeader=Truth,F The World!!! # 添加请求头
全局过滤器GlobalFilter
1.首先 创建一个类继承GlobalFilter类实现里面的方法
@Order(-1) @Component public class AuthorizeFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1.获取请求参数 MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams(); //2.获取authorization参数,getFirst获取指定key的第一个value String authorization = queryParams.getFirst("authorization"); //3.校验 if ("admin".equals(authorization)){ return chain.filter(exchange); } //4.拦截 //4.1禁止访问 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //4.2结束处理 return exchange.getResponse().setComplete(); } }
过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter,GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。
跨域问题处理
第三天
初识Docker
- 什么是Docker
- Docker和虚拟机的区别
- Docker架构
- 安装Docker
项目部署的问题
大型项目组件较多,运行环境也较为复炸,部署时会碰到一些问题:
- 依赖关系复炸,容易出现兼容性问题
- 开发、测试、生成环境有差异
Docker
Docker如何解决依赖的兼容问题的?
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
Docker允许开发中将应用、依赖、函数库、配置一起打包,形参可移植镜像
- Docker应用运行在容器中,使用沙箱机制,相互隔离
Docker如何解决开发、测试、生产环境有差异的问题
- Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此跨域在任意Linux操作系统上运行
总结:
Docker是一个快速交付应用、运行应用的技术:
- 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
- 运行时利用沙箱机制形参隔离容器,各个应用互不干扰
- 启动、一处都可以通过一行命令完成,方便快捷
Docker与虚拟机
Docker和虚拟机的差异
- docker是一个系统的进程;虚拟机是在操作系统中的操作系统
- docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
镜像和容器
镜像(Image):Docker将应用及其所需的依赖、函数库、环境、配置等文件打包在一起,成为镜像。
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
Docker和DockerHub
- DockerHub:DockerHub是一个Docker镜像的托管平台。这样的平台成为DockerRegister
- 国内也有类似于DockerHub的公开服务,比如网易云镜像服务,阿里云镜像库等。
Docker结构
Docker是一个CS家口的程序,由两部分组成:
- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
- 客户端(client):通过命令或RestApi向Docer服务端发送指令。可以在本地或远程向服务端发送指令。
总结 :
镜像:将应用程序及其依赖、环境、配置打包在一起
容器:镜像运行起来就是容器,一个镜像可以运行多个容器
Docker结构:
服务端:接受命令或远程请求,操作镜像或容器
客户端:发送命令或者请求到Docker服务端
DockerHub:
一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
自定义镜像
镜像结构
总结:
- BasImage层:包换基本的系统函数库、环境变量、文件系统
- Entrypoint:入口,是镜像中应用启动的命令
- 其他:在BaseImage基础上添加依赖、安装程序,完成整个应用的安装和配置
什么是Dockerfile
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer
基于Ubuntu镜像构建一个新镜像,运行一个java项目
- 新建一个空文件夹docker-demo
- 拷贝课前资料中的docker-demo.jar文件到docker-demo
- 拷贝课前资料中的jdk8.tar.gz文件到docker-demo这个目录
- 拷贝课前资料提供的Dockerfile到docker-demo这个目录
- 进入docker-demo
- 运行命令:docker build -t javaweb:1.0 . (注意有1.0后面要空格加一个.表示dockerfile在当前目录)
基于java:8-alpine镜像,将一个Java项目构建为镜像
实现思路如下:
- 新建一个空的目录,然后在目录中新建一个文件,命名为Dockerfile
- 拷贝课前资料提供的docker-demo.jar到这个目录中
- 编写Dockerfile文件:
- 基于java:8-alpne作为镜像
- 将app.jar拷贝到镜像中
- 暴露端口
- 编写入口ENTRYPOINT
- 使用docker build命令构建镜像
- 使用docker run创建容器并运行
总结:
- Dockerfile的本质是一个文件,通过指令描述镜像的构建过程
- Dockerfile的第一行必须是FROM,从一个基础镜像来构建
- 基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如:java:8-alpine
DockerCompose
- Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
- Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行的
- DockerCompose的详细语法参考官网:Compose specification | Docker Documentation
第四天
初识MQ消息队列
- 同步通信
- 异步通信
- MQ常见框架
同步调用方案
同步调用的优点:
- 时效性强,可以立即得到结果
同步调用的问题:
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失败的问题
异步调用方案
异步调用常见实现就是事件驱动模式
异步通信的优点:
- 耦合度低
- 吞吐量提升
- 故障隔离
- 流量削峰
异步通信的缺点:
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
什么是MQ
MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
RabbitMQ快速入门
- RabbitMQ概述和安装
- 常见消息模型
- 快速入门
RabbitMQ概述
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/
RabbitMQ中的几个概念:
- cahnnel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
MQ消息队列UI管理台用的是5672
MQ消息队列用的是15672
基本消息队列的发送流程:
- 建立connection
- 创建channel
- 利用channel申明队列
- 利用channel向队列发送消息
@Test public void testSendMessage() throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.240.128"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("root"); factory.setPassword("123456"); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null); // 4.发送消息 String message = "hello, rabbitmq!"; channel.basicPublish("", queueName, null, message.getBytes()); System.out.println("发送消息成功:【" + message + "】"); // 5.关闭通道和连接 channel.close(); connection.close(); }
基本消息队列的消息接受流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 定义consumer的消费新防卫handleDelivery()
- 利用channel将消费者与队列绑定
public static void main(String[] args) throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.240.128"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("root"); factory.setPassword("123456"); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null); // 4.订阅消息 channel.basicConsume(queueName, true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 5.处理消息 String message = new String(body); System.out.println("接收到消息:【" + message + "】"); } }); System.out.println("等待接收消息。。。。"); }
SpringAMQP
- Basic Queue简单队列模型
- Work Queue工作队列模型
- 发布、订阅模型-Fanout
- 发布、订阅模型-Direct
- 发布、订阅模型-Topic
- 消息转换器
什么是AMQP
- 应用间消息通信的一种协议,与语言和平台无关。
SpringAMQP实现消息的发送
- 引入amqb的依赖
<!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 配置RabbitMQ地址
spring: rabbitmq: host: 192.168.240.128 port: 5672 username: root password: 123456 virtual-host: /
- 利用RabbitTemplate的convertAndSend方法发送消息(这个方法并不会自己创建消息队列,需要用到已有的消息队列)
@SpringBootTest @RunWith(SpringRunner.class) public class SpringAmqpTest { @Resource private RabbitTemplate rabbitTemplate; @Test public void testSendMessage(){ String queueName = "simple.queue"; String message = "zzzz"; rabbitTemplate.convertAndSend(queueName,message); } }
SpringAMQP实现消息的接受
- 在服务中配置application.yml配置文件,添加mq连接信息:
logging: pattern: dateformat: MM-dd HH:mm:ss:SSS spring: rabbitmq: host: 192.168.240.128 port: 5672 username: root password: 123456 virtual-host: /
- 在consumer服务中新建一个类,编写消费逻辑:
@Component public class SpringRabbitListener { @RabbitListener(queues = "${rabbit.queue:simple.queue}") public void listenSimpleQueueMessage(String msg){ System.out.println("Spring 消费者接受到消息:"+msg); } }
SpringAMQP实现多个消费者接受一个消息队列
@RabbitListener(queues = "${rabbit.queue:simple.queue}") public void listen50test1(String msg) throws InterruptedException { System.out.println("test1 接收到消息:"+msg+"时间:"+ LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = "${rabbit.queue:simple.queue}") public void listen50test2(String msg) throws InterruptedException { System.out.println("test2 接收到消息:"+msg+"时间:"+ LocalTime.now()); Thread.sleep(200); }
发布(Publish) 、订阅(Subscribe)
一般的消息都是只能被一个消费者消费,消费完直接删除
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)
- Fanout:广播
- Direct:路由
- Topic:话题
注意:exchange负责消息路由,而不是存储,路由失败则消息丢失
发布订阅-Fanout Exchange(广播)
Fanout Exchange会接受到的消息路由到每一个跟其绑定的queue
利用SpringAMQP演示FanoutExchange的使用
实现思路如下:
- 在consumer服务中,利用代码声明队列、交换机,并将两者绑定
- 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
- 在publisher中编写测试方法,向exchange交换机中发送信息
声明交换机和队列并将其绑定
@Configuration public class FanoutConfig { /** * 1.交换机fanout声明 */ @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("fanout.test1"); } /** * 2.消息队列queue1声明 */ @Bean public Queue fanoutQueue1(){ return new Queue("fanout.queue1"); } /** * 3.绑定队列1到交换机 */ @Bean public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } /** * 4.消息队列queue2声明 */ @Bean public Queue fanoutQueue2(){ return new Queue("fanout.queue2"); } /** * 5.绑定队列2到交换机 */ @Bean public Binding fanoutBinding(Queue fanoutQueue2,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } }
消息发送
@Test public void fanoutExchangeTest(){ log.info("交换机名称"); String exchangeName = "fanout.test1"; log.info("发送消息,参数分别是:交换机名称,RoutingKey(暂时为空),消息"); rabbitTemplate.convertAndSend(exchangeName,"","fanoutExchange"); }
消息接受
/** * 声明FanoutExchange交换机 * @param msg * @throws InterruptedException */ @RabbitListener(queues = "${rabbit.queue:fanout.queue1}") public void listen50test1(String msg) throws InterruptedException { System.out.println("listen1 接收到消息:"+msg+"时间:"+ LocalTime.now()); } @RabbitListener(queues = "${rabbit.queue:fanout.queue2}") public void listen50test2(String msg) throws InterruptedException { System.out.println("listen2 接收到消息:"+msg+"时间:"+ LocalTime.now()); }
发布订阅-DirectExchange(路由)
Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
利用SpringAMQP演示DirectExchange的使用
- 利用@RabbitListener声明Exchange、Queue、RoutingKey
- 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
- 在publisher中编写测试方法,向交换机Exchange发送消息
总结:
描述下Direct交换机与Fanout交换机的差异
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabblitListener注解声明队列和交换机有哪些常见的注解
- @Queue绑定消息队列
- @Exchange绑定交换机
发布订阅-TopicExchange
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以.分割。
Queue与Exchange指定BindingKey时可以使用通配符:
#:代指0个或多个单词
*:代指一个单词
实现思路如下:
- 并利用@RabbitListener声明Exchange、Queue、RoutingKey
- 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
- 在publisher中编写测试方法,向exchange发送消息
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "topic.exchange",type = ExchangeTypes.TOPIC), key = ("china.#") )) public void TopicListen1(String msg){ System.out.println("topic1"+msg); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "topic.exchange",type = ExchangeTypes.TOPIC), key = ("#.news") )) public void TopicListen2(String msg){ System.out.println("topic2"+msg); }
/** * topic交换机发送消息 */ @Test public void topicExchangeTest(){ log.info("交换机名称"); String exchangeName="topic.exchange"; rabbitTemplate.convertAndSend(exchangeName,"china.news","1china.news"); rabbitTemplate.convertAndSend(exchangeName,"china.edu","2china.edu"); rabbitTemplate.convertAndSend(exchangeName,"k.news","3k.news"); }
描述下Direct交换机和Topic交换机的差异
Direct可以利用通配符来进行queue绑定
SpringAMQP-消息转换器
测试发送Object类型的消息
说明:在SpringAMQP的发送方法中,接受消息的类型是Object,也就说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送.
SpirngAMQP实现序列化传送数据JACKSON序列化
1.导入依赖
@Component public class SerMessageConfiguration { @Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); } }
2.配置序列化bean
@Component public class SerMessageConfiguration { @Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); } }
3.传递消息
/** * 测试SpringAMQP传递JackJson序列化的对象 */ @Test public void SerTest(){ log.info("测试序列化数据"); man mane = new man("zk",12,123); rabbitTemplate.convertAndSend("simple.queue", Map.of("man",mane)); }
4.绑定序列通道接受消息(注意接受的module里面也得引入依赖和配置序列化bean)
@RabbitListener(queues = "simple.queue") public void serTest1(Map<String,Object> o){ System.out.println(o.get("man")); System.out.println(o); }
DAY5 分布式搜索elasticsearch基础
分布式搜索elasticsearch基础
- 初识elasticsearch
- 索引库操作
- 文档操作
- RestAPI
初识elasticsearch
- 了解ES
- 倒排索引
- es的一些概念
- 安装es、kibana
了解ES
elasticsearch是elastic stack的核心,负责存储、搜索分析数据
总结:
什么是elasticsearch(底层是Lucene)
- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是elastic stack(ELK)
- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
什么是Lucene
- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心api
正向索引和倒排索引
什么是文档和词条?
- 每一条数据就是一个文档
- 对文档中的内容分词,得到的词语就是词条
什么是正向索引
- 基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条
什么时倒排索引
- 对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条拆线呢到文档id,而后获取到文档
ES和Mysql概念对比
架构:
Mysql:擅长事务类型操作,可以确保数据安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
总结:
文档:一条数据就是一个文档,es中是json格式
字段:Json文档中的字段
索引:同类型文档的集合
映射:索引中文档的约束,比如字段名称、类型
elasticsearch与数据库的关系:
- 数据库负责事务类型操作
- elasticsearch负责海量数据的搜索、分析、计算
安装ES,kibana ,ik插件分词器
1.部署单点es
因为我们还需要部署kibana容器,因此需要让es和kebana容器互联。这里先创建一个网络
docker network create es-net
1.2加载镜像,黑马程序员的课程提供了资料
这里我们采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。
课前资料提供了镜像的tar包:
上传到虚拟机中,然后运行命令加载即可:
# 导入数据 docker load -i es.tar
同理还有
kibana
的tar包也需要这样做。运行docker命令,部署单点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 "cluster.name=es-docker-cluster"
:设置集群名称
-e "http.host=0.0.0.0"
:监听的地址,可以外网访问
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小
-e "discovery.type=single-node"
:非集群模式
-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录
-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录
-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录
--privileged
:授予逻辑卷访问权
--network es-net
:加入一个名为es-net的网络中
-p 9200:9200
:端口映射配置在浏览器中输入:http://192.168.240.128/http://192.168.150.101:9200 即可看到elasticsearch的响应结果:
运行docker命令,部署kibana
docker run -d \ --name kibana \ -e ELASTICSEARCH_HOSTS=http://es:9200 \ --network=es-net \ -p 5601:5601 \ kibana:7.12.1
--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中
-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
-p 5601:5601
:端口映射配置此时,在浏览器输入地址访问:http://192.168.150.101:5601,即可看到结果
索引库操作
mapping属性
mapping是对索引库中文文档的约束,常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本),keyword(精确值:品牌,邮箱,ip)
- 数值:long、integer、short、byte、double、flowat
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
索引库的创建
#创建索引库 put /zk { "mappings":{ "properties":{ "info":{ "type":"text", "analyzer":"ik_smart" }, "email":{ "type":"keyword", "index":false }, "name":{ "type":"object", "properties":{ "firstname":{ "type":"keyword" }, "lastname":{ "type":"keyword" } } } } } }
索引库的操作
#查询索引 GET /zk #修改索引 只能增加没有的字段,已有字段不能修改 PUT /zk/_mapping { "properties":{ "age":{ "type":"integer" } } } #删除索引 DELETE /zk
文档操作
添加文档
新增文档的DSL语法如下:
POST /zk/_doc/1 { "info":"我的天涯,不看不看", "email":"1281284@qq.com", "name":{ "firstName":"云", "lastName":"赵" } }
查看删除文档
# 查询文档 GET /zk/_doc/1 # 删除文档 delete /zk/_doc/1
修改文档
方式一:全量修改,会删除旧文档,添加新文档
Post方法,如果文档id不存在则直接创建一个新的,如果id存在删除原有的创建新的,实现修改。
# 修改文档 POST /zk/_doc/1 { "info":"我的天涯,不看不看", "email":"asdasda@qq.com", "name":{ "firstName":"云", "lastName":"赵" } }
结果:reslut显示结果为updated
{ "_index" : "zk", "_type" : "_doc", "_id" : "1", "_version" : 3, "result" : "updated", "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "_seq_no" : 2, "_primary_term" : 1 }
方式二:增量修改,修改指定字段值
# 修改文档 局部更新 POST /zk/_update/1 { "doc":{ "email":"iszj@qq.com" } }
结果:
{ "_index" : "zk", "_type" : "_doc", "_id" : "1", "_version" : 4, "_seq_no" : 3, "_primary_term" : 1, "found" : true, "_source" : { "info" : "我的天涯,不看不看", "email" : "iszj@qq.com", "name" : { "firstName" : "云", "lastName" : "赵" } } }
RestClient操作索引库
- 创建索引库
- 删除索引库
- 判断索引库是否存在
什么是RestClient
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端本质就是组装DSL语句,通过http请求发送给ES。
根据课前资料提供的酒店数据创建索引库,索引库名为hotel,mapping属性根据数据库结构定义。
基本步骤如下:
- 导入课前资料Demo
- 分析数据结构,定义mapping属性
- 初始化JavaRestClient
- 利用JavaRestClient创建索引库
- 利用JavaRestClient删除索引库
- 利用JavaRestClient判断索引库是否存在
2.分析数据结构,定义mapping属性,
需求搜索多个字段,可以新建一个字段名为all,将所需要搜索的字段,copy_to进去all字段里面
PUT /hotel { "mappings": { "properties": { "id":{ "type": "keyword" }, "name":{ "type": "text", "analyzer": "ik_max_word", "copy_to": "all" }, "address":{ "type": "keyword", "index": false }, "price":{ "type": "integer", "copy_to": "all" }, "score":{ "type": "integer", "copy_to": "all" }, "brand":{ "type": "keyword" }, "city":{ "type": "keyword", "copy_to": "all" }, "startName":{ "type": "keyword" }, "bussiness":{ "type": "text", "copy_to": "all" }, "location":{ "type": "geo_point" }, "pic":{ "type": "keyword", "index": false }, "all":{ "type": "text", "analyzer": "ik_max_word" } } } }
3.初始化JavaRestClient
引入es的RestHighLevelClient依赖:
<!-- Elasticsearch --> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.12.1</version> </dependency>
因为Springboot默认的ES版本是7.6.2,所以我没需要覆盖默认的ES版本:
<properties> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
初始化RestHighLevelClient:
@BeforeEach四jUnti里面的注解,意义在每个@Test方法之前先执行一遍
注意这边应该是es的端口号而不是ui界面的端口号
public class HotelIndexTest { private RestHighLevelClient client; @BeforeEach void setup(){ this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.240.128:9200") )); } @AfterEach void tearDown() throws IOException { this.client.close(); } }
创建索引库
@Test void createHotelIndex() throws IOException { // 1.创建Request对象 CreateIndexRequest request = new CreateIndexRequest("hotel1"); // 2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句 request.source(MAPPING_TEMPLATE, XContentType.JSON); // 3.发起请求 client.indices().create(request, RequestOptions.DEFAULT); }
public class HotelConstants { public static final String MAPPING_TEMPLATE="{\n" + " \"mappings\": {\n" + " \"properties\": {\n" + " \"id\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"name\":{\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"address\":{\n" + " \"type\": \"keyword\",\n" + " \"index\": false\n" + " },\n" + " \"price\":{\n" + " \"type\": \"integer\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"score\":{\n" + " \"type\": \"integer\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"brand\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"city\":{\n" + " \"type\": \"keyword\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"startName\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"bussiness\":{\n" + " \"type\": \"text\",\n" + " \"copy_to\": \"all\"\n" + "\n" + " },\n" + " \"location\":{\n" + " \"type\": \"geo_point\"\n" + " },\n" + " \"pic\":{\n" + " \"type\": \"keyword\",\n" + " \"index\": false\n" + " },\n" + " \"all\":{\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}"; }
删除索引库
/** * 删除索引库 */ @Test void deleteHotelIndex() throws IOException { // 1.创建DeleteRequest对象 DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hotel1"); // 2.发起请求 client.indices().delete(deleteIndexRequest,RequestOptions.DEFAULT); }
判断索引库是否存在
/** * 判断hotel是否存在 */ @Test void existHotelIndex() throws IOException { // 1.创建getIndexRequest对象 GetIndexRequest getIndexRequest = new GetIndexRequest("hotel"); boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT); System.out.println(exists); }
RestClient操作文档
案例:利用JavaRestClient实现文档的CRUD
去数据库查询酒店数据,导入到hotel索引库,实现酒店数据的CRUD
基本步骤如下:
- 初始化JavaRestClient
- 利用JavaRestClient新增酒店数据
- 利用JavaRestClient根据id查询酒店数据
- 利用JavaRestClient删除酒店数据
- 利用JavaRestClient修改酒店数据
初始化JavaRestClient
@BeforeEach void setUp(){ this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.240.128:9200") )); }
利用JavaRestClient新增酒店数据
@Test void testAddDocument() throws IOException { //1.准备Request对象 Hotel hotel = hotelService.getById(38665); //2.转换为文档类型,其实就是将一个类转换为另一个类,因为我们存入的索引库类型,经纬度合在一起了 HotelDoc hotelDoc = new HotelDoc(hotel); //3.准备Request对象,IndexRequest索引库名,ID文档id IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString()); //4.准备Json文档 request.source(JSON.toJSONString(hotelDoc), XContentType.JSON); //5.发送请求 client.index(request, RequestOptions.DEFAULT); }
根据id查询酒店数据@Test void testgetDocument() throws IOException { //1.准备Request GetRequest getIndexRequest = new GetRequest("hotel", "38665"); //2.发送请求,得到响应 GetResponse res = client.get(getIndexRequest, RequestOptions.DEFAULT); //3.解析响应结果 String sourceAsString = res.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); System.out.println(hotelDoc); }
根据id修改酒店数据
@Test void testUpdateDocument() throws IOException { //1.创建request对象 UpdateRequest updateRequest = new UpdateRequest("hotel","38665"); //2.准备参数,没两个参数为一对 updateRequest.doc( "age",21, "name","rosasde" ); //3.更新文档 client.update(updateRequest,RequestOptions.DEFAULT); }
根据id删除文档
/** * 删除文档 */ @Test void testDeleteDocument() throws IOException { DeleteRequest deleteRequest = new DeleteRequest("hotel","38665"); client.delete(deleteRequest,RequestOptions.DEFAULT); }
总结:
利用JavaRestClient批量导入ES
需求:批量查询酒店数据,然后批量导入索引库中
思路:
- 利用mybatis-plus查询酒店数据
- 将查询到的酒店数据(Hotel)转换为文档类型(HotelDoc)数据
- 利用JavaRestClient中的Bulk批处理,实现批量新增文档,代码如下
/** * 批量增加 */ @Test void testBulkRequest() throws IOException { //批量查询酒店数据 List<Hotel> list = hotelService.list(); //1.创建Request BulkRequest request = new BulkRequest(); //2.准备参数,添加多个新增的request list.stream().forEach(l->{ //转换为文档doc HotelDoc hotelDoc = new HotelDoc(l); //创建新文档的BulkRequest对象 request.add(new IndexRequest("hotel") .id(String.valueOf(hotelDoc.getId())) .source(JSON.toJSONString(hotelDoc),XContentType.JSON)); }); //3.发送请求 client.bulk(request,RequestOptions.DEFAULT); }
第6天
- DSL查询文档
- 搜索结果处理
- RestClient查询文档
- 黑马旅游案例
DSL查询文档
DSL查询分类
- 全文检查搜索
- 精准查询
- 地理坐标查询
- 组合查询
DSL Query的分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
- 查询所有:查询所有数据,一般测试用。列如:match_all
- 全文检索(full text)查询:利用分词器对用户输入的内容分词,然后去倒排索引库中匹配。列如:
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
- ids
- range
- term
- 地理(geo)查询:根据经纬度查询。例如:
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
- bool
- function_score
DSL Query基本语法:
总结:
查询DSL的基本语法是什么?
- GET /索引库名/_search
- {"query":{"查询类型":{"FiELD":"TEXT"}}}
全文检索查询
全文检索查询,会对用户输入内容分词,常用于搜索框搜索:
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:
match的第一个字段是查询字段名,由于我们将多个字段copyTo到all这个字段中,可以直接通过all查询
multi_match:与match查询类似,只不过允许同时查询多个字段,语法:
match和multi_match的区别是什么?
- match:根据一个字段查询
- multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
term查询:
range查询:
查询price字段>=100 <=200的问题,去掉e就是大于小于gte lte
总结:
- term查询:根据词条精确皮皮额,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
- range查询:根据数值范围查询,可以是数值、日期的范围
地理查询
根据经纬度查询。常见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
根据经纬度查询,列如:
- geo_bounding_box:查询geo_point值落在某个矩形范围的所有文档
- geo_distance:查询到指定中心点小于某个距离值的所有文档
DSL相关性算法
elasticsearch中的相关性打分算法是什么?
- TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
- BM25:在elasticearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平
Function Score Query
默认模式为相乘,所以原本的这家店的得分score为3.39,现在编变成了33.9
GET /hotel/_search { "query": { "function_score": { "query": {"match": { "all": "外滩" }}, "functions": [ {"filter": {"term": { "id": "434082" }}, "weight": 10 } ], "boost_mode": "multiply" } } }
复合查询Boolean Query
布尔查询是一个或多个查询子句的组合。子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
例子:取出所有如家酒店,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。尽量关键字放在must,其他的放在must_not,filter中.
GET /hotel/_search { "query": { "bool": { "must": [ { "match": {"name": "如家" } }, { "range": {"price": {"gte":300}} } ], "must_not": [ { "range": { "price": { "gte": 400 } } } ], "filter": [ { "geo_distance": { "distance": "10km", "location": { "lat": 31.21, "lon": 121.5 } } } ] } } }
搜索结果处理
- 排序
- 分页
- 高亮
排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
#sort排序 GET /hotel/_search { "query": { "match_all": {} }, "sort": [ { "score": "desc" },{ "price": "asc" } ] } #找到121.612282,31.034661周围的酒店,距离升序排序 GET /hotel/_search { "query": { "match_all": {} }, "sort": [ { "_geo_distance": { "location": { "lat": 31.034661, "lon": 121.612282 }, "order": "asc", "unit": "km" } } ] }
分页
from+size:
- 优点:支持随即翻页
- 缺点:深度分页问题,默认查询上限(from+size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search:
- 优点:没有查询上限(单次查询的size不超过1000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,列如手机向下滚动翻页
scroll:
- 优点:没有查询上限(单词查询的size不超过10000)
- 缺点:会有外额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用aftersearch方案。
高亮
高亮:就是在搜索结果中把搜索关键字突出显示。
#高亮查询,默认情况下,ES搜索字段必须与高亮字段一致 GET /hotel/_search { "query": { "match": { "all": "如家" } },"highlight": { "fields": { "name": { "require_field_match": "false" } } } }
总结:
RestClient查询文档
- 快速入门
- match查询
- 精确查询
- 符合查询
- 排序、分页、高亮
快速入门
我们通过match_all来掩饰下基本的API,先看请求的DSL的组织:
@SpringBootTest public class HotelSearchTest { private RestHighLevelClient client; @BeforeEach void setup(){ this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.240.128:9200") )); } @AfterEach void tearDown() throws IOException { this.client.close(); } @Test void testMatchAll() throws IOException { //1.准备Request,参数放文档名称 SearchRequest searchRequest = new SearchRequest("hotel"); //2.准备DSL searchRequest.source().query(QueryBuilders.matchAllQuery()); //3.发送请求 SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); //4.解析结果 SearchHits hits = response.getHits(); //4.1查询的总条数 long value = hits.getTotalHits().value; //4.2查询结果数组 SearchHit[] hits1 = hits.getHits(); //4.3遍历 for (SearchHit searchHit : hits1){ //获取文档的source,String类型的字符串 String sourceAsString = searchHit.getSourceAsString(); //反序列化,将Json字符串转换为类 HotelDoc o = JSON.parseObject(sourceAsString, HotelDoc.class); System.out.println(o); } } } 结果: HotelDoc(id=36934, name=7天连锁酒店(上海宝山路地铁站店), address=静安交通路40号, price=336, score=37, brand=7天酒店, city=上海, starName=二钻, business=四川北路商业区, location=31.251433, 121.47522, pic=https://m.tuniucdn.com/fb2/t1/G1/M00/3E/40/Cii9EVkyLrKIXo1vAAHgrxo_pUcAALcKQLD688AAeDH564_w200_h200_c1_t0.jpg) HotelDoc(id=38609, name=速8酒店(上海赤峰路店), address=广灵二路126号, price=249, score=35, brand=速8, city=上海, starName=二钻, business=四川北路商业区, location=31.282444, 121.479385, pic=https://m.tuniucdn.com/fb2/t1/G2/M00/DF/96/Cii-TFkx0ImIQZeiAAITil0LM7cAALCYwKXHQ4AAhOi377_w200_h200_c1_t0.jpg) HotelDoc(id=38665, name=速8酒店上海中山北路兰田路店, address=兰田路38号, price=226, score=35, brand=速8, city=上海, starName=二钻, business=长风公园地区, location=31.244288, 121.422419, pic=https://m.tuniucdn.com/fb2/t1/G2/M00/EF/86/Cii-Tlk2mV2IMZ-_AAEucgG3dx4AALaawEjiycAAS6K083_w200_h200_c1_t0.jpg) HotelDoc(id=38812, name=7天连锁酒店(上海漕溪路地铁站店), address=徐汇龙华西路315弄58号, price=298, score=37, brand=7天酒店, city=上海, starName=二钻, business=八万人体育场地区, location=31.174377, 121.442875, pic=https://m.tuniucdn.com/fb2/t1/G2/M00/E0/0E/Cii-TlkyIr2IEWNoAAHQYv7i5CkAALD-QP2iJwAAdB6245_w200_h200_c1_t0.jpg) HotelDoc(id=39106, name=7天连锁酒店(上海莘庄地铁站店), address=闵行莘庄镇七莘路299号, price=348, score=41, brand=7天酒店, city=上海, starName=二钻, business=莘庄工业区, location=31.113812, 121.375869, pic=https://m.tuniucdn.com/fb2/t1/G2/M00/D8/11/Cii-T1ku2zGIGR7uAAF1NYY9clwAAKxZAHO8HgAAXVN368_w200_h200_c1_t0.jpg) HotelDoc(id=39141, name=7天连锁酒店(上海五角场复旦同济大学店), address=杨浦国权路315号, price=349, score=38, brand=7天酒店, city=上海, starName=二钻, business=江湾、五角场商业区, location=31.290057, 121.508804, pic=https://m.tuniucdn.com/fb2/t1/G2/M00/C7/E3/Cii-T1knFXCIJzNYAAFB8-uFNAEAAKYkQPcw1IAAUIL012_w200_h200_c1_t0.jpg) HotelDoc(id=45845, name=上海西藏大厦万怡酒店, address=虹桥路100号, price=589, score=45, brand=万怡, city=上海, starName=四钻, business=徐家汇地区, location=31.192714, 121.434717, pic=https://m.tuniucdn.com/fb3/s1/2n9c/48GNb9GZpJDCejVAcQHYWwYyU8T_w200_h200_c1_t0.jpg) HotelDoc(id=45870, name=上海临港豪生大酒店, address=新元南路555号, price=896, score=45, brand=豪生, city=上海, starName=四星级, business=滴水湖临港地区, location=30.871729, 121.81959, pic=https://m.tuniucdn.com/fb3/s1/2n9c/2F5HoQvBgypoDUE46752ppnQaTqs_w200_h200_c1_t0.jpg) HotelDoc(id=46829, name=上海浦西万怡酒店, address=恒丰路338号, price=726, score=46, brand=万怡, city=上海, starName=四钻, business=上海火车站地区, location=31.242977, 121.455864, pic=https://m.tuniucdn.com/fb3/s1/2n9c/x87VCoyaR8cTuYFZmKHe8VC6Wk1_w200_h200_c1_t0.jpg) HotelDoc(id=47066, name=上海浦东东站华美达酒店, address=施新路958号, price=408, score=46, brand=华美达, city=上海, starName=四钻, business=浦东机场核心区, location=31.147989, 121.759199, pic=https://m.tuniucdn.com/fb3/s1/2n9c/2pNujAVaQbXACzkHp8bQMm6zqwhp_w200_h200_c1_t0.jpg) 09-30 17:13:29.253 [SpringContextShutdownHook] INFO o.s.scheduling.concurrent.ThreadPoolTaskExecutor - Shutting down ExecutorService 'applicationTaskExecutor'
总结:
查询的基本步骤是:
- 创建Searchrequest对象
- 准备Request.source(),也就是DSL。
- QueryBulders来构建查询条件
- 传入Request.source()的query()方法
- 发送请求,得到结果
- 解析结果(参考JSON结果,从外到内,逐层解析)
黑马旅游的综合案例,如何根据条件进行ES查询
根据前端传递的参数设置获取的条件
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; private String brand; private String city; private Integer minPrice; private Integer maxPrice; private String starName; }
@RestController @RequestMapping("hotel") @AllArgsConstructor public class HotelController { private final IHotelService iHotelService; @PostMapping("list") public PageResult search(@RequestBody RequestParams requestParams){ return iHotelService.search(requestParams); } }
ServiceImpl中进行数据的查询和处理
其中client是通过spring @bean注解进行配置注入
@Bean public RestHighLevelClient client(){ return new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.240.128:9200") )); }
private final RestHighLevelClient client; @Override public PageResult search(RequestParams requestParams) { //1.创建请求 SearchRequest searchRequest = new SearchRequest("hotel"); //2.准备DSl,请求条件 //2.1query if (StringUtils.hasText(requestParams.getKey())){ searchRequest.source().query(QueryBuilders.matchQuery("all", requestParams.getKey())); }else { searchRequest.source().query(QueryBuilders.matchAllQuery()); } //2.2分页 final int page = requestParams.getPage(); final int size = requestParams.getSize(); searchRequest.source().from((page-1)*size).size(size); //3.发送分页请求 SearchResponse search = null; try { search = client.search(searchRequest, RequestOptions.DEFAULT); } catch (IOException e) { throw new RuntimeException(e); } return handleResponse(search); } public PageResult handleResponse(SearchResponse searchResponse){ //4.解析响应 SearchHits hits = searchResponse.getHits(); //4.1获取总条数 final long total = hits.getTotalHits().value; //4.2文档数组 SearchHit[] hits1 = hits.getHits(); List<HotelDoc> list = new ArrayList<>(); for (SearchHit searchHit:hits1){ //获取文档数组中的source json字符串 String sourceAsString = searchHit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); list.add(hotelDoc); } return new PageResult(total, list); }
综合案例查询步骤详解
1.创建一个searchRequset请求(一般不知道创建什么request的情况可以先通过client发起请求然后查看参数)
//1.创建请求 SearchRequest searchRequest = new SearchRequest("hotel");
2.准备请求条件,做好分页查询匹配的条件和分页
//2.准备DSl,请求条件 //2.1query if (StringUtils.hasText(requestParams.getKey())){ searchRequest.source().query(QueryBuilders.matchQuery("all", requestParams.getKey())); }else { searchRequest.source().query(QueryBuilders.matchAllQuery()); } //2.2分页 final int page = requestParams.getPage(); final int size = requestParams.getSize(); searchRequest.source().from((page-1)*size).size(size);
3.发送分页请求
//3.发送分页请求 SearchResponse search = null; try { search = client.search(searchRequest, RequestOptions.DEFAULT); } catch (IOException e) { throw new RuntimeException(e); }
4.数据解析(这个是es查询获取数据的结构)
{ "took" : 12, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 30, "relation" : "eq" }, "max_score" : 2.0235128, "hits" : [ { "_index" : "hotel", "_type" : "_doc", "_id" : "339952837", "_score" : 2.0235128, "_source" : { "address" : "良乡西路7号", "brand" : "如家", "business" : "房山风景区", "city" : "北京", "id" : 339952837, "location" : "39.73167, 116.132482", "name" : "如家酒店(北京良乡西路店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/3Dpgf5RTTzrxpeN5y3RLnRVtxMEA_w200_h200_c1_t0.jpg", "price" : 159, "score" : 46, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "2359697", "_score" : 1.9514182, "_source" : { "address" : "清河小营安宁庄东路18号20号楼", "brand" : "如家", "business" : "上地产业园/西三旗", "city" : "北京", "id" : 2359697, "location" : "40.041322, 116.333316", "name" : "如家酒店(北京上地安宁庄东路店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2wj2f8mo9WZQCmzm51cwkZ9zvyp8_w200_h200_c1_t0.jpg", "price" : 420, "score" : 46, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "1455383931", "_score" : 1.9514182, "_source" : { "address" : "西乡河西金雅新苑34栋", "brand" : "如家", "business" : "宝安商业区", "city" : "深圳", "id" : 1455383931, "location" : "22.590272, 113.881933", "name" : "如家酒店(深圳宝安客运中心站店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2w9cbbpzjjsyd2wRhFrnUpBMT8b4_w200_h200_c1_t0.jpg", "price" : 169, "score" : 45, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "728180", "_score" : 1.8842843, "_source" : { "address" : "西乡大道298-7号(富通城二期公交站旁)", "brand" : "如家", "business" : "宝安体育中心商圈", "city" : "深圳", "id" : 728180, "location" : "22.569693, 113.860186", "name" : "如家酒店(深圳宝安西乡地铁站店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/FHdugqgUgYLPMoC4u4rdTbAPrVF_w200_h200_c1_t0.jpg", "price" : 184, "score" : 43, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "2316304", "_score" : 1.8842843, "_source" : { "address" : "龙岗街道龙岗墟社区龙平东路62号", "brand" : "如家", "business" : "龙岗中心区/大运新城", "city" : "深圳", "id" : 2316304, "location" : "22.730828, 114.278337", "name" : "如家酒店(深圳双龙地铁站店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/4AzEoQ44awd1D2g95a6XDtJf3dkw_w200_h200_c1_t0.jpg", "price" : 135, "score" : 45, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "1765008760", "_score" : 1.8842843, "_source" : { "address" : "西直门北大街49号", "brand" : "如家", "business" : "西直门/北京展览馆地区", "city" : "北京", "id" : 1765008760, "location" : "39.945106, 116.353827", "name" : "如家酒店(北京西直门北京北站店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/4CLwbCE9346jYn7nFsJTQXuBExTJ_w200_h200_c1_t0.jpg", "price" : 356, "score" : 44, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "416121", "_score" : 1.8216157, "_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" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "441836", "_score" : 1.8216157, "_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" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "517915", "_score" : 1.8216157, "_source" : { "address" : "布吉路1036号", "brand" : "如家", "business" : "田贝/水贝珠宝城", "city" : "深圳", "id" : 517915, "location" : "22.583191, 114.118499", "name" : "如家酒店·neo(深圳草埔地铁站店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/228vhBCQmFRFWQBYX1cgoFQb6x58_w200_h200_c1_t0.jpg", "price" : 159, "score" : 44, "starName" : "二钻" } }, { "_index" : "hotel", "_type" : "_doc", "_id" : "197492479", "_score" : 1.8216157, "_source" : { "address" : "光明南大街14号", "brand" : "如家", "business" : "顺义温泉休闲区", "city" : "北京", "id" : 197492479, "location" : "40.124783, 116.65751", "name" : "如家酒店(北京顺义中心地铁站店)", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2hNBSjmMTk6JQ2o8ixr5s3ioevhB_w200_h200_c1_t0.jpg", "price" : 306, "score" : 45, "starName" : "二钻" } } ] } }
数据解析,json字符串解析
public PageResult handleResponse(SearchResponse searchResponse){ //4.解析响应 SearchHits hits = searchResponse.getHits(); //4.1获取总条数 final long total = hits.getTotalHits().value; //4.2文档数组 SearchHit[] hits1 = hits.getHits(); List<HotelDoc> list = new ArrayList<>(); for (SearchHit searchHit:hits1){ //获取文档数组中的source json字符串 String sourceAsString = searchHit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); list.add(hotelDoc); } return new PageResult(total, list); }
利用BooleQueryBuilder实现多个条件查询
注意一些不重要的条件通过filter来添加到查询条件中,因为这样不会参与评分score,关键字查询通过must来添加到BoolQUeryBuilder中,这种添加会参与评分
private SearchRequest extracted(RequestParams requestParams, SearchRequest searchRequest) { //2.1构建query BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (StringUtils.hasText(requestParams.getKey())){ boolQuery.must(QueryBuilders.matchQuery("all", requestParams.getKey())); }else { boolQuery.must(QueryBuilders.matchAllQuery()); } //keyword字符串过滤 品牌 if (StringUtils.hasText(requestParams.getBrand())){ boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand())); } //keyword字符串过滤 城市 if (StringUtils.hasText(requestParams.getCity())){ boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity())); } //keyword字符串过滤 星级 if (StringUtils.hasText(requestParams.getStarName())) { boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName())); } //range价格过滤 价格 gte是>= lte是<= if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) { boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice())); } //2.2分页 final int page = requestParams.getPage(); final int size = requestParams.getSize(); searchRequest.source().query(boolQuery).from((page-1)*size).size(size); return searchRequest; }
实例查询附近的酒店
前端传递过来的location通过sort排序放入
new GeoPoint生成的是一个用逗号隔开的经纬度坐标 x,y的形式,传入的也是一个x,y形式的字符串
//2.3排序 final String location = requestParams.getLocation(); searchRequest.source().sort(SortBuilders. geoDistanceSort("location", new GeoPoint(location)) .order(SortOrder.ASC) .unit(DistanceUnit.KILOMETERS) );
实例通过算分控制实现广告功能
private SearchRequest extracted(RequestParams requestParams, SearchRequest searchRequest) { //2.1构建query BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (StringUtils.hasText(requestParams.getKey())){ boolQuery.must(QueryBuilders.matchQuery("all", requestParams.getKey())); }else { boolQuery.must(QueryBuilders.matchAllQuery()); } //keyword字符串过滤 品牌 if (StringUtils.hasText(requestParams.getBrand())){ boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand())); } //keyword字符串过滤 城市 if (StringUtils.hasText(requestParams.getCity())){ boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity())); } //keyword字符串过滤 星级 if (StringUtils.hasText(requestParams.getStarName())) { boolQuery.filter(QueryBuilders.termQuery("starName.keyword", requestParams.getStarName())); } //range价格过滤 价格 gte是>= lte是<= if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) { boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice())); } //算分控制 FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery( //原始查询 boolQuery, //function score数组 new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ //一个具体的functionscore数组 new FunctionScoreQueryBuilder.FilterFunctionBuilder( //过滤条件 QueryBuilders.termQuery("isAD", true), //算分函数 ScoreFunctionBuilders.weightFactorFunction(100) ) }); //2.2分页 final int page = requestParams.getPage(); final int size = requestParams.getSize(); searchRequest.source().query(functionScoreQueryBuilder).from((page-1)*size).size(size); //2.3排序 if (StringUtils.hasText(requestParams.getLocation())){ final String location = requestParams.getLocation(); searchRequest.source().sort(SortBuilders. geoDistanceSort("location", new GeoPoint(location)) .order(SortOrder.ASC) .unit(DistanceUnit.KILOMETERS) ); } return searchRequest; }