1、什么是微服务?
微服务架构(Microservice Architecture)是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。你可以将其看作是在架构层次而非获取服务的,
简单来说,微服务就是将一个大型项目的各个业务代码,拆分成多个互不相干的小项目,而这些小项目专心的完成自己的功能,而且可以调用别的小项目的方法,从而完成整体功能
微服务的概念是由Martin Fowler(**马丁·福勒**)在2014年提出的,
微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的规模的集中管理能力,服务可以用不同的编程语言与数据库等组件实现。
22
2、怎么搭建微服务项目
在微服务概念提出之前(2014年),每个厂商都有自己的解决方案
如:
Spring自己编写的框架或软件
Netflix(奈非):早期提供了全套的微服务架构解决方案
alibaba(阿里巴巴):新版本的SpringCloudAlibaba正在迅速占领市场(推荐使用)
但是Martin Fowler(**马丁·福勒**)提出了微服务的标准之后,为了技术统一和兼容性,很多企业开始支持这个标准
现在我们开发的微服务项目,大多数都是在马丁·福勒标准下的
如果我们自己编写支持这个标准的代码是不现实的,必须通过现成的框架或组件完成满足这个微服务标准的项目结构和格式
当今程序员要想快速开发满足上面微服务标准的项目结构,首选SpringCloud
Spring Cloud
SpringCloud是由Spring提供的一套能够快速搭建微服务架构程序的框架集
框架集表示SpringCloud不是一个框架,而是很多框架的统称
SpringCloud就是为了搭建微服务架构项目出现的
有人将SpringCloud称之为"Spring全家桶",广义上指代Spring的所有产品。
在功能上可以分为:5种。
1.微服务的注册中心
2.微服务间的调用
3. 微服务的分布式事务
4. 微服务的限流
5. 微服务的网关
2.1、注册中心Nacos
Nacos是Spring Cloud Alibaba提供的一个软件
这个软件主要具有注册中心和配置中心(待更新中)的功能
我们先说它注册中心的功能
微服务中所有项目都必须注册到注册中心才能成为微服务的一部分
注册中心和企业中的人力资源管理部门有相似
当前微服务项目中所有的模块,在启动前,必须添加注册到Nacos的配置
所谓注册,就是将自己的信息,提交到Nacos来保存。
Nacos的启动
因为Nacos是java开发的
我们要启动Nacos必须保证当前系统配置了java环境变量
简单来说就是要环境变量中,有JAVA_HOME的配置,指向安装jdk的路径
确定了支持java后,就可以启动Nacos了
startup.cmd:windows启动nacos的命令文件
D:\tools\nacos\bin>startup.cmd -m standalone
-m 表示要设置启动参数
standalone:翻译为标准的孤独的,意思是正常的使用单机模式启动
运行成功默认占用8848端口,并且在代码中提示
http://localhost:8848/nacos
![image-20220505105822385](images/image-20220505105822385.png)
如果是首次访问,会出现这个界面
登录系统
用户名:nacos
密码:nacos
登录之后可以进入后台列表
不能关闭启动nacos的dos窗口
我们要让我们编写的项目注册到Nacos,才能真正是微服务项目。
创建一个微服务项目:
我们微服务开发过程中,一般都会使用一个Idea中包含多个项目的形式
<!-- Nacos注册依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
```yaml
spring: application: # 为当前项目起名,这个名字会被Nacos记录并使用 name: nacos-xxxxx cloud: nacos: discovery: # 配置Nacos所在的位置,用于注册时提交信息 server-addr: localhost:8848
这个形式就是先创建一个"父项目",再在这个父项目中创建多个子项目的操作
我们每次创建一个子项目之后都要进行"父子相认"
父项目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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.tedu</groupId> <artifactId>csmall</artifactId> <version>0.0.1-SNAPSHOT</version> <name>csmall</name> <description>Demo project for Spring Boot</description> <!-- 当前项目会以一个pom文件的形式被子项目继承 --> <packaging>pom</packaging> <!-- 表示当前项目是一个父项目,以pom文件的形式,供子项目继承 --> <packaging>pom</packaging> <!-- 当前父项目包含的所有模块,module就是模块的意思 --> <modules> <module>csmall-stock</module> </modules> </project>
子项目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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>cn.tedu</groupId> <artifactId>csmall</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.tedu</groupId> <artifactId>csmall-stock</artifactId> <version>0.0.1-SNAPSHOT</version> <name>csmall-stock</name> <description>Demo project for Spring Boot</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> </project>
如果我们自己编写的父项目想定义我们项目中需要的依赖版本号的话,也是可以实现的
这样做可以统一所有子项目的版本,在更新版本时,只需要修改父项目中定义的版本号即可
父项目的pom文件添加如下内容
```xml
<!-- 定义父项目需要的版本号参数 -->
<properties>
<mybatis.version>2.2.2</mybatis.version>
</properties>
<!-- dependencyManagement不是添加依赖,而是确定子项目使用依赖时的版本,也称锁版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
```子项目中如果需要mybatis的依赖只需要添加如下内容即可,无需再指定版本号
```xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
```上面的操作也称之为"锁版本"
在实际开发中经常有,一个类需要在多个微服务项目中使用的情况,为了减少代码的冗余 只是编写通用类和代码,实际上不需要运行,
除了实体类多个模块需要使用之外,像异常类和控制器返回的JsonResult类也是多个模块需要使用的类型,它们也要编写在通用类中
Nacos心跳机制:
心跳:周期性表示自己健康的机制
Nacos内部注册的服务都会有一个心跳机制
心跳机制的目的,是每个服务和Nacos保持沟通和交换信息的机制
默认情况下,服务启动后每隔5秒会向Nacos发送一个"心跳包",这个心跳包中包含了当前服务的基本信息
Nacos接收到这个心跳包,首先检查当前服务在不在注册列表中,如果不在按新服务的业务进行注册,如果在,表示当前这个服务是健康状态
如果一个服务连续3次心跳(默认15秒)没有和Nacos进行信息的交互,就会将当前服务标记为不健康的状态
如果一个服务连续6次心跳(默认30秒)没有和Nacos进行信息的交互,Nacos会将这个服务从注册列表中剔除,这些时间都是可以通过配置修改的。
实际上Nacos的服务类型还有分类
* 临时实例(默认)
* 持久化实例(永久实例)
默认每个服务都是临时实例
如果想标记一个服务为永久实例
cloud:
nacos:
discovery:
# ephemeral设置当前项目启动时注册到nacos的类型 true(默认):临时实例 false:永久实例
ephemeral: false
持久化实例启动时向nacos注册,nacos会对这个实例进行持久化处理
心跳包的规则和临时实例一致,只是不会将该服务从列表中剔除
一般情况下,我们创建的服务都是临时实例
只有项目的主干业务才会设置为永久实例
2.2、Dubbo概述(微服务间的调用)
首先要知道什么是RPC?
RPC是Remote Procedure Call的缩写 翻译为:远程过程调用
目标是为了实现两台(多台)计算机\服务器,相互调用方法\通信的解决方案
RPC只是实现远程调用的一套标准
该标准主要规定了两部分内容:通信协议 和 序列化协议
2.2.1.通信协议:
通信协议指的就是远程调用的通信方式 实际上这个通知的方式可以有多种
例如:写信,飞鸽传书,闪送等等
在程序中,通信方式也有多种
2.2.2.序列化协议:
序列化协议指通信内容的格式,双方都要理解这个格式
发送信息是序列化过程,接收信息需要反序列化
程序中,序列化的方式也是多种的
那么什么是Dubbo?
上面对RPC有基本认识之后,再学习Dubbo就简单了
Dubbo是一套RPC框架。既然是框架,我们可以在框架结构高度,定义Dubbo中使用的通信协议,使用的序列化框架技术,而数据格式由Dubbo定义,我们负责配置之后直接通过客户端调用服务端代码。
可以说Dubbo就是RPC概念的实现
Dubbo是SpringCloudAlibaba提供的框架
能够实现微服务相互调用的功能!
Dubbo对协议的支持
RPC框架分通信协议和序列化协议
Dubbo框架支持多种通信协议和序列化协议,可以通过配置文件进行修改
Dubbo支持的通信协议
* dubbo协议(默认)
* rmi协议
* hessian协议
* http协议
* webservice
* .....
支持的序列化协议
* hessian2(默认)
* java序列化
* compactedjava
* nativejava
* fastjson
* dubbo
* fst
* kryo
Dubbo默认情况下,支持的协议有如下特征
* 采用NIO单一长链接
* 优秀的并发性能,但是处理大型文件的能力差
Dubbo方便支持高并发和高性能
Dubbo服务的注册与发现
在Dubbo的调用过程中,必须包含注册中心的支持
注册中心推荐阿里自己的Nacos,兼容性好,能够发挥最大性能
但是Dubbo也支持其它软件作为注册中心(例如Redis,zookeeper等)
服务发现,即消费端自动发现服务地址列表的能力,是微服务框架需要具备的关键能力,借助于自动化的服务发现,微服务之间可以在无需感知对端部署位置与 IP 地址的情况下实现通信。
consumer服务的消费者,指服务的调用者(使用者)
provider服务的提供者,指服务的拥有者(生产者)
在Dubbo中,远程调用依据是服务的提供者在Nacos中注册的服务名称
一个服务名称,可能有多个运行的实例,任何一个空闲的实例都可以提供服务
Dubbo的注册发现流程
1.首先服务的提供者启动服务时,将自己的具备的服务注册到注册中心,其中包括当前提供者的ip地址和端口号等信息,Dubbo会同时注册该项目提供的远程调用的方法
2.消费者(使用者)启动项目,也注册到注册中心,同时从注册中心中获得当前项目具备的所有服务列表
3.当注册中心中有新的服务出现时,会通知已经订阅发现的消费者,消费者会更新所有服务列表
4.RPC调用,消费者需要调用远程方法时,根据注册中心服务列表的信息,只需服务名称,不需要ip地址和端口号等信息,就可以利用Dubbo调用远程方法了
我们当前csmall项目的远程调用关系如下
yml文件其它的不动,但是在dev.yml文件中要添加dubbo的配置信息和依赖信息
dubbo: protocol: # port设置-1 表示当前Dubbo端口号是自动动态生成 # 会自动从20880开始寻找可用的端口号,如果被占用,就递增寻找下一个,直到找到可用为止 port: -1 # 设置连接的名称,一般固定设置为dubbo name: dubbo registry: # 声明当前Dubbo注册到的注册中心类型和位置 address: nacos://localhost:8848 consumer: # 当本项目启动时,是否检查当前项目需要的所有Dubbo服务是否是可用状态 # 我们设置它的值为false,表示项目启动时不检查,所需的服务是否可用 check: false
<!-- Dubbo的依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency> <!-- 相关的业务逻辑层接口依赖 --> <dependency> <groupId>cn.tedu</groupId> <artifactId>csmall-order-service</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
负载均衡
什么是负载均衡
在实际项目中,一个服务基本都是集群模式的,也就是多个功能相同的项目在运行,这样才能承受更高的并发,
这时一个请求到这个服务,就需要确定访问哪一个服务器
Dubbo框架内部支持负载均衡算法,能够尽可能的让请求在相对空闲的服务器上运行
在不同的项目中,可能选用不同的负载均衡策略,以达到最好效果
Loadbalance:就是负载均衡的意思
Dubbo内置负载均衡策略算法
Dubbo内置4种负载均衡算法
- random loadbalance:随机分配策略(默认)
- round Robin Loadbalance:权重平均分配
- leastactive Loadbalance:活跃度自动感知分配
- consistanthash Loadbalance:一致性hash算法分配
实际运行过程中,每个服务器性能不同
在负载均衡时,都会有性能权重,这些策略算法都考虑权重问题
随机分配策略
假设我们当前3台服务器,经过测试它们的性能权重比值为5:3:1
下面可以生成一个权重模型
随机生成随机数
在哪个范围内让哪个服务器运行
优点:
算法简单,效率高,长时间运行下,任务分配比例准确
缺点:
偶然性高,如果连续的几个随机请求发送到性能弱的服务器,会导致异常甚至宕机
权重平滑分配
如果几个服务器权重一致,那么就是依次运行
但是服务器的性能权重一致的可能性很小
所以我们需要权重平滑分配
一个优秀的权重分配算法,应该是让每个服务器都有机会运行的
如果一个集群服务器性能比为5:3:1
1>A 2>A 3>A 4>A 5>A 6>B 7>B 8>B 9>C
10>A
上面的安排中,连续请求一个服务器肯定是不好的,我们希望所有的服务器都能够穿插在一起运行
Dubbo2.7之后更新了这个算法使用"**平滑加权算法**"优化权重平均分配策略
优点:
能够尽可能的在权重要求的情况下,实现请求的穿插运行(交替运行),不会发生随机策略中的偶发情况
缺点
服务器较多时,可能需要减权和复权的计算,需要消耗系统资源
活跃度自动感知
记录每个服务器处理一次请求的时间
按照时间比例来分配任务数,运行一次需要时间多的分配的请求数较少
一致性Hash算法
根据请求的参数进行hash运算
以后每次相同参数的请求都会访问固定服务器
因为根据参数选择服务器,不能平均分配到每台服务器上
使用的也不多
## Dubbo生产者消费者配置小结
Dubbo生产者消费者相同的配置
pom文件添加dubbo依赖,yml文件配置dubbo信息
**生产者**
* 要有service接口项目
* 提供服务的业务逻辑层实现类要添加@DubboService注解
* SpringBoot启动类要添加@EnableDubbo注解
2.3 、Seata概述 (微服务分布式事务)
什么是Seata
Seata 是一款开源的分布式事务解决方案,
致力于在微服务架构下提供高性能和简单易用的分布式事务服务也是Spring Cloud Alibaba提供的组件
Seata官方文档
更多信息可以通过官方文档获取
为什么需要Seata
我们之前学习了单体项目中的事务
使用的技术叫Spring声明式事务
能够保证一个业务中所有对数据库的操作要么都成功,要么都失败,来保证数据库的数据完整性
但是在微服务的项目中,业务逻辑层涉及远程调用,当前模块发生异常,无法操作远程服务器回滚
这时要想让远程调用也支持事务功能,就需要使用分布式事务组件Seata
> 事务的4个特性:ACID特性
- 原子性
- 一致性
- 隔离性
- 永久性
Seata保证微服务远程调用业务的原子性
**Seata将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。**
Seata的运行原理(AT模式)
观察下面事务模型
上面结构是比较典型的远程调用结构
如果account操作数据库失败需要让order模块和storage模块撤销(回滚)操作
声明式事务不能完成这个操作
需要使用Seata来解决
Seata构成部分包含
* 事务协调器TC
* 事务管理器TM
* 资源管理器RM
我们项目使用AT(自动)模式完成分布式事务的解决
AT模式运行过程
1.事务的发起方(TM)会向事务协调器(TC)申请一个全局事务id,并保存
2.Seata会管理事务中所有相关的参与方的数据源,将数据操作之前和之后的镜像都保存在undo_log表中,这个表是seata组件规定的表,没有它就不能实现效果,依靠它来实现提交(commit)或回滚(roll back)的操作
3.事务的发起方(TM)会连同全局id一起通过远程调用运行资源管理器(RM)中的方法
4.RM接收到全局id,去运行指定方法,并将运行结果的状态发送给TC
5.如果所有分支运行都正常,事务管理器(TM)会通过事务协调器通知所有模块执行数据库操作,真正影响数据库内容,反之如果有任何一个分支模块运行异常,都会通知TC,再由TC通知所有分支将数据库操作回滚,恢复成运行之前的样子
Seata的启动
seata也是java开发的,启动方式和nacos很像
只是启动命令不同
>它要求配置环境变量中Path属性值有java的bin目录路径
解压后路径不要用中文,不要用空格
也是解压之后的bin目录下
在路径上输入cmd进入dos窗口
```
D:\tools\seata\seata-server-1.4.2\bin>seata-server.bat -h 127.0.0.1 -m file
```
输入后,最后出现8091端口的提示即可!
AT模式的运行有一个非常明显的前提条件,这个条件不满足,就无法使用AT模式
这个条件就是事务分支都必须是操作关系型数据库(mysql\MariaDB\Oracle)
因为关系型数据库才支持提交和回滚,其它非关系型数据库都是直接影响数据(例如Redis)
所以如果我们在业务过程中有一个节点操作的是Redis或其它非关系型数据库时,就无法使用AT模式
除了AT模式之外还有TCC、SAGA 和 XA 事务模式
TCC模式
简单来说,TCC模式就是自己编写代码完成事务的提交和回滚
在TCC模式下,我们需要为参与事务的业务逻辑编写一组共3个方法
(prepare\commit\rollback)
prepare:准备
commit:提交
rollback:回滚
* prepare方法是每个模块都会运行的方法
* 当所有模块的prepare方法运行都正常时,运行commit
* 当任意模块运行的prepare方法有异常时,运行rollback
这样的话所有提交或回滚代码都由自己编写
优点:虽然代码是自己写的,但是事务整体提交或回滚的机制仍然可用(仍然由TC来调度)
缺点:每个业务都要编写3个方法来对应,代码冗余,而且业务入侵量大
SAGA模式
SAGA模式的思想是对应每个业务逻辑层编写一个新的类,可以设置指定的业务逻辑层方法发生异常时,运行新编写的类中的代码
这样编写代码不影响已经编写好的业务逻辑代码
一般用于修改已经编写完成的老代码
缺点是每个事务分支都要编写一个类来回滚业务,
会造成类的数量较多,开发量比较大
XA模式
支持XA协议的数据库分布式事务,使用比较少。
使用Seata
配置Seata
在需要Seata支持的模块中添加依赖和yaml配置
<!-- Seata和SpringBoot整合依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<!-- Seata 完成分布式事务的两个相关依赖(Seata会自动使用其中的资源) -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
seata: tx-service-group: csmall_group # 定义分组名称,为了与其它项目区分 service: vgroup-mapping: csmall_group: default # csmall_group分组使用Seata的默认配置完成事务 grouplist: default: localhost:8091 # 配置seata的地址和端口号(8091是默认端口号)
注意:同一个事务必须在同一个tx-service-group中同时指定相同的seata地址和端口
当前分布式事务模型的触发者模块,它应该是事务的起点,但是它不连接数据库,所以配置稍有不同
pom文件seata依赖仍然需要,但是只需要seata依赖
添加完必要的配置之后
要想启动Seata非常简单,只要在起点业务的业务逻辑方法上添加专用的注解 @DubboReference即可
添加这个注解的模块就是模型中的TM
他调用的所有远程模块都是RM
例如:
package cn.tedu.csmall.business.service.impl; import cn.tedu.csmall.business.service.IBusinessService; import cn.tedu.csmall.commons.pojo.order.dto.OrderAddDTO; import cn.tedu.csmall.order.service.IOrderService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.stereotype.Service; @Service @Slf4j public class BusinessServiceImpl implements IBusinessService { // Dubbo调用order模块的新增订单的方法 // 单纯的消费者,不需要在类上添加@DubboService @DubboReference private IOrderService dubboOrderService; // Global全局,Transactional事务 // 一旦编写@GlobalTransactional标记这个方法 // 就相当于设置了分布式事务的起点,当前模块就是分布式事务模型中的TM // 最终效果是由当前方法调用的所有远程服务中对数据库的操作要么都执行,要么都不执行 @GlobalTransactional @Override public void buy() { // 模拟购买业务 // 创建用于新增订单的DTO实体OrderAddDTO OrderAddDTO orderAddDTO=new OrderAddDTO(); // 为orderAddDTO赋值 orderAddDTO.setUserId("UU100"); orderAddDTO.setCommodityCode("PC100"); orderAddDTO.setCount(10); orderAddDTO.setMoney(666); // 因为是模拟购买,现在还不能调用order模块,所以只是输出 log.info("新增订单的信息为:{}",orderAddDTO); // dubbo调用业务 dubboOrderService.orderAdd(orderAddDTO); } }
2.3、Sentinel(微服务限流)
官网地址
https://sentinelguard.io/zh-cn/
什么是Sentinel
Sentinel也是Spring Cloud Alibaba的组件
Sentinel英文翻译"哨兵\门卫"
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
为什么需要Sentinel
为了保证服务器运行的稳定性,在请求数到达设计最高值时,将过剩的请求限流,保证在设计的请求数内的请求能够稳定完成处理。限流针对的是控制器方法。
- 丰富的应用场景
双11,秒杀,12306抢火车票
- 完备的实时状态监控
可以支持显示当前项目各个服务的运行和压力状态,分析出每台服务器处理的秒级别的数据
- 广泛的开源生态
很多技术可以和Sentinel进行整合,SpringCloud,Dubbo,而且依赖少配置简单
- 完善的SPI扩展
Sentinel支持程序设置各种自定义的规则
添加sentinel的依赖
<!-- Sentinel 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
application-dev.yml文件添加配置
cloud:
sentinel:
transport:
dashboard: localhost:8080 # 配置sentinel仪表台的位置
# 执行限流的端口号,每个项目唯一(别的项目例如cart模块,再设置的话就不能用8721了)
port: 8721
nacos:
discovery:
# 配置Nacos所在的位置,用于注册时提交信息
server-addr: localhost:8848
Sentinel启动
```
java -jar sentinel-dashboard-1.8.2.jar
```
启动之后
打开浏览器http://localhost:8080/
会看到下面的界面
用户名和密码都是
sentinel
刚开始什么都没有,是空界面
后面我们有控制器的配置就会出现信息了
限流方法
以stock模块为例
package cn.tedu.csmall.stock.webapi.controller;
import cn.tedu.csmall.commons.exception.CoolSharkServiceException;
import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO;
import cn.tedu.csmall.commons.restful.JsonResult;
import cn.tedu.csmall.commons.restful.ResponseCode;
import cn.tedu.csmall.stock.service.IStockService;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/stock")
@Api(tags = "库存管理")
public class StockController {
@Autowired
private IStockService stockService;
@PostMapping("/reduce/count")
@ApiOperation("减少商品库存数")
// @SentinelResource注解标记的控制层方法,会在运行时被Sentinel进行管理
// 在这个控制层方法第一次运行后,可以在Sentinel仪表台界面中设置限流规则
// "减少库存的方法"设置了当前方法在仪表台显示的名称
// blockHandler是指定限流时运行方法的配置
// fallback 是指当控制器方法运行发生异常时,运行的降级方法的名称
@SentinelResource(value = "减少库存的方法",blockHandler = "blockError",
fallback = "fallbackError")
public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){
// 调用业务逻辑层
stockService.reduceCommodityCount(stockReduceCountDTO);
return JsonResult.ok("库存减少已执行!");
}
nacos\seata\sentinel要启动
重启stock服务(其它服务都可以停掉)
如果不运行knife4j测试,sentinel的仪表盘不会有任何信息
在第一次运行了减少库存方法之后,sentinel的仪表盘才会出现nacos-stock的信息
选中这个信息点击"簇点链路"
找到我们编写的"减少库存的方法"点 "+流控"
设置流控规则
我们先设置QPS为1也就是每秒请求数超过1时,进行限流
然后我们可以快速双击knife4j减少库存的方法,触发它的流控效果
这样的流控没有正确的消息提示,我们需要自定义方法进行正确的提示给用户看到
自定义限流方法
对与被限流的请求,我们可以自定义限流的处理方法
默认情况下可能不能正确给用户提示,一般情况下,对被限流的请求也要有"服务器忙请重试"或类似的提示
StockController类中@SentinelResource注解中,可以自定义处理限流情况的方法
// Sentinel 自定义限流方法规则
// 1.访问修饰符必须是public
// 2.返回值类型必须和控制器方法一致
// 3.方法名称必须匹配控制器方法blockHandler配置的名称
// 4.参数列表,前面必须和控制器方法一致,后面添加BlockException类型的参数,表示限流方法
public JsonResult blockError(StockReduceCountDTO stockReduceCountDTO,
BlockException e){
// 进这个方法就是被限流的请求,直接返回限流信息即可
return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,
"服务器忙,请稍后再试");
}
重启stock-webapi模块
再次尝试被限流,观察被限流的提示
QPS与并发线程数
* QPS:是每秒请求数
单纯的限制在一秒内有多少个请求访问控制器方法
* 并发线程数:是当前正在使用服务器资源请求线程的数量
限制的是使用当前服务器的线程数
自定义降级方法
所谓降级就是正常运行控制器方法的过程中
控制器方法发生了异常,Sentinel支持我们运行别的方法来处理异常,或运行别的业务流程处理
我们也学习过处理控制器异常的统一异常处理类,和我们的降级处理有类似的地方
但是Sentinel降级方法优先级高,而且针对单一控制器方法编写
StockController类中@SentinelResource注解中,可以定义处理降级情况的方法
// 降级方法:上面@SentinelResource中fallback指定的降级方法
// 声明格式:基本和限流方法相同,方法参数不需要添加异常类型
// 当控制器方法运行发送异常时,Sentinel会自动调用这个方法
// 实际业务中,可以是新版的业务发生异常,然后转而运行老版代码的机制
public JsonResult fallbackError(StockReduceCountDTO stockReduceCountDTO){
// 因为没有老版本代码可用,所以也是返回错误信息
return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,
"运行发生异常,服务降级!");
}
sentinel限流的代码汇总:
package cn.tedu.csmall.stock.webapi.controller; import cn.tedu.csmall.commons.exception.CoolSharkServiceException; import cn.tedu.csmall.commons.pojo.stock.dto.StockReduceCountDTO; import cn.tedu.csmall.commons.restful.JsonResult; import cn.tedu.csmall.commons.restful.ResponseCode; import cn.tedu.csmall.stock.service.IStockService; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/base/stock") @Api(tags = "库存管理") public class StockController { @Autowired private IStockService stockService; @PostMapping("/reduce/count") @ApiOperation("减少商品库存数") // @SentinelResource注解标记的控制层方法,会在运行时被Sentinel进行管理 // 在这个控制层方法第一次运行后,可以在Sentinel仪表台界面中设置限流规则 // "减少库存的方法"设置了当前方法在仪表台显示的名称 // blockHandler是指定限流时运行方法的配置 // fallback 是指当控制器方法运行发生异常时,运行的降级方法的名称 @SentinelResource(value = "减少库存的方法",blockHandler = "blockError", fallback = "fallbackError") public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){ // 测试Sentinel降级 if(Math.random()<0.5){ // 随机抛出异常,抛出的异常会被降级方法处理 throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,"随机异常"); } // 调用业务逻辑层 stockService.reduceCommodityCount(stockReduceCountDTO); return JsonResult.ok("库存减少已执行!"); } // Sentinel 自定义限流方法规则 // 1.访问修饰符必须是public // 2.返回值类型必须和控制器方法一致 // 3.方法名称必须匹配控制器方法blockHandler配置的名称 // 4.参数列表,前面必须和控制器方法一致,后面添加BlockException类型的参数,表示限流方法 public JsonResult blockError(StockReduceCountDTO stockReduceCountDTO, BlockException e){ // 进这个方法就是被限流的请求,直接返回限流信息即可 return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR, "服务器忙,请稍后再试"); } // 降级方法:上面@SentinelResource中fallback指定的降级方法 // 声明格式:基本和限流方法相同,方法参数不需要添加异常类型 // 当控制器方法运行发送异常时,Sentinel会自动调用这个方法 // 实际业务中,可以是新版的业务发生异常,然后转而运行老版代码的机制 public JsonResult fallbackError(StockReduceCountDTO stockReduceCountDTO){ // 因为没有老版本代码可用,所以也是返回错误信息 return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR, "运行发生异常,服务降级!"); } }
2.4 、SpringGateway网关
奈非框架简介
早期(2020年前)奈非提供的微服务组件和框架受到了很多开发者的欢迎
这些框架和SpringCloud Alibaba的对应关系我们要了解
现在还有很多旧项目维护是使用奈非框架完成的微服务架构
Nacos对应Eureka都是注册中心
Dubbo对应Ribbon+feign都是实现微服务远程RPC调用的组件
Sentinel对应Hystrix都是做项目限流熔断降级的组件
Gateway对应Zuul都是网关组件
Gateway框架不是阿里写的,是Spring提供的
什么是网关
"网"指网络,"关"指关口或关卡
网关:就是指网络中的关口\关卡
网关就是当前微服务项目的"**统一入口**"
程序中的网关就是当前微服务项目对外界开放的统一入口
所有外界的请求都需要先经过网关才能访问到我们的程序
提供了统一入口之后,方便对所有请求进行统一的检查和管理
网关的主要功能有
* 将所有请求统一经过网关
* 网关可以对这些请求进行检查
* 网关方便记录所有请求的日志
* 网关可以统一将所有请求路由到正确的模块\服务上
路由的近义词就是"分配"
Spring Gateway简介
我们使用Spring Gateway作为当前项目的网关框架
Spring Gateway是Spring自己编写的,也是SpringCloud中的组件
SpringGateway官网
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
SpringGateway网关是一个依赖,不是一个软件
所以我们要使用它的话,必须先创建一个SpringBoot项目
这个项目也要注册到Nacos注册中心,因为网关项目也是微服务项目的一个组成部分
以 beijing 和 shanghai 这两个编写好的两个项目为例:
gateway项目就是网关项目,需要添加相关配置
<dependencies>
<!-- Gateway依赖 -->
<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>
<!-- 网关负载均衡依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
```yaml
server:
port: 9000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
# 网关也是微服务项目的一部分,所以也要注册到Nacos
server-addr: localhost:8848
gateway:
# routes是一个数组,数组中的数据使用"-"开头表示数据中的一个对象
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 当前路由的名称,和任何其他名称没有关联,只是不能和后面再出现的路由名称重复
- id: gateway-beijing
# 当匹配当前路由设置时,访问指定的服务器名称(Nacos注册的服务器名称)
# lb是LoadBalance的缩写,是负载均衡的调用
uri: lb://beijing
# 编写断言配置,断言的意思就是满足指定条件时运行某些事情
# predicates:断言
predicates:
# 断言中我们编写当路径满足指定条件时
# 当请求路径以/bj/开头时,就会路由到上面设置好的beijing服务器运行
# ↓ P大写!!!!!!!!
- Path=/bj/**
# spring.cloud.gateway.routes[0].uri
# spring.cloud.gateway.routes[0].predicates[0]
动态路由
网关项目随着微服务数量的增多
gateway项目的yml文件配置会越来越多,维护的工作量也会越来越大
所以我们希望gateway能够设计一套默认情况下自动路由到每个模块的路由规则
这样的话,不管当前项目有多少个路由目标,都不需要维护yml文件了
这就是我们SpringGateway的动态路由功能
配置文件中开启即可
```yaml
server:
port: 9000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
# 网关也是微服务项目的一部分,所以也要注册到Nacos
server-addr: localhost:8848
gateway:
discovery:
locator:
# 这是开启动态路由的配置,动态路由设置默认是不开启的 也就是enabled: false
# 路由规则是在网关端口号后,先写路由目标注册到nacos的名称,再编写具体路径
# localhost:9000/beijing/bj/show
enabled: true
```
路由规则是在9000端口号后面先编写路由目标项目注册到nacos的名称,再编写具体路径
内置断言
predicates:断言
断言的意思就是判断某个条件是否满足
我们之前使用了Path断言,判断请求的路径是不是满足条件,例如是不是/sh/** /bj/**
如果路径满足这个条件,就路由到指定的服务器
但是Path实际上只是SpringGateway提供的多种内置断言中的一种
还有很多其它断言
- after
- before
- between
- cookie
- header
- host
- method
- path
- query
- remoteaddr
**时间相关**
after,before,between
判断当前时间在指定时间之前,之后或之间的操作
如果条件满足可以执行路由操作,否则拒绝访问
表示时间的格式比较特殊,先使用下面代码获得时间
```
ZonedDateTime.now()
```
运行程序输出,可获得当前时间,这个时间的格式可能是
```
2022-08-25T10:11:32.694+08:00[Asia/Shanghai]
```
下面在yml配置中添加新的断言配置
使用After设置必须在指定时间之后访问
```yaml
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 必须满足所有断言条件才能成功路由
# 路径必须是sh开头 && 实际必须在下面时间之后
- After=2022-08-25T10:18:40.694+08:00[Asia/Shanghai]
```
必须在指定时间之后才能访问服务
否则发生404错误拒绝访问
需要注意测试时,先启动Nacos,再启动shanghai之后启动gateway
测试时必须通过9000端口访问才能有效果
使用Before设置必须在指定时间之前访问
```yaml
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 必须满足所有断言条件才能成功路由
# 路径必须是sh开头 && 实际必须在下面时间之后
- Before=2022-08-25T10:22:10.694+08:00[Asia/Shanghai]
```
使用Between设置必须在指定时间之间访问
```yaml
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 必须满足所有断言条件才能成功路由
# 路径必须是sh开头 && 实际必须在下面时间之后
- Between=2022-08-25T10:23:50.694+08:00[Asia/Shanghai],2022-08-25T10:24:30.694+08:00[Asia/Shanghai]
```
**要求指定参数的请求**
Query断言,判断是否包含指定的参数名称,包含参数名称才能通过路由
```yaml
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 当前请求必须包含名为name的参数,才能正常路由
- Query=name
```
内置过滤器
Gateway还提供的内置过滤器
不要和我们学习的filter混淆
内置过滤器允许我们在路由请求到目标资源的同时,对这个请求进行一些加工或处理
常见过滤器也有一些
我们给大家演示一下AddRequestParameter过滤器
它的作用是在请求中添加参数
```yaml
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
filters:
# 在请求到控制器前,添加参数age=18
# 控制器中可以获得这个age参数的值
- AddRequestParameter=age,18
predicates:
- Path=/sh/**
# 当前请求必须包含名为name的参数,才能正常路由
- Query=name
```
在shanghai的控制器方法中添加代码接收name,age的值
@RestController
@RequestMapping("/sh")
public class ShanghaiController {
@GetMapping("/show")
public String show(String name,Integer age){
System.out.println(ZonedDateTime.now());
return "这里是上海!name:"+name+",age:"+age;
}
}
网关项目的knife4j配置
我们希望配置网关之后,在使用knife4j测试时
就不来回切换端口号了
我们需要在网关项目中配置Knife4j才能实现
而这个配置是固定的,
只要是网关项目配置各个子模块的knife4j功能,就直接复制这几个类即可
在gateway项目中创建config包
SwaggerProvider
@Component
public class SwaggerProvider implements SwaggerResourcesProvider {
/**
* 接口地址
*/
public static final String API_URI = "/v2/api-docs";
/**
* 路由加载器
*/
@Autowired
private RouteLocator routeLocator;
/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String applicationName;
@Override
public List<SwaggerResource> get() {
//接口资源列表
List<SwaggerResource> resources = new ArrayList<>();
//服务名称列表
List<String> routeHosts = new ArrayList<>();
// 获取所有可用的应用名称
routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
.filter(route -> !applicationName.equals(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 去重,多负载服务只添加一次
Set<String> existsServer = new HashSet<>();
routeHosts.forEach(host -> {
// 拼接url
String url = "/" + host + API_URI;
//不存在则添加
if (!existsServer.contains(url)) {
existsServer.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(host);
resources.add(swaggerResource);
}
});
return resources;
}
}
SwaggerController类
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerController {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerController(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
SwaggerHeaderFilter类
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
private static final String URI = "/v2/api-docs";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path,URI )) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}
可以通过19000端口测试各个业务模块的功能
如果不使用网关一切正常,但是启动网关访问失败的话,就是gateway项目配置问题
Gateway和SpringMvc依赖冲突问题和解决
这两个依赖在同一个项目中时,默认情况下启动会报错
SpringMvc框架中自带一个Tomcat服务器
而SpringGateway框架中自带一个Netty的服务器
在启动项目时,两个框架中包含的服务器都想占用相同端口,因为争夺端口号和主动权而发生冲突
导致启动服务时报错
要想能够正常启动必须在yml文件配置
spring:
main:
web-application-type: reactive
reactive:反应的
添加这个配置之后,会Tomcat服务器会变成非阻塞的运行
3、 Elasticsearch 概述
Elasticsearch下载
官方下载链接
https://www.elastic.co/cn/downloads/past-releases#elasticsearch
什么是Elasticsearch
elastic:富有弹性的
search:搜索
我们可以把它简称为ES,但是搜索它的资料时(例如百度)还是使用Elasticsearch进行搜索更准确
**这个软件不再是SpringCloud提供的,它也不针对微服务环境的项目来开发**
Elasticsearch和redis\mysql一样,不仅服务与java语言,其它语言也可以使用
它的功能也类似一个数据库,能高效的从大量数据中搜索匹配指定关键字的内容
它也将数据保存在硬盘中
这样的软件有一个名称**全文搜索引擎**
它本质就是一个java项目,使用它进行数据的增删改查就是访问这个项目的控制器方法(url路径)
ES的底层技术
ES使用了java的一套名为Lucene的API
这个API提供了全文搜索引擎核心操作的接口,相当于搜索引擎的核心支持,ES是在Lucene的基础上进行了完善,实现了开箱即用的搜索引擎软件
市面上和ES功能类似的软件有
Solr/MongoDB
为什么需要Elasticsearch
数据库进行模糊查询严重低下
所有关系型数据库都有这个缺点(mysql\mariaDB\oracle\DB2等)
在执行类似下面模糊查询时
当前互联网项目要求"三高"的需求下,这样的效率肯定不能接受
Elasticsearch主要是为了解决数据库模糊查询性能低下问题的
ES进行优化之后,从同样数据量的ES中查询相同条件数据,效率能够提高100倍以上
数据库中的索引(基本概念)
所谓的索引(index)其实就是数据目录
通常情况下,索引是为了提高查询效率的
数据库索引分两大类
* 聚集索引
* 非聚集索引
**聚集索引**就是数据库保存数据的物理顺序依据,默认情况下就是主键id,所以按id查询数据库中的数据效率非常高
**非聚集索引**
如果想在非主键列上添加索引,就是非聚集索引了
例如我们在数据库表中存在一个姓名列,我们为姓名列创建索引
在创建索引时,会根据姓名内容来创建索引
例如"张三" 这个姓名,创建索引后查询效率就会明显提升
如果没有索引,这样的查询就会引起效率最低的"逐行搜索",就是一行一行的查这个数据的姓名是不是张三,效率就会非常低
模糊查询时因为'%鼠标%',使用的是前模糊条件,使用索引必须明确前面的内容是什么,前模糊查询是不能使用索引的,只能是全表的逐行搜索,所以效率非常低
所以当我们项目中设计了根据用户输入关键字进行查询时,需要使用**全文搜索引擎**来优化
> 1.创建的索引会占用硬盘空间
>
> 2.创建索引之后,对该表进行增删改操作时,会引起索引的更新,所以效率会降低
>
> 3.对数据库进行批量新增时,先删除索引,增加完毕之后再创建
>
> 4.不要对数据样本少的列添加索引
>
> 5.模糊查询时,查询条件前模糊的情况,是无法启用索引的
>
> 6.每次从数据表中查询的数据的比例越高,索引的效果越低
>
> 7.当我们执行查询时,where条件后应该先查询有索引的列
Elasticsearch运行原理
要想使用ES提高模糊查询效率
首先要将数据库中的数据复制到ES中
在新增数据到ES的过程中,ES可以对指定的列进行分词索引保存在索引库中
形成倒排索引结构
Elasticsearch的启动
双击bin\elasticsearch.bat运行,
浏览器输入地址:localhost:9200看到如下内容即可
ES基本使用
ES启动完成后,我们要学习如何操作它
我们已经讲过,操作ES是对ES发送请求
我们创建一个子项目search,在这个子项目中创建一个专门发送各种类型请求的文件来操作ES
创建search项目也要父子相认
然后子项目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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>csmall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>search</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
创建一个能够向ES发送请求的文件
这种能够向指定url发送请求的文件格式称之为http client(http 客户端)
![image-20220825162421614](images/image-20220825162421614.png)
文件类型叫HTTP Request文件
我们可以起名为elasticsearch
我们先从最简单的请求开始
向es发送指令
```json
### 三个#是注释,也是分隔符,这个文件中每个请求代码之间都要用###分隔
GET http://localhost:9200### 测试ES的分词功能,运行分词,查看分词效果
POST http://localhost:9200/_analyze
Content-Type: application/json{
"text": "罗技激光鼠标",
"analyzer": "standard"
}
``````json
{
"text": "罗技激光无线游戏鼠标",
"analyzer": "ik_smart"
}
``````json
POST http://localhost:9200/_analyze
Content-Type: application/json{
"text": "北京成功举行了冬季奥林匹克运动会",
"analyzer": "ik_max_word"
}
```
analyze:分析
analyzer:分析者(分词器)
standard是ES默认的分词器,"analyzer": "standard"是可以省略的
standard这个分词器只能对英文等西文字符(有空格的),进行正确分词
引入一个中文常见词语的词库,分词时按照词库中的词语分词即可
我们可以使用免费的中文分词器词库插件IK来实现中文分词效果
**ik_smart**
* 优点:特征是粗略快速的将文字进行分词,占用空间小,查询速度快
* 缺点:分词的颗粒度大,可能跳过一些重要分词,导致查询结果不全面,查全率低
**ik_max_word**
* 优点:特征是详细的文字片段进行分词,查询时查全率高,不容易遗漏数据
* 缺点:因为分词太过详细,导致有一些无用分词,占用空间较大,查询速度慢
使用ES操作数据
ES是一个数据库性质的软件
可以执行增删改查操作,只是他操作数据不使用sql,数据的结构和关系型数据库也不同
我们先了解一下ES保存数据的结构
![image-20220510142049346](images/image-20220510142049346.png)
* ES启动后,ES服务可以创建多个index(索引),index可以理解为数据库中表的概念
* 一个index可以创建多个保存数据的document(文档),一个document理解为数据库中的一行数据
* 一个document中可以保存多个属性和属性值,对应数据库中的字段(列)和字段值
SpringBoot 操作 Elasticsearch
Spring Data简介
原生状态下,我们使用JDBC连接数据库,因为代码过于繁琐,所以改为使用Mybatis框架
在ES的原生状态下,我们java代码需要使用socket访问ES,但是也是过于繁琐,我们可以使用SpringData框架简化
Spring Data是Spring提供的一套连接各种第三方数据源的框架集
我们需要使用的是其中连接ES的Spring Data Elasticseatrch
官方网站:https://spring.io/projects/spring-data
官网中列出了SpringData支持连接操作的数据源列表
下面我们就按照SpringDataElasticsearch的步骤对ES进行操作
## 添加依赖和配置
就使用我们之前创建的search模块来操作ES
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>csmall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>search</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- spring data elasticsearch 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- 为了方便测试添加测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
application.properties添加配置
```properties
# 设置ES所在的ip地址端口号
spring.elasticsearch.rest.uris=http://localhost:9200
# 设置日志门槛,用于显示ES的运行信息
logging.level.cn.tedu.search=debug
# SpringDataElasticsearch底层有一个专门输出运行状态的类,也要设置
logging.level.org.elasticsearch.client.RestClient=debug
SpringData自定义查询
SpringData框架提供的基本增删改查方法并不能完全满足我们的业务需要
如果是针对当前Es数据,进行个性化的自定义查询,那还是需要自己编写查询代码
就像我们要实现根据关键词查询商品信息一样,完成类似数据库中的模糊查询
单条件查询
我们查询需求为输出所有数据中title属性包含"游戏"这个分词的商品信息
> 参考数据库中模糊查询
>
> ```sql
> select * from item where title like '%游戏%'
> ```
我们使用SpringDataES进行查询,本质上还是相当于ES文档中执行的查询语句
在SpringData框架下,ItemRepository接口中实现更加简单
// SpringData自定义查询
// 可以通过遵循SpringData框架给定的格式定义方法名称,
// SpringData会根据方法名称自动生成查询语句
// query(查询):表示当前方法是一个查询方法,类似sql语句中的select
// Item/Items:确定要查询哪一个实体类,不带s的是单个对象,带s是集合
// By(通过/根据):标识开始设置查询条件,类似sql语句中的where
// Title:要查询的字段,可以根据查询条件修改为Item中的任何字段
// Matches:执行查询的操作,Matches表示字符串的匹配,而且这个匹配是支持分词的,类似sql语句的like
Iterable<Item> queryItemsByTitleMatches(String title);
下面可以开始在测试类中进行测试查询
//单条件查询
@Test
void queryOne(){
// 查询ES中items索引中,title字段包含"游戏"关键字的数据
Iterable<Item> items=itemRepository.queryItemsByTitleMatches("激光游戏");
items.forEach(item -> System.out.println(item));
}
上面代码运行时底层运行的查询语句为:
```json
### 单条件搜索
POST http://localhost:9200/items/_search
Content-Type: application/json{
"query": {"match": { "title": "游戏" }}
}
```
### 多条件查询
在相对复杂的查询逻辑下
经常使用多个条件来定位查询需要的数据
这样就需要逻辑运算符"and"/"or"
ItemRepository接口中添加多条件的查询方法
// 多条件查询
// 多个条件之间我们需要使用And和Or来分隔,来表示他们的查询逻辑
// 方法的参数赋值是依据方法定义的参数顺序依次向条件中赋值的
Iterable<Item> queryItemsByTitleMatchesAndBrandMatches(
String title,String brand);
测试代码如下
// 多条件查询
@Test
void queryTwo(){
// 查询ES中items索引中,title字段包含"游戏"并且品牌是"罗技"的数据
Iterable<Item> items=itemRepository
.queryItemsByTitleMatchesAndBrandMatches("游戏","罗技");
items.forEach(item -> System.out.println(item));
}
底层运行的请求.
### 多字段搜索
POST http://localhost:9200/items/_search
Content-Type: application/json{
"query": {
"bool": {
"must": [
{ "match": { "title": "游戏"}},
{ "match": { "brand": "罗技"}}
]
}
}
}
当查询条件关系为And时,查询语句关键字为must
当查询条件关系为Or时,查询语句关键字为should
### 排序查询
默认情况下从ES中查询获得的数据排序依据是ES查询得出的相关性分数(score)
但是如果想改变这个排序就需要在查询方法上添加新的关键字
在ItemRepository接口添加具备排序功能的查询方法
// 排序查询
Iterable<Item> queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
String title,String brand);
测试
// 排序查询
@Test
void queryOrder(){
Iterable<Item> items=itemRepository
.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
"游戏","罗技");
items.forEach(item -> System.out.println(item));
}
底层运行的代码
```json
### 多字段搜索
POST http://localhost:9200/items/_search
Content-Type: application/json{
"query": {
"bool": {
"should": [
{ "match": { "title": "游戏"}},
{ "match": { "brand": "罗技"}}
]
}
},"sort":[{"price":"desc"}]
}
```
### 分页查询
SpringData框架支持完成分页查询
需要在ItemRepository接口中修改方法的参数和返回值就可以实现
// 分页查询
// 返回值修改为Page类型,这个类型中包含了查询到的分页数据,和本次查询相关的分页信息
// 分页信息包含:当前页,总页数,总条数,每页条数,是否有上一页或下一页等
// 方法参数,在所有的参数后再添加一个新的参数类型,Pageable
Page<Item> queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
String title, String brand, Pageable pageable);
测试代码
// 分页查询
@Test
void queryPage(){
int pageNum=2; // 要查询的页码
int pageSize=2; // 每页包含的数据条数
Page<Item> page=itemRepository
.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
"游戏","罗技", PageRequest.of(pageNum-1,pageSize));
page.forEach(item -> System.out.println(item));
// page对象中包含的分页和信息:
System.out.println("总页数:"+page.getTotalPages());
System.out.println("总条数:"+page.getTotalElements());
System.out.println("当前页:"+(page.getNumber()+1));
System.out.println("每页条数:"+page.getSize());
System.out.println("是否为首页:"+page.isFirst());
System.out.println("是否为末页:"+page.isLast());