早期的服务器状态,安装好一些固定内容,让用户访问
功能单一,如果不修改代码,内容是不会变的,只能做信息的呈现或输出
网页中的数据可能来自数据库,数据库中的数据可以在后台中进行修改
实现不修改页面代码,但是变化页面内容的效果
因为有了数据库的支持,动态网站开始支持登录注册,增删改查功能
随着互联网的普及,个人的社交需求提升
出现了很多由用户贡献内容的网站
微博,抖音,淘宝,大众点评或类似的网站
随着用户的增加,各种并发的增高,要求我们的服务器在繁忙的情况下,也需要快速的做出响应
用户体验必须保证,这样就要求我们的项目有下面三个目标
"高并发,高可用,高性能"
高并发:很多人同时访问这个网站,这个网站不能失能
高可用:全年365天每天24小时随时可以访问,不能因为个别服务器的异常,导致整个项目瘫痪
高性能:当有用户访问时响应的速度要尽量的快,及时并发高,也要快速响应
微服务的"三高"
现在市面上常见的java开发的项目可以分为两大类
一般指一个企业或机构内部使用的网站或服务器应用程序
使用的人群比较固定,并不向全国乃至全世界开放
例如,商业,企事业单位,医疗,金融,军事,政府等
所以这个项目没有代替品,对"三高"没有强烈要求
企业级项目一般会在权限和业务流程方面设计的比较复杂
能够向全国乃至全世界开放的网站或服务器应用程序
我们手机中安装的app大部分都是互联网应用
微信,支付宝,京东,淘宝,饿了么,美团,抖音,qq音乐,爱奇艺,高德地图等
它们因为商业竞争等原因,对服务器的性能有非常高的要求,就是我们之前提到的"三高"
但是互联网应用一般没有权限和业务非常复杂的需求
综上所述,企业级应用和互联网应用的偏重点不同
在当今java开发业界中,基本规律如下
微服务的概念是由Martin Fowler(马丁·福勒)在2014年提出的
微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的规模的集中管理能力,服务可以用不同的编程语言与数据库等组件实现。
简单来说,微服务就是将一个大型项目的各个业务代码,拆分成多个互不相干的小项目,而这些小项目专心的完成自己的功能,而且可以调用别的小项目的方法,从而完成整体功能
京东\淘宝这样的大型互联网应用程序,基本每个操作都是一个单独的微服务在支持:
左侧小餐馆就像单体项目
一旦服务器忙,所有业务都无法快速响应
即使添加了服务器,也不能很好的解决这个问题
不能很好的实现"高并发,高可用,高性能"
但是因为服务器数量少,所以成本低,适合并发访问少的项目
右侧大餐厅就是微服务项目
每个业务专门一批人来负责,业务之间互不影响
在某个模块性能不足时,针对这个模块添加服务器改善性能
万一一个服务器发生异常,并不会影响整体功能
但是完成部署的服务器数量多,成本高,需要较多投资,能够满足"高并发,高可用,高性能"的项目
在微服务概念提出之前(2014年),每个厂商都有自己的解决方案
但是Martin Fowler(马丁·福勒)提出了微服务的标准之后,为了技术统一和兼容性,很多企业开始支持这个标准
现在我们开发的微服务项目,大多数都是在马丁·福勒标准下的
如果我们自己编写支持这个标准的代码是不现实的,必须通过现成的框架或组件完成满足这个微服务标准的项目结构和格式
当今程序员要想快速开发满足上面微服务标准的项目结构,首选SpringCloud
SpringCloud是由Spring提供的一套能够快速搭建微服务架构程序的框架集
框架集表示SpringCloud不是一个框架,而是很多框架的统称
SpringCloud就是为了搭建微服务架构项目出现的
有人将SpringCloud称之为"Spring全家桶",广义上指代Spring的所有产品
内容的提供者
这里讲的是阿里巴巴组件
功能上分类
Nacos是Spring Cloud Alibaba提供的一个软件
这个软件主要具有注册中心和配置中心的功能
微服务中所有项目都必须注册到注册中心才能成为微服务的一部分
注册中心和企业中的人力资源管理部门有相似
微服务项目中所有的模块,在启动前,必须添加注册到Nacos的配置
所谓注册,就是将自己的信息,提交到Nacos来保存
Nacos的下载
https://github.com/alibaba/nacos/releases/download/1.4.3/nacos-server-1.4.3.zip
国外网站,下载困难可以多试几次
或直接向项目经理老师索取
Nacos的启动
因为Nacos是java开发的
我们要启动Nacos必须保证当前系统配置了java环境变量
简单来说就是要环境变量中,有JAVA_HOME的配置,指向安装jdk的路径
确定了支持java后,就可以启动Nacos了
mac系统一定要到http://doc.canglaoshi.org/查看homebrew相关知识
mac系统安装Nacos推荐
mac系统如何安装nacos(window系统通用)?详细教程一文解决_七度_的博客-CSDN博客_mac 安装nacos
将压缩包解压(注意不要有中文路径或空格)
打开解压得到的文件夹后打开bin目录会有如下内容
cmd结尾的文件是windows版本的
sh结尾的文件是linux和mac版本的
startup是启动文件,shutdown是停止文件
Windows下启动Nacos不能直接双击cmd文件
需要在dos窗口运行
在当前资源管理器地址栏输入cmd
D:\tools\nacos\bin>startup.cmd -m standalone
startup.cmd:windows启动nacos的命令文件
-m 表示要设置启动参数
standalone:翻译为标准的孤独的,意思是正常的使用单机模式启动
运行成功默认占用8848端口,并且在代码中提示
如果不输入standalone运行会失败
startup.cmd -m standalone
如果报了
"please set JAVA_HOME......."
表示当前项目没有配置java环境变量(主要是没有设置JAVA_HOME)
如果运行没有报错
打开浏览器输入地址
http://localhost:8848/nacos
如果是首次访问,会出现这个界面
登录系统
用户名:nacos
密码:nacos
登录之后可以进入后台列表
不能关闭启动nacos的dos窗口
我们要让我们编写的项目注册到Nacos,才能真正是微服务项目
我们已经讲过,一个项目要想称为微服务项目体系的一部分
必须将当前项目的信息注册到Nacos
我们要添加一些配置,实现模块启动时注册到Nacos的效果
首先在模块pom文件中添加依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
我们在创建好的application-dev.yml中编写对nacos注册的配置信息
spring:
application:
# 为当前项目起名,这个名字会被Nacos记录并使用
name: nacos-business
cloud:
nacos:
discovery:
# 配置Nacos所在的位置,用于注册时提交信息
server-addr: localhost:8848
启动之后,
重启business模块,如果启动也正常,就应该将当前项目的信息提交给Nacos
在Nacos的服务管理->服务列表中,能看到nacos-business的名称
常见面试题
心跳:周期性表示自己健康的机制
Nacos内部注册的服务都会有一个心跳机制
心跳机制的目的,是每个服务和Nacos保持沟通和交换信息的机制
默认情况下,服务启动后每隔5秒会向Nacos发送一个"心跳包",这个心跳包中包含了当前服务的基本信息
Nacos接收到这个心跳包,首先检查当前服务在不在注册列表中,如果不在按新服务的业务进行注册,如果在,表示当前这个服务是健康状态
如果一个服务连续3次心跳(默认15秒)没有和Nacos进行信息的交互,就会将当前服务标记为不健康的状态
如果一个服务连续6次心跳(默认30秒)没有和Nacos进行信息的交互,Nacos会将这个服务从注册列表中剔除
这些时间都是可以通过配置修改的
实例类型分类
实际上Nacos的服务类型还有分类
默认每个服务都是临时实例
如果想标记一个服务为永久实例
cloud:
nacos:
discovery:
# ephemeral设置当前项目启动时注册到nacos的类型 true(默认):临时实例 false:永久实例
ephemeral: false
持久化实例启动时向nacos注册,nacos会对这个实例进行持久化处理
心跳包的规则和临时实例一致,只是不会将该服务从列表中剔除
一般情况下,我们创建的服务都是临时实例
只有项目的主干业务才会设置为永久实例
RPC是Remote Procedure Call的缩写 翻译为:远程过程调用
目标是为了实现两台(多台)计算机\服务器,相互调用方法\通信的解决方案
RPC只是实现远程调用的一套标准
该标准主要规定了两部分内容
1.通信协议
2.序列化协议
为了方便大家理解RPC,下面的图片帮助理解
上面图是老婆和老公在家的时,老婆让老公洗碗的调用流程
这个流程可以理解为项目内的功能的调用,类似面向对象编程实例化对象,调用方法的过程
但是这个调用关系如果是远程的,意思是老婆和老公现在是两个不同的项目
我们看到上图中,远程调用必须借助一个通信设备,图片中是手机
通信协议指的就是远程调用的通信方式
实际上这个通知的方式可以有多种
例如:写信,飞鸽传书,闪送等等
在程序中,通信方式也有多种
序列化协议指通信内容的格式,双方都要理解这个格式
上面的图片中,老婆给老公发信息,一定是双方都能理解的信息
发送信息是序列化过程,接收信息需要反序列化
程序中,序列化的方式也是多种的
上面对RPC有基本认识之后,再学习Dubbo就简单了
Dubbo是一套RPC框架。既然是框架,我们可以在框架结构高度,定义Dubbo中使用的通信协议,使用的序列化框架技术,而数据格式由Dubbo定义,我们负责配置之后直接通过客户端调用服务端代码。
可以说Dubbo就是RPC概念的实现
Dubbo是SpringCloudAlibaba提供的框架
能够实现微服务相互调用的功能!
我们学习的Dubbo指的都是2.7之后的版本
是能够和SpringCloudAlibaba配合使用的
Dubbo历程
2012年底dubbo停止更新后到2017年dubbo继续更新之前
2015SpringCloud开始兴起,当时没有阿里的框架
国内公司要从SpringCloud和Dubbo中抉择使用哪个微服务方案
在2012年dubbo停止更新后国内的当当网在dubbo的基础上开发了dubboX框架,并进行维护
2019年后,SpringCloud和Dubbo才能共同使用
RPC框架分通信协议和序列化协议
Dubbo框架支持多种通信协议和序列化协议,可以通过配置文件进行修改
Dubbo支持的通信协议
支持的序列化协议
Dubbo默认情况下,支持的协议有如下特征
Dubbo方便支持高并发和高性能
在Dubbo的调用过程中,必须包含注册中心的支持
注册中心推荐阿里自己的Nacos,兼容性好,能够发挥最大性能
但是Dubbo也支持其它软件作为注册中心(例如Redis,zookeeper等)
服务发现,即消费端自动发现服务地址列表的能力,是微服务框架需要具备的关键能力,借助于自动化的服务发现,微服务之间可以在无需感知对端部署位置与 IP 地址的情况下实现通信。
上面RPC的示例中,老婆就是服务的消费端,她能发现老公具备的服务
如果老婆调用了老公的服务,就是完成了Dubbo调用
consumer服务的消费者,指服务的调用者(使用者)也就是老婆的位置
provider服务的提供者,指服务的拥有者(生产者)也就是老公的位置
在Dubbo中,远程调用依据是服务的提供者在Nacos中注册的服务名称
一个服务名称,可能有多个运行的实例,任何一个空闲的实例都可以提供服务
常见面试题:Dubbo的注册发现流程
1.首先服务的提供者启动服务时,将自己的具备的服务注册到注册中心,其中包括当前提供者的ip地址和端口号等信息,Dubbo会同时注册该项目提供的远程调用的方法
2.消费者(使用者)启动项目,也注册到注册中心,同时从注册中心中获得当前项目具备的所有服务列表
3.当注册中心中有新的服务出现时,会通知已经订阅发现的消费者,消费者会更新所有服务列表
4.RPC调用,消费者需要调用远程方法时,根据注册中心服务列表的信息,只需服务名称,不需要ip地址和端口号等信息,就可以利用Dubbo调用远程方法了
在实际项目中,一个服务基本都是集群模式的,也就是多个功能相同的项目在运行,这样才能承受更高的并发
这时一个请求到这个服务,就需要确定访问哪一个服务器
Dubbo框架内部支持负载均衡算法,能够尽可能的让请求在相对空闲的服务器上运行
在不同的项目中,可能选用不同的负载均衡策略,以达到最好效果
Loadbalance:就是负载均衡的意思
Dubbo内置4种负载均衡算法
实际运行过程中,每个服务器性能不同
在负载均衡时,都会有性能权重,这些策略算法都考虑权重问题
假设我们当前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运算
以后每次相同参数的请求都会访问固定服务器
因为根据参数选择服务器,不能平均分配到每台服务器上
使用的也不多
Releases · seata/seata · GitHub
https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.zip
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
也是Spring Cloud Alibaba提供的组件
Seata官方文档
Seata
更多信息可以通过官方文档获取
我们之前学习了单体项目中的事务
使用的技术叫Spring声明式事务
能够保证一个业务中所有对数据库的操作要么都成功,要么都失败,来保证数据库的数据完整性
但是在微服务的项目中,业务逻辑层涉及远程调用,当前模块发生异常,无法操作远程服务器回滚
这时要想让远程调用也支持事务功能,就需要使用分布式事务组件Seata
事务的4个特性:ACID特性
Seata保证微服务远程调用业务的原子性
Seata将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata构成部分包含
观察下面事务模型
上面结构是比较典型的远程调用结构
如果account操作数据库失败需要让order模块和storage模块撤销(回滚)操作
声明式事务不能完成这个操作
需要使用Seata来解决
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通知所有分支将数据库操作回滚,恢复成运行之前的样子
AT模式的运行有一个非常明显的前提条件,这个条件不满足,就无法使用AT模式
这个条件就是事务分支都必须是操作关系型数据库(mysql\MariaDB\Oracle)
因为关系型数据库才支持提交和回滚,其它非关系型数据库都是直接影响数据(例如Redis)
所以如果我们在业务过程中有一个节点操作的是Redis或其它非关系型数据库时,就无法使用AT模式
简单来说,TCC模式就是自己编写代码完成事务的提交和回滚
在TCC模式下,我们需要为参与事务的业务逻辑编写一组共3个方法
(prepare\commit\rollback)
prepare:准备
commit:提交
rollback:回滚
这样的话所有提交或回滚代码都由自己编写
优点:虽然代码是自己写的,但是事务整体提交或回滚的机制仍然可用(仍然由TC来调度)
缺点:每个业务都要编写3个方法来对应,代码冗余,而且业务入侵量大
SAGA模式的思想是对应每个业务逻辑层编写一个新的类,可以设置指定的业务逻辑层方法发生异常时,运行当新编写的类中的代码
这样编写代码不影响已经编写好的业务逻辑代码
一般用于修改已经编写完成的老代码
缺点是每个事务分支都要编写一个类来回滚业务,
会造成类的数量较多,开发量比较大
支持XA协议的数据库分布式事务,使用比较少
RM配置
模块需要添加下面pom依赖和配置
io.seata
seata-spring-boot-starter
com.github.pagehelper
pagehelper-spring-boot-starter
com.alibaba
fastjson
下面修改模块的application-dev.yml
代码如下
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地址和端口
TM配置
当模块作为当前分布式事务模型的触发者
它应该是事务的起点,但是它不连接数据库,所以配置稍有不同
pom文件seata依赖仍然需要,但是只需要seata依赖
io.seata
seata-spring-boot-starter
application-dev.yml是一样的
seata:
tx-service-group: csmall_group # 定义分组名称,为了与其它项目区分
service:
vgroup-mapping:
csmall_group: default # csmall_group分组使用Seata的默认配置完成事务
grouplist:
default: localhost:8091 # 配置seata的地址和端口号(8091是默认端口号)
添加完必要的配置之后
要想启动Seata非常简单,只要在起点业务的业务逻辑方法上添加专用的注解即可
添加这个注解的模块就是模型中的TM
他调用的所有远程模块都是RM
@Service
@Slf4j
public class BusinessServiceImpl implements IBusinessService {
// Dubbo调用order模块的新增订单的方法
// 单纯的消费者,不需要在类上添加@DubboService
@DubboReference
private IOrderService dubboOrderService;
// Global全局,Transactional事务
// 一旦编写@GlobalTransactional标记这个方法
// 就相当于设置了分布式事务的起点,当前模块就是分布式事务模型中的TM
// 最终效果是由当前方法调用的所有远程服务中对数据库的操作要么都执行,要么都不执行
@GlobalTransactional
@Override
public void buy() {
// 模拟购买业务
// 代码略...
}
}
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端口的提示即可!
官网地址
home
下载地址
Releases · alibaba/Sentinel · GitHub
Sentinel也是Spring Cloud Alibaba的组件
Sentinel英文翻译"哨兵\门卫"
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
为了保证服务器运行的稳定性,在请求数到达设计最高值时,将过剩的请求限流,保证在设计的请求数内的请求能够稳定完成处理
双11,秒杀,12306抢火车票
可以支持显示当前项目各个服务的运行和压力状态,分析出每台服务器处理的秒级别的数据
很多技术可以和Sentinel进行整合,SpringCloud,Dubbo,而且依赖少配置简单
Sentinel支持程序设置各种自定义的规则
我们的限流针对的是控制器方法
我们找一个简单的模块来测试和观察限流效果
在csmall-stock-webapi模块中
添加sentinel的依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
application-dev.yml文件添加配置
cloud:
sentinel:
transport:
dashboard: localhost:8080 # 配置sentinel仪表台的位置
# 执行限流的端口号,每个项目唯一(别的项目例如cart模块,再设置的话就不能用8721了)
port: 8721
nacos:
discovery:
# 配置Nacos所在的位置,用于注册时提交信息
server-addr: localhost:8848
使用下面命令执行jar包
java -jar sentinel-dashboard-1.8.2.jar
启动之后
打开浏览器http://localhost:8080/
会看到下面的界面
用户名和密码都是
sentinel
刚开始什么都没有,是空界面
后面我们有控制器的配置就会出现信息了
演示限流的效果
StockController在减少库存的方法上添加限流的注解
@PostMapping("/reduce/count")
@ApiOperation("减少商品库存数")
// @SentinelResource注解标记的控制层方法,会在运行时被Sentinel进行管理
// 在这个控制层方法第一次运行后,可以在Sentinel仪表台界面中设置限流规则
// "减少库存的方法"设置了当前方法在仪表台显示的名称
@SentinelResource("减少库存的方法")
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注解中,可以自定义处理限流情况的方法
@PostMapping("/reduce/count")
@ApiOperation("减少商品库存数")
// @SentinelResource注解标记的控制层方法,会在运行时被Sentinel进行管理
// 在这个控制层方法第一次运行后,可以在Sentinel仪表台界面中设置限流规则
// "减少库存的方法"设置了当前方法在仪表台显示的名称
// blockHandler是指定限流时运行方法的配置
@SentinelResource(value = "减少库存的方法",blockHandler = "blockError")
public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){
// 调用业务逻辑层
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,
"服务器忙,请稍后再试");
}
重启stock-webapi模块
再次尝试被限流,观察被限流的提示
单纯的限制在一秒内有多少个请求访问控制器方法
限制的是使用当前服务器的线程数
所谓降级就是正常运行控制器方法的过程中
控制器方法发生了异常,Sentinel支持我们运行别的方法来处理异常,或运行别的业务流程处理
我们也学习过处理控制器异常的统一异常处理类,和我们的降级处理有类似的地方
但是Sentinel降级方法优先级高,而且针对单一控制器方法编写
StockController类中@SentinelResource注解中,可以定义处理降级情况的方法
@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("库存减少已执行!");
}
// 限流方法略.....
// 降级方法:上面@SentinelResource中fallback指定的降级方法
// 声明格式:基本和限流方法相同,方法参数不需要添加异常类型
// 当控制器方法运行发送异常时,Sentinel会自动调用这个方法
// 实际业务中,可以是新版的业务发生异常,然后转而运行老版代码的机制
public JsonResult fallbackError(StockReduceCountDTO stockReduceCountDTO){
// 因为没有老版本代码可用,所以也是返回错误信息
return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,
"运行发生异常,服务降级!");
}
重启模块测试
当发生随机异常时,就运行降级方法
当没有发生随机异常时,就正常运行!
早期(2020年前)奈非提供的微服务组件和框架受到了很多开发者的欢迎
这些框架和SpringCloud Alibaba的对应关系我们要了解
现在还有很多旧项目维护是使用奈非框架完成的微服务架构
Nacos对应Eureka都是注册中心
Dubbo对应Ribbon+feign都是实现微服务远程RPC调用的组件
Sentinel对应Hystrix都是做项目限流熔断降级的组件
Gateway对应Zuul都是网关组件
Gateway框架不是阿里写的,是Spring提供的
"网"指网络,"关"指关口或关卡
网关:就是指网络中的关口\关卡
网关就是当前微服务项目的"统一入口"
程序中的网关就是当前微服务项目对外界开放的统一入口
所有外界的请求都需要先经过网关才能访问到我们的程序
提供了统一入口之后,方便对所有请求进行统一的检查和管理
网关项目git地址
jtzhanghl/gateway-demo
网关的主要功能有
路由的近义词就是"分配"
我们使用Spring Gateway作为当前项目的网关框架
Spring Gateway是Spring自己编写的,也是SpringCloud中的组件
SpringGateway官网
Spring Cloud Gateway
网关项目git地址
jtzhanghl/gateway-demo
SpringGateway网关是一个依赖,不是一个软件
所以我们要使用它的话,必须先创建一个SpringBoot项目
这个项目也要注册到Nacos注册中心,因为网关项目也是微服务项目的一个组成部分
beijing和shanghai是编写好的两个项目
gateway项目就是网关项目,需要添加相关配置
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-loadbalancer
我们从yml文件配置开始添加
server:
port: 9000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
# 网关也是微服务项目的一部分,所以也要注册到Nacos
server-addr: localhost:8848
gateway:
# routes是一个数组,数组中的数据使用"-"开头表示数据中的一个对象
routes: # 开始编写Gateway路由配置
# 当前路由的名称,和任何其他名称没有关联,只是不能和后面再出现的路由名称重复
- id: gateway-beijing
# 当匹配当前路由设置时,访问指定的服务器名称(Nacos注册的服务器名称)
# lb是LoadBalance的缩写,是负载均衡的调用
uri: lb://beijing
# 编写断言配置,断言的意思就是满足指定条件时运行某些事情
# predicates:断言
predicates:
# 断言中我们编写当路径满足指定条件时
# 当请求路径以/bj/开头时,就会路由到上面设置好的beijing服务器运行
# ↓ P大写!!!!!!!!
- Path=/bj/**
报错信息特别长的
然后停掉所有服务重启
是因为当前计算机wifi网卡配置或防火墙软件导致的,可以无视
路由规则解释
如果路径是 /base/business开头的, 就去找nacos-business服务器
如果路径是 /base/cart开头的, 就去找nacos-cart服务器
如果路径是 /base/order开头的, 就去找nacos-order服务器
如果路径是 /base/stock开头的, 就去找nacos-stock服务器
gateway项目
如果路径是 /bj开头 就去找beijing服务器
如果路径是 /sh开头 就去找shanghai服务器
下面对项目所有服务器添加路由配置信息
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/**
上面的yml配置了bejing和shanghai项目的路由信息
我们使用
http://localhost:9000/bj/show可以访问beijing服务器的资源
http://localhost:9000/sh/show可以访问shanghai服务器的资源
以此类推,再有很多服务器时,我们都可以仅使用9000端口号来将请求路由到正确的服务器
就实现了gateway成为项目的统一入口的效果
网关项目随着微服务数量的增多
gateway项目的yml文件配置会越来越多,维护的工作量也会越来越大
所以我们希望gateway能够设计一套默认情况下自动路由到每个模块的路由规则
这样的话,不管当前项目有多少个路由目标,都不需要维护yml文件了
这就是我们SpringGateway的动态路由功能
配置文件中开启即可
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
判断当前时间在指定时间之前,之后或之间的操作
如果条件满足可以执行路由操作,否则拒绝访问
表示时间的格式比较特殊,先使用下面代码获得时间
ZonedDateTime.now()
运行程序输出,可获得当前时间,这个时间的格式可能是
2022-08-25T10:11:32.694+08:00[Asia/Shanghai]
下面在yml配置中添加新的断言配置
使用After设置必须在指定时间之后访问
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设置必须在指定时间之前访问
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 必须满足所有断言条件才能成功路由
# 路径必须是sh开头 && 实际必须在下面时间之后
- Before=2022-08-25T10:22:10.694+08:00[Asia/Shanghai]
使用Between设置必须在指定时间之间访问
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断言,判断是否包含指定的参数名称,包含参数名称才能通过路由
routes: # 开始编写Gateway路由配置
- id: gateway-shanghai
uri: lb://shanghai
predicates:
- Path=/sh/**
# 当前请求必须包含名为name的参数,才能正常路由
- Query=name
Gateway还提供的内置过滤器
不要和我们学习的filter混淆
内置过滤器允许我们在路由请求到目标资源的同时,对这个请求进行一些加工或处理
常见过滤器也有一些
我们给大家演示一下AddRequestParameter过滤器
它的作用是在请求中添加参数
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;
}
}
重启shanghai和gateway进行测试
http://localhost:9000/sh/show?name=tom
因为过滤器的存在,控制器可以获取网关过滤器添加的参数值
其他内置过滤器和自定义过滤器的使用,同学们可以查阅相关文档自己了解
我们希望配置网关之后,在使用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 get() {
//接口资源列表
List resources = new ArrayList<>();
//服务名称列表
List 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 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;
}
}
cn.tedu.gateway.controller
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> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("")
public Mono swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
cn.tedu.gateway.filter
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);
};
}
}
之前网关的演示项目我们添加的网关依赖
org.springframework.cloud
spring-cloud-starter-gateway
当前csmall项目需要配置knife4j的路由配置,需要编写一个控制器
所以我们添加了SpringMvc的依赖
org.springframework.boot
spring-boot-starter-web
这两个依赖在同一个项目中时,默认情况下启动会报错
SpringMvc框架中自带一个Tomcat服务器
而SpringGateway框架中自带一个Netty的服务器
在启动项目时,两个框架中包含的服务器都想占用相同端口,因为争夺端口号和主动权而发生冲突
导致启动服务时报错
要想能够正常启动必须在yml文件配置
spring:
main:
web-application-type: reactive
reactive:反应的
添加这个配置之后,会Tomcat服务器会变成非阻塞的运行
官方下载链接
https://www.elastic.co/cn/downloads/past-releases#elasticsearch
elastic:富有弹性的
search:搜索
我们可以把它简称为ES,但是搜索它的资料时(例如百度)还是使用Elasticsearch进行搜索更准确
这个软件不再是SpringCloud提供的,它也不针对微服务环境的项目来开发
Elasticsearch和redis\mysql一样,不仅服务与java语言,其它语言也可以使用
它的功能也类似一个数据库,能高效的从大量数据中搜索匹配指定关键字的内容
它也将数据保存在硬盘中
这样的软件有一个名称全文搜索引擎
它本质就是一个java项目,使用它进行数据的增删改查就是访问这个项目的控制器方法(url路径)
ES的底层技术
ES使用了java的一套名为Lucene的API
这个API提供了全文搜索引擎核心操作的接口,相当于搜索引擎的核心支持,ES是在Lucene的基础上进行了完善,实现了开箱即用的搜索引擎软件
市面上和ES功能类似的软件有
Solr/MongoDB
数据库进行模糊查询严重低下
所有关系型数据库都有这个缺点(mysql\mariaDB\oracle\DB2等)
在执行类似下面模糊查询时
select * from spu where spu_name like '%鼠标%'
测试证明一张千万级别的数据表进行模糊查询需要20秒以上
当前互联网项目要求"三高"的需求下,这样的效率肯定不能接受
Elasticsearch主要是为了解决数据库模糊查询性能低下问题的
ES进行优化之后,从同样数据量的ES中查询相同条件数据,效率能够提高100倍以上
所谓的索引(index)其实就是数据目录
通常情况下,索引是为了提高查询效率的
数据库索引分两大类
聚集索引就是数据库保存数据的物理顺序依据,默认情况下就是主键id,所以按id查询数据库中的数据效率非常高
非聚集索引
如果想在非主键列上添加索引,就是非聚集索引了
例如我们在数据库表中存在一个姓名列,我们为姓名列创建索引
在创建索引时,会根据姓名内容来创建索引
例如"张三" 这个姓名,创建索引后查询效率就会明显提升
如果没有索引,这样的查询就会引起效率最低的"逐行搜索",就是一行一行的查这个数据的姓名是不是张三,效率就会非常低
模糊查询时因为'%鼠标%',使用的是前模糊条件,使用索引必须明确前面的内容是什么,前模糊查询是不能使用索引的,只能是全表的逐行搜索,所以效率非常低
所以当我们项目中设计了根据用户输入关键字进行查询时,需要使用全文搜索引擎来优化
索引面试题
1.创建的索引会占用硬盘空间
2.创建索引之后,对该表进行增删改操作时,会引起索引的更新,所以效率会降低
3.对数据库进行批量新增时,先删除索引,增加完毕之后再创建
4.不要对数据样本少的列添加索引
5.模糊查询时,查询条件前模糊的情况,是无法启用索引的
6.每次从数据表中查询的数据的比例越高,索引的效果越低
7.当我们执行查询时,where条件后应该先查询有索引的列
要想使用ES提高模糊查询效率
首先要将数据库中的数据复制到ES中
在新增数据到ES的过程中,ES可以对指定的列进行分词索引保存在索引库中
形成倒排索引结构
这里使用7.6.2的版本
压缩包280M左右,复制到没有中文,没有空格的目录下解压
双击bin\elasticsearch.bat运行
elasticsearch.bat
双击之后可能会看到下面的dos界面
这个界面不能关闭,一旦关闭ES就停止了
验证ES的运行状态
浏览器输入地址:localhost:9200看到如下内容即可
mac系统启动
tar -xvf elasticsearch-7.6.2-darwin-x86_64.tar.gz
cd elasticsearch-7.6.2/bin
./elasticsearch
linux:
tar -xvf elasticsearch-7.6.2-linux-x86_64.tar.gz
cd elasticsearch-7.6.2/bin
./elasticsearch
ES启动完成后,我们要学习如何操作它
我们已经讲过,操作ES是对ES发送请求
我们创建一个子项目search,在这个子项目中创建一个专门发送各种类型请求的文件来操作ES
创建search项目也要父子相认
然后子项目pom文件如下
4.0.0
cn.tedu
csmall
0.0.1-SNAPSHOT
cn.tedu
search
0.0.1-SNAPSHOT
search
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter
删除test文件夹
下面创建一个能够向ES发送请求的文件
这种能够向指定url发送请求的文件格式称之为http client(http 客户端)
文件类型叫HTTP Request文件
我们可以起名为elasticsearch
我们先从最简单的请求开始
向es发送指令
### 三个#是注释,也是分隔符,这个文件中每个请求代码之间都要用###分隔
GET http://localhost:9200
### 测试ES的分词功能,运行分词,查看分词效果
POST http://localhost:9200/_analyze
Content-Type: application/json
{
"text": "罗技激光鼠标",
"analyzer": "standard"
}
analyze:分析
analyzer:分析者(分词器)
standard是ES默认的分词器,"analyzer": "standard"是可以省略的
standard这个分词器只能对英文等西文字符(有空格的),进行正确分词
但是中文分词不能按空格分,按这个分词器分词,每个字都会形成分词,这样的结果不能满足我们日常的搜索需要
我们解决中文不能正确分词的问题
实际上要引入一个中文常见词语的词库,分词时按照词库中的词语分词即可
我们可以使用免费的中文分词器词库插件IK来实现中文分词效果
安装插件之后要重启ES才能生效
关闭Es窗口之后再双击运行即可
ES启动之后,将中文分词器插件设置完成,在运行分词
{
"text": "罗技激光无线游戏鼠标",
"analyzer": "ik_smart"
}
再次运行分词测试,应该看到正常的中文分词效果
我们安装的ik实际上不只一个分词器
实际上除了ik_smart之外还有ik_max_word
POST http://localhost:9200/_analyze
Content-Type: application/json
{
"text": "北京成功举行了冬季奥林匹克运动会",
"analyzer": "ik_smart"
}
POST http://localhost:9200/_analyze
Content-Type: application/json
{
"text": "北京成功举行了冬季奥林匹克运动会",
"analyzer": "ik_max_word"
}
上面的两个分词器运行分词,结果会有非常明显的区别
总结区别如下
ik_smart
ik_max_word
ES是一个数据库性质的软件
可以执行增删改查操作,只是他操作数据不使用sql,数据的结构和关系型数据库也不同
我们先了解一下ES保存数据的结构
原生状态下,我们使用JDBC连接数据库,因为代码过于繁琐,所以改为使用Mybatis框架
在ES的原生状态下,我们java代码需要使用socket访问ES,但是也是过于繁琐,我们可以使用SpringData框架简化
Spring Data是Spring提供的一套连接各种第三方数据源的框架集
我们需要使用的是其中连接ES的Spring Data Elasticseatrch
官方网站:Spring Data
官网中列出了SpringData支持连接操作的数据源列表
下面我们就按照SpringDataElasticsearch的步骤对ES进行操作
就使用我们之前创建的search模块来操作ES
pom文件添加依赖
4.0.0
cn.tedu
csmall
0.0.1-SNAPSHOT
cn.tedu
search
0.0.1-SNAPSHOT
search
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-data-elasticsearch
org.springframework.boot
spring-boot-starter-test
application.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
和数据库一样
我们操作ES时也需要一个类似实体类的数据类,作为操作ES的数据载体
search项目创建entity包
在包中创建Item(商品)类
@Data
@Accessors(chain = true) // 支持链式set赋值功能
@AllArgsConstructor // 自动生成当前类的全参构造
@NoArgsConstructor // 自动生成当前类的无参构造
// @Document是SpringDataES框架标记实体类的注解
// indexName指定的是索引名称,运行时items索引不存在,SpringDataES会自动创建这个索引
@Document(indexName = "items")
public class Item implements Serializable {
// SpringData标记当前属性为ES主键的注解
@Id
private Long id;
// SpringData标记title属性的支持分词的类似和相关分词器
@Field(type = FieldType.Text,
analyzer = "ik_max_word",
searchAnalyzer = "ik_max_word")
private String title;
// Keyword是不需要分词的字符串类型
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Keyword)
private String brand;
@Field(type = FieldType.Double)
private Double price;
// 图片地址不会称为搜索条件,所以不需要进行索引,不索引能节省一些数据空间
// 设置index=false 今后所有不会称为查询条件的列都照此配置
// 不索引,不代表不保存数据,数据本身仍然是保存在ES的
@Field(type = FieldType.Keyword,index = false)
private String imgPath;
// images/xxx/xxx/a09f-887ac-ac006-7128311231234
}
我们使用SpringData连接ES
需要使用SpringData框架对持久层的命名规则
持久层规范名称为repository(仓库),创建这个包,包中创建接口ItemRepository
ItemRepository
// Repository 是spring家族框架对持久层的命名
@Repository
public interface ItemRepository extends
ElasticsearchRepository- {
// ItemRepository接口要继承SpringData提供的ElasticsearchRepository父接口
// 一旦继承,当前接口就会被识别为连接ES的持久层类,SpringData会自动为它生成基本增删改查方法
// ElasticsearchRepository<[关联的实体类名称],[实体类主键类型]>
}
创建test测试包
创建测试类
编写测试
// 测试类必须添加下面的注解,否则无法运行
@SpringBootTest
public class SpringEsTest {
// 装配ItemRepository
@Autowired
private ItemRepository itemRepository;
// 执行单增
@Test
void addOne(){
// 实例化一个Item对并赋值
Item item=new Item()
.setId(1L)
.setTitle("罗技激光无线游戏鼠标")
.setCategory("鼠标")
.setBrand("罗技")
.setPrice(168.0)
.setImgPath("/1.jpg");
// 利用SpringDataES提供的方法完成新增功能
itemRepository.save(item);
System.out.println("ok");
}
// 单查
@Test
void getOne(){
// SpringDataES提供了按id查询ES中数据的方法
// Optional是一个类似包装类的概念,查询结果封装到这个类型中
Optional- optional=itemRepository.findById(1L);
Item item=optional.get();
System.out.println(item);
}
// 批量增
@Test
void addList(){
// 实例化一个List对象
List
- list=new ArrayList<>();
// 将要新增的对象保存在List中
list.add(new Item(2L,"罗技激光有线办公鼠标","鼠标",
"罗技",88.0,"/2.jpg"));
list.add(new Item(3L,"雷蛇机械无线游戏键盘","键盘",
"雷蛇",299.0,"/3.jpg"));
list.add(new Item(4L,"微软有线静音办公鼠标","鼠标",
"微软",205.0,"/4.jpg"));
list.add(new Item(5L,"罗技机械有线背光键盘","键盘",
"罗技",268.0,"/5.jpg"));
itemRepository.saveAll(list);
System.out.println("ok list");
}
// 全查
@Test
void getAll(){
// SpringDataES对ES全查(指定索引)返回数据的方法
Iterable
- items=itemRepository.findAll();
for(Item item : items){
System.out.println(item);
}
items.forEach(item -> System.out.println(item));
}
}
SpringData框架提供的基本增删改查方法并不能完全满足我们的业务需要
如果是针对当前Es数据,进行个性化的自定义查询,那还是需要自己编写查询代码
就像我们要实现根据关键词查询商品信息一样,完成类似数据库中的模糊查询
我们查询需求为输出所有数据中title属性包含"游戏"这个分词的商品信息
参考数据库中模糊查询
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- queryItemsByTitleMatches(String title);
下面可以开始在测试类中进行测试查询
//单条件查询
@Test
void queryOne(){
// 查询ES中items索引中,title字段包含"游戏"关键字的数据
Iterable- items=itemRepository.queryItemsByTitleMatches("激光游戏");
items.forEach(item -> System.out.println(item));
}
上面代码运行时底层运行的查询语句为:
### 单条件搜索
POST http://localhost:9200/items/_search
Content-Type: application/json
{
"query": {"match": { "title": "游戏" }}
}
在相对复杂的查询逻辑下
经常使用多个条件来定位查询需要的数据
这样就需要逻辑运算符"and"/"or"
ItemRepository接口中添加多条件的查询方法
// 多条件查询
// 多个条件之间我们需要使用And和Or来分隔,来表示他们的查询逻辑
// 方法的参数赋值是依据方法定义的参数顺序依次向条件中赋值的
Iterable- queryItemsByTitleMatchesAndBrandMatches(
String title,String brand);
测试代码如下
// 多条件查询
@Test
void queryTwo(){
// 查询ES中items索引中,title字段包含"游戏"并且品牌是"罗技"的数据
Iterable- 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- queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
String title,String brand);
测试
// 排序查询
@Test
void queryOrder(){
Iterable- items=itemRepository
.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
"游戏","罗技");
items.forEach(item -> System.out.println(item));
}
底层运行的代码
### 多字段搜索
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- queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
String title, String brand, Pageable pageable);
测试代码
// 分页查询
@Test
void queryPage(){
int pageNum=2; // 要查询的页码
int pageSize=2; // 每页包含的数据条数
Page- 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());
@Highlight(
parameters = @HighlightParameters(
preTags = "",
postTags = ""
),
fields = @HighlightField(name = "title")
)
List> findByTitle(String title, Pageable page);
测试
@Test
void highLight(){
Pageable p=PageRequest.of(0,10);
List> list=itemRepository.findByTitle("鼠标",p);
for(SearchHit- item:list){
System.out.println(item);
}
}
查询结果
2022-09-02 10:35:54.466 DEBUG 1628 --- [ main] org.elasticsearch.client.RestClient : request [POST http://localhost:9200/items/_search?typed_keys=true&max_concurrent_shard_requests=5&ignore_unavailable=false&expand_wildcards=open&allow_no_indices=true&ignore_throttled=true&search_type=dfs_query_then_fetch&batched_reduce_size=512&ccs_minimize_roundtrips=true] returned [HTTP/1.1 200 OK]
SearchHit{id='1', score=0.57122886, sortValues=[], content=Item(id=1, title=罗技激光无线游戏鼠标, category=鼠标, brand=罗技, price=168.0, imgPath=/1.jpg), highlightFields={title=[罗技激光无线游戏鼠标]}}
SearchHit{id='4', score=0.5314989, sortValues=[], content=Item(id=4, title=微软有线静音办公鼠标, category=鼠标, brand=微软, price=205.0, imgPath=/4.jpg), highlightFields={title=[微软有线静音办公鼠标]}}
SearchHit{id='2', score=0.49693614, sortValues=[], content=Item(id=2, title=罗技激光有线办公鼠标, category=鼠标, brand=罗技, price=88.0, imgPath=/2.jpg), highlightFields={title=[罗技激光有线办公鼠标]}}
所谓分页,就是查询结果数据较多时,采用按页显示的方法,而不是一次性全部显示
实现分页查询需要我们开发过程中多几个步骤
我们可以使用sql语句中添加limit关键字的方法实现分页查询
但是查询分页内容时,我们要自己计算相关的分页信息和参数
limit 0,10 limit 10,10
分页逻辑无论什么业务都是类似的,所以有框架帮助我们高效实现分页功能
PageHelper框架可以实现我们提供页码和每页条数,自动实现分页效果,收集分页信息
PageHelper的分页原理就是在程序运行时,在sql语句尾部添加limit关键字,并按照分页信息向limit后追加分页数据
要想使用,首先还是添加依赖
com.github.pagehelper
pagehelper-spring-boot-starter
在添加seata支持时已经添加了pagehepler依赖
这里使用csmall-order-webapi模块来完成分页的测试
首先编写分页的持久层mapper,持久层功能是全查所有订单信息
OrderMapper添加方法
// 分页查询全部订单
// PageHelper框架是自动在sql语句后添加limit关键字实现分页的
// 因为这个特性,我们编写的查询语句,和不分页时是一样的
@Select("select id,user_id,commodity_code,count,money from order_tbl")
List findAllOrders();
注意这个方法并不需要任何分页的参数或返回值,sql也不需要编写limit
都是在业务逻辑层中由PageHelper框架处理的
下面就转到业务逻辑层实现类,先编写一个方法使用PageHelper的功能
先不用写接口,直接在业务逻辑层中写方法
OrderServiceImpl添加方法
// 分页查询所有订单的方法
// page 是页码
// pageSize 是每页条数
public PageInfo getAllOrdersByPage(Integer page,Integer pageSize){
// PageHepler框架实现分页最核心的代码就是在运行要分页的查询语句之前
// 通过框架给定的方法设置要分页查询的要求(第几页,每页多少条)
// 参数page和SpringData框架的设计不同,page为1就是第一页,page为2就是第二页
PageHelper.startPage(page,pageSize);
// 当上面设置完分页要求后,下面紧随的下一次查询,
// 就会自动在sql语句末尾添加limit关键字,limit后面的值就是按page,pageSize得出的
List list= orderMapper.findAllOrders();
// list并不是全部的订单的集合,而是按上面分页条件查询出的分页数据
// 在返回时不直接返回list对象,而是返回PageHelper框架提供的PageInfo类型对象
// 这个对象可以保存list集合的同时,还能自动计算分页信息
return new PageInfo<>(list);
}
PageInfo对象既包含查询数据结果,又包含分页信息
附:PageInfo全部分页信息属性
//当前页
private int pageNum;
//每页的数量
private int pageSize;
//当前页的行数量
private int size;
//当前页面第一个元素在数据库中的行号
private int startRow;
//当前页面最后一个元素在数据库中的行号
private int endRow;
//总页数
private int pages;
//前一页页号
private int prePage;
//下一页页号
private int nextPage;
//是否为第一页
private boolean isFirstPage;
//是否为最后一页
private boolean isLastPage;
//是否有前一页
private boolean hasPreviousPage;
//是否有下一页
private boolean hasNextPage;
//导航条中页码个数
private int navigatePages;
//所有导航条中显示的页号
private int[] navigatepageNums;
//导航条上的第一页页号
private int navigateFirstPage;
//导航条上的最后一页号
private int navigateLastPage;
在OrderController类中添加调用分页方法
@GetMapping("/page")
@ApiOperation("分页查询所有订单")
@ApiImplicitParams({
@ApiImplicitParam(value = "页码",name = "page" ,example = "1"),
@ApiImplicitParam(value = "每页条数",name = "pageSize" ,example = "6")
})
public JsonResult> pageOrders(
Integer page,Integer pageSize){
// 执行分页查询的业务逻辑层
PageInfo pageInfo=orderService.getAllOrdersByPage(page,pageSize);
return JsonResult.ok("查询完成",pageInfo);
}
启动Nacos\Seata
启动order
进行knife4j测试http://localhost:20002/doc.html#/home
可以观察控制台输出的运行的sql语句(会自动添加limit关键字)
当前我们分页查询返回的类型是PageInfo
如果用这个类型来做业务逻辑层的返回值,当当前方法作为dubbo生产者对外提供服务时
消费者调用该服务需要使用PageInfo类型对象来接收,这样要求消费者也添加PageHepler依赖,这是不合理的
所以我们设计在commons模块中,添加一个专门用于返回分页结果的类JsonPage,代替PageInfo
这样当前微服务项目中,所有分页或类似的操作,就都可以使用这个类了
例如之前SpringDataElasticsearch框架也支持分页,返回类型为Page,它也可以替换为JsonPage
因为需要在commons模块中使用PageInfo类型,所以commons模块要添加pageHelper的依赖
com.github.pagehelper
pagehelper
5.2.0
在restful包中新建一个JsonPage类
代码如下
// 通用的返回各种类型分页结果的信息类
@Data
public class JsonPage implements Serializable {
// 根据实际需求,定义需要的分页信息
// 实际开发中可能较多,我们这里就声明4个基本的
@ApiModelProperty(value = "总页数",name = "totalPages")
private Integer totalPages;
@ApiModelProperty(value = "总条数",name = "totalCount")
private Long totalCount;
@ApiModelProperty(value = "当前页码",name = "page")
private Integer page;
@ApiModelProperty(value = "每页条数",name = "pageSize")
private Integer pageSize;
// 如果需要再添加其它属性即可
// 除了分页信息,还有查询出的分页数据
@ApiModelProperty(value = "分页数据",name = "list")
private List list;
// 上面定义了所有分页数据需要的属性
// 下面可以编写一个将PageInfo类型转换为JsonPage类型的方法
// 如果需要将其它框架的分页对象转换,例如SpringData的Page类,那么就再编写新的方法即可
public static JsonPage restPage(PageInfo pageInfo){
// 开始进行转换,基本思路是将pageInfo对象中的数据赋值给JsonPage对象
JsonPage result=new JsonPage<>();
// 赋值分页信息
result.setTotalPages(pageInfo.getPages());
result.setTotalCount(pageInfo.getTotal());
result.setPage(pageInfo.getPageNum());
result.setPageSize(pageInfo.getPageSize());
// 赋值分页数据
result.setList(pageInfo.getList());
// 别忘了返回
return result;
}
}
下面去使用这个类
csmall-order-service业务逻辑层接口项目添加方法
返回值使用JsonPage
// 返回JsonPage类型的分页查询全部订单方法
JsonPage getAllOrdersByPage(Integer page,Integer pageSize);
业务逻辑层返回值的修改影响控制器方法的调用
// ↓↓↓↓↓↓↓↓
public JsonPage getAllOrdersByPage(Integer pageNum, Integer pageSize){
PageHelper.startPage(pageNum,pageSize);
List list= orderMapper.findAllOrders();
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
return JsonPage.restPage(new PageInfo<>(list));
}
再去修改OrderController中方法调用的位置
@Autowired
// ↓↓↓↓↓↓↓↓↓↓↓↓
private IOrderService orderService;
//...
// ↓↓↓↓↓↓↓↓
public JsonResult> pageOrders(Integer pageNum, Integer pageSize){
// 分页调用
//↓↓↓↓↓↓ ↓↓↓↓↓↓↓↓↓
JsonPage jsonPage=orderService.getAllOrdersByPage(
pageNum,pageSize);
// ↓↓↓↓↓↓↓↓↓↓
return JsonResult.ok("查询完成",jsonPage);
保证启动Nacos\Seata
重启order测试
能出现查询结果即可
leaf是叶子的意思
我们使用的Leaf是美团公司开源的一个分布式序列号(id)生成系统
我们可以在Github网站上下载项目直接使用
上面的图片中
是一个实际开发中常见的读写分离的数据库部署格式
专门进行数据更新(写)的有两个数据库节点
它们同时新增数据可能产生相同的id
一旦生成相同的id,数据同步就会有问题
会产生id冲突,甚至引发异常
我们为了在这种多数据库节点的环境下能够产生唯一id
可以使用Leaf来生成
Leaf底层支持通过"雪花算法"生成不同id
我们使用的是单纯的序列
要想使用,需要事先设置好leaf的起始值和缓存id数
举例,从1000开始缓存500
也就是从id1000~1499这些值,都会保存在Leaf的内存中,当有服务需要时,直接取出下一个值
取出过的值不会再次生成
leaf要想设置起始值和缓存数
需要给leaf创建一个指定格式的数据库表
运行过程中会从数据库表获取信息
我们当前的信息保存在leafdb.leaf_alloc表中
获取Id:
private static final RestTemplate restTemplate = new RestTemplate();
public static Long getDistributeId(String key) {
String url = "http://127.0.0.1:9090/api/segment/get/" + key;
return restTemplate.getForObject(url, Long.class);
}