目录
3、服务远程调用(使用RestTemplate发送http请求)
2、 负载均衡策略IRule(修改eureka拉取服务后选择的方式)
3.自定义镜像文件dockerfile(docker部署java项目)
5、使用workqueue模型(再AMQP完成基本消息队列基础上)
创建elasticsearch容器(这里挂载的数据卷再/var/lib/docker/volumes当中)
a、初始化restclient(ResthighlevelClient)
一、微服务架构
1、架构
- 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
- 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
- 微服务:一种良好的分布式架构方案
①优点:拆分粒度更小、服务更独立、耦合度更低
②缺点:架构非常复杂,运维、监控、部署难度提高
- SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件
2、 服务拆分
服务拆分原则
这里我总结了微服务拆分时的几个原则:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
3、服务远程调用(使用RestTemplate发送http请求)
第一步:注册所需要的bean对象
@SpringBootConfiguration
public class RestTemplateConfig {
//.注册spring发送http请求RestTemplate
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
第二步,一般在service里面发送请求,这里面的user-service我们写在另一个module里面
@Service
public class OrderService { //这里我们发送的是get请求
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2.访问路径
String url = "http://localhost:8081/user/"+ order.getUserId();
//3.利用RestTemplate发送http请求,查询order当中的userid所对应的对象
User user = restTemplate.getForObject(url, User.class);
// 4.返回
order.setUser(user);
return order;
}
}
4、Eureka
cloud-demo
1.Eureka的结构和作用
问题1:order-service如何得知user-service实例地址?
获取地址信息的流程如下:
- user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
- eureka-server保存服务名称到服务实例地址列表的映射关系
- order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取问题2:order-service如何从多个user-service实例中选择具体的实例?
- order-service从实例列表中利用负载均衡算法选中一个实例地址
- 向该实例地址发起远程调用问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
- user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
- 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
- order-service拉取服务时,就能将故障实例排除了
2、搭建Eureka服务
这个一般重新创建一个moduo来作为eureka服务
(1)依赖文件
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
(2)编写启动类
在启动类加入注解::
@SpringBootApplication
@EnableEurekaServer // Eureka启动注解
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
(3)配置文件
、、、yml
server:
port: 10086
spring:
application:
name: eureka-server #配置服务器名称
eureka:
client:
service-url: #配置服务器地址
defaultZone: http://127.0.0.1:10086/eureka
3、注册eureka-server服务
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yaml配置
spring:
application:
name: orderservice #服务器名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka 服务器网址
4.服务拉取和负载均衡
将前面服务远程调用当中直接写端口变为eureka服务拉取
5、负载均衡
1.负载均衡流程图
基本流程
拦截我们的RestTemplate请求http://userservice/user/1
- RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
- DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求
2、 负载均衡策略IRule(修改eureka拉取服务后选择的方式)
一般都是在消费者哪里配置(即使用了服务远端调用),如果配置的是bean,就是全局的,如果在yml文件当中配置,就是针对单个服务的。
(1)bean方式配置(这个是配置在我们)
//配置负载均衡调用选择服务的方式,这是随机方式
@Bean
public IRule randomRule(){
return new RandomRule();
}
(2)yml文件配置
order的yml配置文件当中::
userservice: #配置userservice负载均衡规则
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3、饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
```yaml 一般配置在需要消费则的配置文件当中
ribbon:
eager-load:
enabled: true 开启饥饿加载
clients: userservice 指定饥饿加载的服务器名称
二、Nacos
(1)Nacos使用(比eureka服务更加丰富)
1、启动命令
bin目录下:startup.cmd -m standalone(后面加的单词意思是单机启动)
2、在父配置文件当中添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
3、在客户端添加依赖和配置文件
、、、xml 依赖文件
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
```yaml配置文件(order,user等的配置文件里)
spring:
cloud:
nacos:
server-addr: lcalhost:8848
4、两个客户端都添加Nacos依赖配置后,我们之前使用 eureka去调取服务,就会换成Nacos去拉取服务(下面的是使用restTemplate发送http请求调取服务的具体操作)
(2)Nacos多级存储模型(负载均衡)
(二级)集群的方式
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
但是要是想让消费者也按照根据集群选取服务器的方式,也得配置集群名称
2)修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:
```yaml
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
```
(3)Nacos 环境隔离 and 设置临时实例
### 5.5.2.给微服务配置namespace
给微服务配置namespace只能通过修改配置来实现。
例如,修改order-service的application.yml文件:
```yaml
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ 集群名称# 命名空间,填ID 会根据设置的环境来划分我们的服务
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9ephemeral: false # 设置为非临时实例
(4)Nacos配置管理
1、统一配置管理
第一步:在Nacos发布配置信息
idea当中
第二步:在配置文件当中添加依赖
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
第三步:在resources添加bootstrap.yaml,因为这个配置文件优先级在application.yml配置文件之前,所以会先读取这个配置文件,在与本身配置文件合并使用
2)添加bootstrap.yaml,在需要使用Nacos里面配置的服务里配置
```yaml
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
```
第四步:将nacos当中的
第五步:查看是否有调取到我们配置在Nacos当中的配置
2、配置热更新
需要调用我们配置在Nacos配置文件当中的配置的话,使用热部署可以使用下面两种方式进行
第一种方式就是上面统一配置管理的查看是否调用
第二种配置,就是创建一个pojo类来获取配置文件里的属性(第三方注入bean对象)
3、 多环境共享配置
不管我们bootstrap里面使用的是那一种active开发环境配置,服务名.yaml配置一定会读取
所以我们使用它当作共享文件配置
4、 nacos集群的搭建
我们是在同一台电脑上设置的多发nacos,所以要注意端口号不能冲突
文件在java包下的springcloud/基础篇/02里面!
三、Feign
1、远程调用,代替RestTemplate发送http请求
第一步:引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:在需要使用feign的服务启动类上添加注解
@EnableFeignClients //feign启动注解
第三步: 编写Feign客户端
@FeignClient("userservice")//服务名称
public interface UserClient {
@GetMapping("/user/{id}") //请求路径参数等
User findById(@PathVariable("id")Long id);
}
第四步调用:
直接创建接口对象,调用方法
2、自定义配置
***修改日志配置
第二种方式 :通过代码的方式
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;//日志级别
}
}
设置日志级别后再到需要添加日志级别的服务上将日志类添加进去
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)//userservice服务名称
public interface UserClient {
@GetMapping("/user/{id}") //请求路径参数等
User findById(@PathVariable("id") Long id);
}
***feign性能优化
第一步:导入依赖
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
第二步:配置优化(在yaml文件当中)
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
***feign最佳实践
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
第一步:创建feign-api的moduo,添加feign依赖
<!--feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:将所需要的pojo,修改日志文件类,远程调用类创建出来
第三步:在orderservice(消费者)引入我们的feign-api的module,
第四步:在消费者启动类上添加的feign启动注解指定feign位置
@EnableFeignClients(basePackages = "cn.itcast.fegin.client") //feign启动注解
四、gateway
Gateway网关是我们服务的守门神,所有微服务的统一入口。
1、网关服务的搭建
第一步:创建网关gateway的module,导入网关所需要的依赖和nacos注册依赖
<!--网关-->
<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>
第二步:编写springboot启动类
@SpringBootApplication
public class GatewayApplication
{
public static void main( String[] args )
{
SpringApplication.run(GatewayApplication.class,args);
}
}
第三步:网关配置
server:
port: 10010 #网关端口
spring:
application:
name: gateway #路由的id
cloud:
nacos:
server-addr: localhost:8848 #nacos服务地址
gateway:
routes: #网关路由地址
- id: user-service
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user开头就符合要求
总结:本例中,我们将 `/user/**`开头的请求,代理到`lb://userservice`,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
2、路由断言工厂
3、配置路由过滤器
再第一点网关服务搭建下面直接添加就行
server:
port: 10010 #网关端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 #nacos服务地址
gateway:
routes: #网关路由地址
- id: userservice
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user开头就符合要求
filters: #过滤器 有很多很多种,下面只是其中一种
- AddRequestHeader=Truth, xushihao! # 添加请求头
上面这种的话只是对单一的服务有效,并不是全局的,如果想要让它全局,可以将过滤器配置在default里面(对所有的路由都有效)
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Itcast is freaking awesome!
4、全局的过滤器配置
配置自定义的全局过滤器,主要是实现接口GlobalFilter
@Order(-1)//过滤器执行顺序
@Component
public class AuthorizeFilters implements GlobalFilter {//全局的过滤器,一般是用于验证登录
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
//获取参数中的authorizeFilters
String auth = params.getFirst("authorizeFilters");
//判断参数值是否为admit,如果
if(auth.equals("admit")){
//放行
return chain.filter(exchange);
}
//拦截
//禁止访问,设置状态码401
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
//结束处理
return exchange.getResponse().setComplete();
}
}
- 每一个过滤器都必须指定一个int类型的order值,**order值越小,优先级越高,执行顺序越靠前**。
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
- 当过滤器的order值一样时,会按照 defaultFilter(默认过滤器) > 路由过滤器 > GlobalFilter的顺序执行。
五、Docker
1、docker架构
镜像:
- 将应用程序及其依赖、环境、配置打包在一起
容器:
- 镜像运行起来就是容器,一个镜像可以运行多个容器
Docker结构:
- 服务端(server):接收命令或远程请求,操作镜像或容器
- 客户端(client):发送命令或者请求到Docker服务端
DockerHub:
- 一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
2、docker基本操作
*1docker启动操作命令
systemctl start docker # 启动docker服务
systemctl stop docker # 停止docker服务
systemctl restart docker # 重启docker服务
*2.镜像的基本操作
1)去DockerHub搜索Redis镜像(镜像仓库:::https://hub.docker.com/):
2)查看Redis镜像的名称和版本
3)利用docker pull命令拉取镜像(例子看上面的图片)
4)利用docker save命令将 redis:latest打包为一个redis.tar包(docker save -o nginx.tar nginx:latest)
5)利用docker rmi 删除本地的redis:latest (docker rmi nginx:latest)
6)利用docker load 重新加载 redis.tar文件(docker load -i nginx.tar)
不会直接 docker xx --help
*3、容器相关命令
*** 创建一个容器
--查看docker那些容器再运行 docker ps
--删除容器docker rm 容器名
***如果要修改容器那些文件之类的
--进入容器docker exec -it 容器名字 redis-cli (这个就是进入redis容器启动redis-cli,即启动redis)
*4、数据卷相关命令
数据卷是一个虚拟目录,指向宿主机文件系统中的某个目录。
一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。
这样,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了
redis的一般创建容器挂载数据卷
mysql的容器创建的一般操作
docker run \
--name mysql \ 数据库名称
-p 3306:3306 \ 端口号
-e MYSQL_ROOT_PASSWORD=123 \ 数据库连接密码
-v /root/docker/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ 数据库配置文件
-v /root/docker/mysql/data:/var/lib/mysql / 数据库存储数据位置
-d mysql:5.7.25 镜像的名称
3.自定义镜像文件dockerfile(docker部署java项目)
下面这个是基于ubuntu来构建镜像的,比较繁琐
所以,我们一般使用下面的方法,基于java8的环境去构建,基于java8的话我们就不用去构建jdk的环境
Dockerfile文件
# 指定基础镜像(用这个jdk环境那些可以省去些步骤)
FROM java:8-alpine
COPY ./docker-demo.jar(这个是java程序打包过来后存放的位置) /tmp/app.jar(这个是程序拷贝到的地址)# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
使用docker build命令构建镜像::
docker build -t javaweb2.0 . (javaweb2.0是镜像名称,后面空格. 这个是当前目录,就是指向jar,和dockerfile文件所在的文件夹)
4、使用DockerCompose部署微服务
就是一次性创建多个容器
注意::web构建容器里面是先使用dockers build构建镜像,而我们的compse方法就是直接build: . 然后就会到compose这个文件所在的目录去寻找所需要的dockerfile文件,所以,我们使用这个方法构建镜像直接指出dockerfile文件的目录就可以了
1、自定义镜像仓库(创建,打包,推送,拉取)
第一步:docker信任这个镜像仓库地址,我们才能够去访问
第二步:创建一个docker-compose.yml文件,将下面内容放入进去
```yaml
version: '3.0'
services: 这个就是镜像仓库
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui: 这个是我们镜像仓库的一个图形化界面
image: joxit/docker-registry-ui:static
ports:
- 8080:80 图形界面端口访问端口
environment:
- REGISTRY_TITLE=传智教育私有仓库 图形界面仓库名称
- REGISTRY_URL=http://registry:5000 这个是仓库和图形界面内部的访问端口,我们是无法使用的
depends_on: 这个表示我们这个图形界面依赖于谁,这里是依赖于registry
- registry
第三步:进入yml文件所在的文件夹,执行docker-compose up -d 命令来构建镜像仓库
第四步:如果需要推送镜像到镜像仓库
必须先tag重命名,并且以仓库地址作为前缀。
docker tag nginx:latest(需要打包的镜像名称) 192.168.91.128:8080/nginx:1.0(打包后的镜像名称)
第五步:上传命令
使用docker push + 打包后的镜像名称
第六步: 如果再次需要拉取的话
使用docker pull + 镜像仓库pull名称
六、 Rabbitmq
1、同步异步优缺点
同步调用的优点:
- 时效性较强,可以立即得到结果
同步调用的问题:
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失败问题异步好处:
- 吞吐量提升:无需等待订阅者处理完成,响应更快速
- 故障隔离:服务没有直接调用,不存在级联失败问题
- 调用间没有阻塞,不会造成无效的资源占用
- 耦合度极低,每个服务都可以灵活插拔,可替换
- 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件异步缺点:
- 架构复杂了,业务没有明显的流程线,不好管理
- 需要依赖于Broker的可靠、安全、性能
2、docker安装mq(运行mq容器)
先拉取rabbitmq镜像,然后运行rabbitmq容器
docker一般运行rbmq容器的例子
docker run \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
3、基本消息队列
依赖文件
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
基本消息队列发送流程
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.91.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itxsh");
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();
}
}
基本消息队列消息接收流程
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.91.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itxsh");
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("等待接收消息。。。。");
}
}
4、使用springAMQP完成基本消息队列
第一步:导入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
第二步:发送消息
yaml配置
spring:
rabbitmq:
host: 192.168.91.128 #rabbitmq主机ip
port: 5672 #端口
virtual-host: / #虚拟主机
username: itxsh
password: 123456
编写发送消息代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
public RabbitTemplate rabbitTemplate;
//使用springAmqp发送消息,
@Test
public void testSimpleQueue(){
String queueName = "simple.queue";
String message = "许石豪真帅";
rabbitTemplate.convertAndSend(queueName,message);
}
}
第三步:编写接收处理消息
首先还是需要引入依赖并配置rabbitmq的访问
yaml配置
spring:
rabbitmq:
host: 192.168.91.128 #rabbitmq主机ip
port: 5672 #端口
virtual-host: / #虚拟主机
username: itxsh 账号密码
password: 123456
接收处理消息代码(设置一个监听器,当队列当中有消息时候直接消费处理)
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String message){
System.out.println("spring 消费者接收到消息:【" + message + "】");
}
}
注意:rabbitmq消息一旦消费,就拿不回来,没有回溯功能
5、使用workqueue模型(再AMQP完成基本消息队列基础上)
消费者端yaml配置(最重要的是配置消息预取限制)不设置一般queue会讲消息平均分配给消费者,但是有些消费者的处理消息能力比较差,所以我们可以设置一次获取一条消息
spring:
rabbitmq:
host: 192.168.91.128
port: 5672
virtual-host: /
username: itxsh
password: 123456
listener:
simple:
prefetch: 1 #这个是设定消费预取限制,让消费者每次只能获取一条信息,消费完才能获取下一条
6、使用交换机发布订阅模型
交换机作用:一方面是接收消息,一方面是将消息发送给队列
(1)、FanoutExchange (广播)
这种交换机 会把消息发送给每个与它绑定的队列
第一步:声明交换机,声明队列,将队列与交换机绑定在一起
@Configuration
public class FanoutConfig {
//itcast.fanout交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
//fanout.queue1队列1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
//完成了交换机与队列的创建,我们要将交换机与队列绑定在一起
//返回的类型是binding类型,调用BindingBuilder将队列和交换机绑定在一起
@Bean
public Binding fanoutBinding1(FanoutExchange fanoutExchange,Queue fanoutQueue1){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
//fanout.queue2队列2
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
@Bean
public Binding fanoutBinding2(FanoutExchange fanoutExchange,Queue fanoutQueue2){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
第二步:监听消息队列消费消息
@Component
public class SpringRabbitListener {
//队列1 fanout。queue1
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutExchangeQueue1(String message) throws InterruptedException {
System.out.println("消息队列queue1:"+message);
}
//队列2 fanout。queue2
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutExchangeQueue2(String message) throws InterruptedException {
System.out.println("消息队列queue2:"+message);
}
}
第三步:发送消息到交换机测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
public RabbitTemplate rabbitTemplate; //这个导入amqp的core的依赖spring就会帮我们去管理rabbit消息队列
//fanoutExchange
@Test
public void testSendFanoutExchange() throws InterruptedException {
//交换机
String exchangeName = "itcast.fanout";
//消息内容
String message = "保佑许石豪面试通过";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,null,message);
}
}
(2)、DirectExchange(路由)
第一步,再 注册监听器的时候添加交换机消息队列的绑定
@Component
public class SpringRabbitListener {
/*
* DirectExchange
* */
//使用注解完成交换机,消息队列,以及bindingkey的绑定
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue1"),
exchange = @Exchange(name = "itcast.direct",type = "direct"),
key = {"blue","red"}
))
public void listenDirectQueue1(String message){
System.out.println("消息队列DirectQueue1:"+message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue2"),
exchange = @Exchange(name = "itcast.direct",type = "direct"),
key = {"yellow","red"}
))
public void listenDirectQueue2(String message){
System.out.println("消息队列DirectQueue2:"+message);
}
}
第二步:发送消息,发送消息的时候要指定bindingkey
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
public RabbitTemplate rabbitTemplate;
//DirectExchange
@Test
public void testDirectExchange() throws InterruptedException {
//交换机
String exchangeName = "itcast.direct";
//消息内容
String message = "保佑许石豪面试通过";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"yellow",message);
}
}
(3)、topicExchange
第一步: 再 注册监听器的时候添加交换机消息队列的绑定
@Component
public class SpringRabbitListener {
/*topicExchange*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue1"),
exchange = @Exchange(name = "itcast.topic",type = "topic"),
key = "china.#"
))
public void listenTopicExchange1(String message){
System.out.println("消息队列:topic.queue1"+message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue2"),
exchange = @Exchange(name = "itcast.topic",type = "topic"),
key = "#.news"
))
public void listenTopicExchange2(String message){
System.out.println("消息队列:topic.queue2"+message);
}
}
第二步:发送消息,发送消息的时候指定bindingkey
//TopicExchange
@Test
public void testTopicExchange() throws InterruptedException {
//交换机
String exchangeName = "itcast.topic";
//消息内容
String message = "保佑许石豪面试通过";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"china.xushihao",message);
}
(4)消息转换器
因为我们发送消息到队列是object类型,如果不进行转化的话就会是特别长的字符,所以我们一般是将数据转换为json数据
在publisher和consumer两个服务中都引入依赖:
```xml
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
```
//配置消息转换器。
//在启动类中添加一个Bean即可:
```java
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
七、Elasticsearch(分布式搜索)
kibana是操作es的图形界面
1、什么是elasticsearch
分布式搜索是倒排索引搜索
创建elasticsearch容器(这里挂载的数据卷再/var/lib/docker/volumes当中)
创建网络::docker network create es-net
```sh
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 \加入一个名为es-net的网络中
-p 9200:9200 \ 端口
-p 9300:9300 \
elasticsearch:7.12.1
创建kibana容器
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
2、分词器
分词器是我们直接将ik分析器的压缩包解压缩放在es挂载的配置文件包下es-plugins
(1)分词器两种模式
ik分词器是专门给elasticsearch使用的分词器,analyzer包含下面两种模式
e
例子:
(2) 如何添加扩展词语与停用词语
第一步:找到es容器的插件数据卷(/var/lib/docker/volumes/es-plugins/_data)中打开ik分词的config目录下的IKAnalyzer.cfg.xml文件
第二步:在里面指定扩展文件和停用文件文件名
第三步:再IKAnalyzer.cfg.xml文件所在目录创建扩展文件和停用文件,在里面添加词语就可以
3、索引库操作(使用kibana操作)
a、mapping属性
b、 创建索引库
例子:PUT /索引库名字(其中的格式就如同下面的,格式照抄)
我们只写了三个字段,一个info,一个email,一个name,
其中需要使用分词器的就是info,所以属性定义为text,并指定我们的ik分词器,
email的话不需要创建索引进行搜索,所以我们将index默认值更改为flase
name的话下面有两个子字段,所以使用properties再次进行指定
c、索引库的查看,删除,修改
查看::GET /索引库名
删除::DELETE /索引库名
修改::一般来说我们不会去修改索引库,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
4、文档操作
a、#插入文档
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
}
b、查询文档
GET /索引库名/_doc/文档id
c、修改文档,有两种方式,一种是全量修改,一种是增量修改
全量修改::
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
}
增量修改 ::
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
d、删除文档
DELETE /{索引库名}/_doc/id值
八、hotel案例完成es分布式搜索
这个案例先导入hotel的包
1、索引库分析(准备mapper映射)
字段拷贝是为了实现多个字段一起搜索,需要注意的是,经纬度,直接将两个字段合在一块,定义类型。
"location": {
"type": "geo_point"
},
2、使用RestClient来操作索引库
a、初始化restclient(ResthighlevelClient)
第一步:导入依赖
第二步:初始化,我们是再测试方法当进行索引库的操作
@SpringBootTest
public class HotelRestClientTest {
//索引库操作对象
private RestHighLevelClient client;
// 再这个测试类执行方法之前初始化对象
@BeforeEach
public void setClient(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.91.128:9200")
));
}
// 再这个测试类执行方法之后关闭对象
@AfterEach
public void tearDown() throws IOException {
this.client.close();
}
}
b、使用RestClient创建索引库
那个静态常量字符串就是我们mapping映射的dsl语句
c、删除和判断索引库是否存在
//删除
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
判断
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
3、使用Restclient操作文档
a、RestHighLevelClient初始化
初始化操作和操作索引库和上面的是一样
b、新增文档
需要注意的是,我们再给request请求对象传递参数的时候,是转化为Json格式的字符串去传递。
@Test
void testAddDocument() throws IOException {
//从数据库当中查询数据转成json数据给client
Hotel hotel = hotelService.getById(36934L);
//为什么要转成hotelDoc勒,因为创建索引库的时候,我们是将经纬度放在一个字段里,
HotelDoc hotelDoc = new HotelDoc(hotel);
String jsonString = JSON.toJSONString(hotelDoc);
//创建request对象,需要给索引库名字,id
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
//准备json文档,传入数据
request.source(jsonString, XContentType.JSON);
//发送请求
client.index(request, RequestOptions.DEFAULT);
}
c、查询 和删除
查询我们需要从响应体当中拿到source数据。转化为Json格式的字符串之后
//查询
@Test
void testGetDocumentById() throws IOException {
//创建请求对象
GetRequest request = new GetRequest("hotel").id("36934");
//发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//解析数据
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);
System.out.println(hotelDoc);
}
//删除
@Test
void deleteById() throws IOException {
//创建请求对象
DeleteRequest request = new DeleteRequest("hotel", "36934");
//发送请求
client.delete(request,RequestOptions.DEFAULT);
}
d、修改文档
//修改
@Test
void testUpdateDocument() throws IOException {
//创建请求对象
UpdateRequest request = new UpdateRequest("hotel", "36934");
//准备数据
request.doc(
"price","999"
);
//发送请求
client.update(request,RequestOptions.DEFAULT);
}
e、批量插入操作
批量插入就是bulk请求,然后将多个index请求添加进去,最后一起提交。
//批量插入
@Test
void list() throws IOException {
//批量查询查询数据
List<Hotel> hotels = hotelService.list();
//创建bulk请求对象
BulkRequest request = new BulkRequest();
//2.准备参数,添加多个新增的Request
for (Hotel hotel:hotels){
// 2.1.转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.创建新增文档的Request对象,索引库名,id,文档内容json格式
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON));
}
//3.发送请求
client.bulk(request,RequestOptions.DEFAULT);
}
4、 DSL查询语法
所谓dsl就是再kibana当中操作分布式搜索es的操作语句
a、查询所有和全文检索
// 查询所有
GET /索引库名字/_search
{
"query": {
"match_all": {
}}}全文检索单个字段
GET /索引库名/_search
{
"query": {
"match": {
"FIELD": "TEXT"(fleld字段名,text字段值)
} }}全文检索多个字段
GET /indexName/_search
{
"query": {
"multi_match": {
"query": "TEXT",(text字段名)
"fields": ["FIELD1", " FIELD12"] (fleld字段名)
}}}全文检索字段越多i性能越差,建议将多个字段copy to到一个复合字段使用单字段查询
b、精确查询(精确词,范围)
精确查询之精确词
GET /索引库名/_search
{
"query": {
"term": {
"FIELD": { //字段名
"value": "字段值"
}}}}精确查询之范围
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}}}}
c、地理坐标查询
// geo_distance 查询,查询是一个中心点,然后加半径
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 指定半径
"FIELD": "31.21,121.5" // 指定地理坐标的字段,指定圆心
} }}
d、复合查询之修改相关性算分
在es5之前用的tf-idf算法,es5之后用bm25算法比较多
如果要修改相关性算分
e、复合查询 之bool查询
这个查询是地址必须再上海,品牌是皇冠假日或者华美达,价格大于500,评分大于等于45
f、搜索结果处理
排序
分页
from+size缺点,不能查询超过10000条数据,超过就会报错
高亮
5、使用Restclient查询文档
注意:我们的dsl里面的功能在请求对象.source里面(sort排序,query查询,highlighter高光)
a、restclient查询所有和全文检索
第一步:引入依赖
第二步:初始化RestHighLevelClient
第三步:编写业务代码
//1.查询所有
@Test
void testMatchAll() throws IOException {
//准备请求对象,并传递索引库名
SearchRequest request = new SearchRequest("hotel");
//组织dsl语句,我们的dsl语句在querybuilders里面都有
request.source()
.query(QueryBuilders.matchAllQuery()); //这个是matchall
.query(QueryBuilders.multiMatchQuery("如家","name","brand")); //multi_match
.query(QueryBuilders.matchQuery("name","如家")); // 这个是match
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//。。。解析响应对象
SearchHits searchHits = response.getHits();
//获取总条数
TotalHits totalHits = searchHits.getTotalHits();
System.out.println(totalHits);
//获取查询文档数组
SearchHit[] hits = searchHits.getHits();
//遍历
for(SearchHit hit:hits){
//获取文档source
String json = hit.getSourceAsString();
//反序列化输出
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
}
下面这张图片所示的是我们查询结果对应的关系
b、restclient精确查询(精确范围)
//精确查询
@Test
public void termTest() throws IOException {
//准备请求对象
SearchRequest request = new SearchRequest("hotel");
//组织dsl语句
request.source()
// .query(QueryBuilders.rangeQuery("price").gte(100).lte(150)); 范围查询
.query(QueryBuilders.termQuery("city","上海")); //词条查询
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析响应数据
SearchHits searchHits = response.getHits();
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("查询到的总条数为:"+totalHits);
SearchHit[] hits = searchHits.getHits();
for(SearchHit hit:hits){
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
}
c、复合查询之bool查询
需要注意的是bool类型的查询,我们是在外面构建dsl语句,然后写入请求对象里面
//bool查询
@Test
void booSelect() throws IOException {
//创建请求对象
SearchRequest request = new SearchRequest("hotel");
//准备bool查询dsl查询条件
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("city","上海"));
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(150).lte(250));
//将查询条件传入请求对象
request.source().query(boolQuery);
//发送请求
SearchResponse response = client.search( request, RequestOptions.DEFAULT);
//解析响应数据
SearchHits searchHits = response.getHits();
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("查询到的总条数为:"+totalHits);
//获取请求hits文档数组
SearchHit[] hits = searchHits.getHits();
for(SearchHit hit:hits){
//获取文档数据
String json = hit.getSourceAsString();
//反序列化输出数据
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
}
d、搜索结果处理(排序,分页)
注意: 排序,分页都是和查询平级的,所以在source后面直接 . 就可以了,距离排序的话与普通排序相比复杂一点点
//排序分页查询,就是在前面的基础上加上排序分页
@Test
void sortSelect() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchQuery("name","外滩如家")) //查询
.sort("price",SortOrder.ASC) //排序
.from(1).size(5); //分页
//发送请求获取响应对象
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//获取相应数据
SearchHits searchHits = response.getHits();
//获取查询到的语句数据
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("总共查询到多少条数据:"+totalHits);
//获取文档数据数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit:hits){
//获取文档数据
String json = hit.getSourceAsString();
//反序列化输出数据
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
}
e、高亮
注意:我们反序列化取出的是source里面的值,而高亮是和它平级不在里面,所以需要另外处理高亮数据
//高亮
@Test
void highlighter() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchQuery("name","外滩如家"))
.highlighter(new HighlightBuilder().field("name").requireFieldMatch(false)); //创建高亮对象,指定高亮字段是否需要查询字段匹配
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//响应结果
SearchHits searchHits = response.getHits();
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("总共查询到的数据:");
SearchHit[] hits = searchHits.getHits();
for(SearchHit hit:hits){
//获取source
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//处理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//根据字段名获取高亮结果
HighlightField highlightField = highlightFields.get("name");
//取除高亮结果数组里面的值,这里我们就指定了一个,所以就是name
String name = highlightField.getFragments()[0].string();
//将取出的高亮结果赋值给我们要返回的pojo对象
hotelDoc.setName(name);
System.out.println(hotelDoc);
}
}
6、酒店查询案例(hotel-demo)
restclient依赖
初始restclient,直接在启动类注入bean
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.91.128:9200")
));
}
核心业务层
jii
//分页
@Override
public PageResult search(RequestParams params){
try {
//准备request
SearchRequest request = new SearchRequest("hotel");
//准备dsl,关键字搜索,分页
//构建booleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//关键字搜索
String key = params.getKey();
if (key == null || "".equals(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// 3.城市条件
if (params.getCity() != null && !params.getCity().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// 4.品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// 5.星级条件
if (params.getStarName() != null && !params.getStarName().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// 6.价格
if (params.getMinPrice() != null && params.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders
.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}
//分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page-1)*size).size(size);
//排序功能(我附近的酒店)
String location = params.getLocation();
if (location != null && !location.equals("")) {
request.source().sort(SortBuilders
.geoDistanceSort("location",new GeoPoint(location))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
// 2.算分控制(广告置顶)
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查询,相关性算分的查询
boolQuery,
// function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一个function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD", true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
//将bool查询放入查询当中
request.source().query(functionScoreQuery);
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析响应
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//响应结果抽取代码
private PageResult handleResponse(SearchResponse response){
List<HotelDoc> hotelDocList = new ArrayList<>();
//解析响应
SearchHits searchHits = response.getHits();
//获取查询总条数
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit:hits){
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//这个是获取的我附近的酒店距离我的距离
Object[] sortValues = hit.getSortValues();
if (sortValues.length>0) {
hotelDoc.setDistance(sortValues);
}
hotelDocList.add(hotelDoc);
}
return new PageResult(total,hotelDocList);
}
九、 分布式搜索引擎
1、数据的聚合
聚合常见的有三类:
桶(Bucket)聚合:用来对文档做分组
TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
Avg:求平均值
Max:求最大值
Min:求最小值
Stats:同时求max、min、avg、sum等
管道(pipeline)聚合:其它聚合的结果为基础做聚合
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
a、桶的聚合
解析聚合查询数据
b、度量(Metric) 聚合
这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。
2、拼音分词器
将py的依赖解压在es挂载目录下就可以使用,放在ik分词器的同级
3、实现数据同步
【酒店管理系统hotel-admin】
实现的话就是数据库哪里进行新增,修改删除操作之后,将更改了的数据id发送到消息队列当中,hotel-demo当中进行消息的监听,当接收到消息之后,调用索引库操作的Restclient进行对文档进行操作,实现数据的同步。
十、服务保护
1、初识sentinel
解决雪崩常见的4种方式
超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
仓壁模式:船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
断路器模式:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
断路器会统计访问某个服务的请求数量,当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断:
流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
2、安装sentinel
将jar包导入到一个非中文目录下,
如果需要修改配置,我们可以在后面加上-D 来添加配置信息
3、微服务整合sentinel
a、引入依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
b、添加sentinel配置信息
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
4、限流规则(三种)
直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
(比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。)
链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
链路需要进行以下操作:
5、流控效果
十一、服务的隔离与降级
1、cloud种fegin整合sentinel
给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
spring:
cloud: #配置nacos的服务器地址
sentinel:
transport:
dashboard: localhost:8090
web-context-unify: false #为true是只监控controller当中的请求,为true的话就是监控fegin:
sentinel:
enabled:true #开启feign对sentinel的支持
第二步:,实现FallbackFactory:
//编写失败降级逻辑
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient(){
@Override
public User findById(Long id) {
log.error("查询用户异常",throwable);
return new User();
}
};
}
}
在到配置类当中将 UserClientFallbackFactory 注册成Bean
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
第三步:将UserClientFallbackFactory 失败方法添加到client的fallback方法当中(UserClient接口中使用UserClientFallbackFactory)
//feign的最佳实践
@FeignClient(value = "userservice",fallback = UserClientFallbackFactory.class)//服务名称
public interface UserClient {
@GetMapping("/user/{id}") //请求路径参数等
User findById(@PathVariable("id")Long id);
}
2、线程隔离
3、服务熔断降级
a、慢调用
b、异常比例和异常数
4、授权规则
授权规则可以对调用方的来源做控制,例如,通过网关过来的请求还是通过路径直接访问进来,
我们可以在网关yml文件当中可以添加i请求让其带着参数origin,在sentinel中的授权中判断origin的值,不一样就拦截。
实现步骤:
一、实现sentinel获取origin的方式
二、给网关请求添加请求头
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway 添加请求头
routes: 这个是服务路由的
# ...略
三、配置授权规则
这样 ,我们添加的授权规则就会去判断请求头当中的值与我们规定的值是否一样,如果不一样的话就拦截,但是这样还有一个问题 我们拦截之后所有的拦截都是flow limmiting(限流) ,不利于我们进行判断,所以我们可以自定义一个异常类来区分是限流还是熔断还是降级。
异常类型说明
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
代码实现
package cn.itcast.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
这样当我们出现异常时,返回的就是我们自定义的信息以及状态码。