当前位置:   article > 正文

微服务--实用篇-笔记大全_给黑马旅游添加排序功能

给黑马旅游添加排序功能

在这里插入图片描述

服务项目

黑马- SpringCloud微服务技术栈实用篇完成时间2023-3-22

项目涉及技术

  1. 知识点是按照集数依次整理,方便日后回来查找。
  2. 考虑到不是固定的联网方式,时而WiFi,时而热点,配置静态IP会导致每次网络变更后都需要重新配置,所以虚拟机使用的动态路由,当需要运行相关程序时,IP变化,需要修改yml文件 || test测试类 || 启动类里的配置即可。
  3. 将代码路径列举主要是为后续审查。
  4. 自己编写代码路径E:\微服务\实用篇\day01-SpringCloud01\资料\cloud-demo
  5. 打包上传到Linux实现集群部署路径E:\微服务\实用篇\day03-Docker\资料\cloud-demo
  6. mq的代码路径E:\微服务\实用篇\day04-MQ\资料\mq-demo
  7. RestClient操作酒店索引库的代码路径E:\微服务\实用篇\day05-Elasticsearch01\资料\hotel-demo
  8. 操作数据库+mq实现数据同步的代码路径E:\微服务\实用篇\day07-Elasticsearch03\资料\hotel-admin

实用篇

背景
  1. 微服务技术栈导学。
  2. 微服务的架构,背景,技术对比等。
  3. 服务拆分–微服务调用方式。
注册中心
  1. Eureka注册中心–服务原理、搭建、注册、发现->(p9)。
  2. Ribbon负载均衡规则–原理、轮询与随机均衡策略、饥饿加载->(p14)。
  3. Nacos注册中心–安装、注册、发现、服务分级存储(分地域集群)、NacosRule负载均衡策略、权重控制、环境隔离-namespace->(P17)。
  4. Nacos与eureka的共同点、区别。
  5. Nacos启动–在bin目录进入cmd,输入startup.cmd -m standalone
配置中心
  1. Nacos配置管理(ps: 视频p26 – 命名空间是否正确))。
  2. Nacos实现热更新–(1)@Value + RefreshScope来刷新。 (2)@ConfigurationProperties注入,自动刷新。
  3. 多服务共享配置–优先级:nacos中的配置(服务名-profile.yaml > 服务名称.yaml) > 本地配置。
  4. nacos集群搭建–(1)搭建MySQL集群并初始化数据库表。(2)下载解压nacos、修改集群配置、数据库配置。(3)分别启动多个nacos节点。(4)nginx反向代理。
远程调用
  1. http客户端Feign–定义和使用步骤->(p30)。
  2. Feign与RestTemplate的区别与代替。
  3. 自定义Feign的配置–修改日志级别-配置文件&&java代码配置(一般需要配置,详解)、响应结果的解析器、请求参数编码、支持的注解格式、失败重试机制。
  4. Feign的性能优化–默认-URLConnection:默认实现,不支持连接池;Apache HttpClient:支持连接池;OKHttp:支持连接池。
  5. 优化–日志尽量用basic;使用连接池。
  6. Feign最佳实践–controller和FeignClient继承同一接口;将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供消费者使用(抽取)。
  7. 抽取Feign–这里自动注解UserClient报错,是由于扫不到包,下面视频有解决办法-两种导包的方式。
服务网关
  1. 统一网关Gateway–作用:身份认证、权限校验、负载均衡、限流。(p35)
  2. 网关搭建、路由配置–路由id、路由目标(uri)、路由断言(predicates)、路由过滤器(filters)。
  3. 路由断言工厂–11种基本的断言(使用时去Spring Cloud Gateway官方文档中介绍及使用样例)、作用:读取用户定义的断言条件,对请求做出判断。
  4. 路由过滤器–31种过滤规则;过滤器的作用:对路由的请求或响应做加工处理、配置在路由下的过滤器只对当前路由请求生效;defaultFilters默认路由的作用:对所有路由都生效的过滤器。
  5. 全局过滤器–作用:对所有路由都生效的过滤器,并且可以自定义处理逻辑;实现步骤:实现GlobalFilter接口,添加@Order注解或实现Ordered接口,编写处理逻辑。
  6. 过滤器执行顺序–order值越小,优先级越高;当order值一样时,顺序是defaultFilter最先,然后局部的路由过滤器,最后全局过滤器。
  7. CORS跨域–配置参数:允许哪些域名、请求头、请求方式、是否允许使用永久cookie、有效期多久。
项目部署
  1. Docker解决不同组件依赖的兼容性问题–将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包,形成可移植镜像;将每一个应用放到隔离的容器上运行,使用沙箱机制,相互隔离。(P42)
  2. Docker解决开发、测试、生产环境的差异问题–Docker镜像中包含完整运行环境,包括系统函数库,仅依赖Linux系统的内核,因此可以在任意Linux操作系统上运行。
  3. Docker是一个快速交付应用、运行应用的技术–启动、移除都可以通过一行命令完成,方便快捷。
  4. Docker和虚拟机的差异–docker是一个系统进程;虚拟机是在操作系统中的操作系统;docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般。
  5. 镜像:将应用程序及其依赖、环境、配置打包在一起。
  6. 容器:镜像运行起来就是容器,一个镜像可以运行多个容器。
  7. Docker结构–服务端:接受命令或远程请求,操作镜像或容器;客户端:发送命令或请求到Docker服务端。
  8. DockerHub:一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry。
  9. linux卸载、安装、启动docker,配置镜像,Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,所以启动前需要临时关闭防火墙。
# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
#查询防火墙状态
systemctl status firewalld

# 启动docker服务
systemctl start docker  
# 查询docker服务状态
systemctl status docker
# 查看版本
docker -v
# 停止docker服务
systemctl stop docker 
# 重启docker服务
systemctl restart docker  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. Docker基本操作–镜像命令(p47)
  2. docker--help查看帮助文档;如:docker images --help-查看所有的镜像,里面有解释和参数的使用说明。
  3. 依次装nginx、redis。
  4. 下方是一些镜像命令,不需要记住,用时查帮助文档即可。
#查看帮助文档
docker--help
#拉取nginx的命令
docker pull nginx
#查看镜像
docker images

#查询帮助文档
docker save --help
#导出镜像到磁盘  -o 导出后的名称 名称:版本
docker save -o nginx.tar nginx:latest
#删除镜像  rmi 名称:版本  或者  rmi 镜像id
docker rmi nginx:latest
#导入镜像
docker load -i nginx.tar
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  1. docker基本操作–容器命令(p49)
# 运行docker
docker run
#docker run:运行容器  --name:起名字 -p:宿主机端口(可变):容器端口(不可变)  -d:后台运行容器  nginx:是镜像名称
docker run --name name -p 80:80 -d nginx
#redis
docker run --name mr -p 6379:6379 -d redis redis-server --appendonly yes

# 暂停
docker pause 容器名字
# 从暂停到运行
docker unpause 容器名字

# 停止
docker stop 容器名字
# 从停止到运行
docker start 容器名字

# 查看所有运行的容器及状态
docker ps

# 查看容器运行日志  
docker logs 容器名字
# 持续查看输出日志
docker logs -f 容器名字

# 进入容器执行命令
docker exec
# docker exec:进入容器内部执行命令  -it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互  name:容器名称  bash:进入容器后执行的命令,bash是一个Linux终端的交互命令
docker exec -it name bash
# 删除指定容器
docker rm 容器名字
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  1. docker基本操作–数据卷(p53)
  2. 数据卷(volume)是一个虚拟目录,指向宿主机系统中的某一个目录。
  3. 数据卷作用:将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全。
  4. 数据卷基本的命令。
# 数据卷基本语法
docker volume [command]
# 下方是根据命令的command
create # 创建一个volumn  + 名称
inspect # 显示一个或多个volumn的信息  + 名称
ls # 列出所有的volume
prune # 删除未使用的volume
rm # 删除一个或多个指定的volumn + 名称
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. docker基本操作–挂载数据卷(P54)
  2. 如果容器运行时volume不存在,会自动被创建出来。
  3. 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找。
  4. 目录挂载耦合度高,需要我们自己管理,目录容易找。
# docker run的命令中通过 -v 参数挂载文件或目录到容器中:
# (1)-v volume名称:容器内目录
# (2)-v 宿主机文件:容器内文件
# (3)-v 宿主机目录:容器内目录

# docker run:运行容器  --name:起名字  -v volumename:/targetContainerPath -p 8080:80:把宿主机的8080端口映射到容器内的80端口  -d:挂载到后台  nginx:镜像名称
docker run --name mn -v html:/root/html -p 8080:80 -d nginx
# mysql
docker run --name mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf -v /tmp/mysql/data:/var/lib/mysql -d mysql:5.7.25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. Dockerfile–自定义镜像(P56)
  2. 镜像是分层结构,每一层称为一个Layer。BaseImage层:包含基本的系统函数库、环境变量、文件系统;Entrypoint:入口,镜像中应用启动的命令;其它:在BaseImage基础上添加依赖、安装程序、完成整个应用的安装和配置。
  3. Dockerfile是一个文本文件,其中包含一个个指令,用指令来说明要执行什么操作来构建镜像。
  4. Dockerfile的第一行必须是FROM,从一个基础镜像来构建(可以是基本的操作系统,也可以是其它人制作好的镜像)。
  5. 一些常用指令介绍如下:
# 每一个指令都会形成一层Layer
FROM   # 指定基础镜像
ENV    # 设置环境变量,可在后面指令使用
COPY   # 拷贝本地文件到镜像的指定目录
RUN    # 执行Linux的shell命令,一般是安装过程的命令
EXPOSE # 指定容器运行时监听的端口
ENTRYPOINT  # 镜像中应用的启动命令,容器运行时调用
# 利用dockerfile来构建镜像 指令后的.是指DockerFile在当前目录下
docker build -t javaweb:1.0 .
# 将生成的镜像跑起来
docker run --name web -p 8090:8090 -d javaweb:1.0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  1. DockerCompose–微服务集群部署(P58)。
  2. DockerCompose基于Compose文件快速部署分布式应用,无需手动一个个创建和运行容器。
  3. Compose文件是一个文本文件,通过指令定义集群中的每一个容器如何运行(等价于转换docker的各种参数来定义,还有运行容器和构建镜像)。
  4. CentOS7安装DockerCompose。
  5. 使用DockerCompose将前面的项目集群部署到Linux上。(ps:使用xshell不可以直接传输文件夹,可以先压缩上传到Linux,然后解压)
  6. 由于nacos部署比较慢,其它微服务需要依赖到它,所以会导致部分运行时出现错误。解决:先对nacos进行部署,再部署其它微服务。
# 查看DockerCompose的帮助文档
docker-compose --help
# 查看创建的容器
docker ps
# 查看日志  最后可以加微服务名称,查询一个启动的日志
docker-compose logs -f
# 解决nacos部署慢,重启其它微服务
docker-compose restart gateway userservice orderservice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. Docker–镜像仓库(P60)
  2. 搭建镜像仓库–使用DockerCompose部署带有图象界面的DockerRegistry的镜像仓库,需要先配置Docker信任地址。
  3. 在私有镜像仓库推送或拉取镜像,推送镜像到私有镜像服务必须先tag。
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.226.134:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker

# 创建DockerCompose部署带有图象界面的DockerRegistry的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
# 后台运行
docker-compose up -d

# 查看现有镜像
docker images
# 重新tag本地镜像,名称前缀为私有仓库地址
docker tag nginx:latest 192.168.226.134:8080/nginx:1.0
# 推送镜像
docker push 192.168.226.134:8080/nginx:1.0
# 删除镜像
docker rmi 192.168.226.134:8080/nginx:1.0
# 拉取镜像
docker pull 192.168.226.134:8080/nginx:1.0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
异步通信
  1. MQ–RabbitMQ–SpringAMQP(P61)
  2. 同步调用–优点:时效性较强,可以立即得到结果;缺点:耦合度高、性能和吞吐能力下降、有额外的资源消耗、有级联失败问题。
  3. 异步调用的实现-事件驱动优势,事件驱动架构-Broker。
  4. 异步通信–优点:耦合度低、吞吐量提升、故障隔离、流量削峰;缺点:依赖于Broker的可靠性、安全性、吞吐能力、架构复杂了,业务没有明显的流程线,不好追踪管理。
  5. MQ(MessageQueue):消息队列,存放消息的队列。
  6. RabbitMQ、ActiveMQ、RocketMQ、Kafka的对比分析。(P64)
  7. RabbitMQ–部署安装、页面介绍、结构和概念。
# 在线拉取
docker pull rabbitmq:3-management
# 上传好tar包,命令加载镜像
docker load -i mq.tar
# 运行MQ容器
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
 # 查看全部容器
 docker ps -a
 # 重启后重启mq容器
docker start mq
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  1. RabbitMQ部署成功后页面元素分析–channel:操作MQ的工具、exchange:路由消息到队列、queue:缓存消息、virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组。
  2. RabbitMQ入门案例–简单队列模型。官方文档-入门案例-publisher:消息发布者,将消息发送到队列;queue:消息队列,负责接收并缓存消息;consumer:订阅队列,处理队列中的消息。
  3. 基本消息队列的消息发送流程和基本消息队列的消息接收流程。(P67)
  4. SpringAMQP–基于AMQP协议定义的一套API规范,提供模板来发送和接受消息;AMQP介绍–应用间消息通信的一种协议,与语言和平台无关。
  5. 简单队列模型–利用SpringAMQP实现HelloWorld的基础消息队列功能–引入amqp的starter依赖;配置RabbitMQ地址;利用RabbitTemplate的convertAndSend方法来发送消息。(P67)
  6. Work queue–工作队列,可以提高消息处理速度,避免队列消息堆积-默认为:消息预取。
  7. Work模型–多个消费者绑定到一个队列,同一条消息只会被一个消费者处理;通过设置prefetch来控制消费者预取的消息数量。(P71)
  8. 发布、订阅模型–允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机);exchange类型-Fanout:广播、Direct:路由、Topic:话题。(P72)
  9. exchange(交换机)作用–接收publisher发送的消息;将消息按照规则路由到与之绑定的队列;负责消息路由,而不是存储,路由失败则消息丢失。
  10. Fanout Exchange–将接收到的消息路由到每一个跟其绑定的queue。
  11. Direct Exchange–路由模式会将接收到的消息根据规则路由到指定的Queue(每一个Queue都与Exchange设置一个BindingKey);发布者发送消息时,指定消息的RoutingKey;Exchange将消息路由到BindingKey与消息RoutingKey一致的队列。(P74)
  12. TopicExchange–与Direct Exchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割;Queue与Exchange指定BindingKey时可以使用通配符:#-代指0个或多个单词;*:代指一个单词。
  13. 消息转换器–SpringAMQP中消息的反序列化和反序列化-利用MessageConverter实现的,默认是JDK的序列化;注意发送方与接收方必须使用相同的MessageConverter。
分布式搜索
  1. ES==elasticsearch–开源分布式搜索引擎。(P77)
  2. elasticsearch:用来实现搜索、日志统计、分析、系统监控等功能。
  3. elasticsearch+kibana、Logstash、Beats == elastic stack(ELK)。
  4. elasticsearch–核心-存储、计算、搜索数据;可替换组件–kibana - 数据可视化;可替换组件–Logstash、Beats - 数据抓取
  5. Lucene–Apache的搜索引擎类库-易扩展、高性能、基于倒排索引-提供搜索引擎核心API-仅支持Java语言。
  6. 数据库表–文档:每一条数据就是一个文档;词条:对文档中的内容分词,得到的词语就是词条。
  7. 正向索引–基于文档id创建索引;查询词条时必须先找到文档,然后判断是否包含词条 - 数据库的模糊查询-逐条查询判断。
  8. 倒排索引–对文档内容分词,对词条创建索引,并记录词条所在文档的id;查询是先根据词条查询文档id,而后获取文档。
  9. ES–存储-面向文档存储的,文档数据会被序列化为JSON格式;索引-相同类型的文档的集合;映射-索引中文档的字段约束信息,类似表的结构约束。
  10. MySQL与elasticsearch的概念对比、架构分析、关系。(P80)
  11. 安装部署es、kibana,需要先让es和kibana容器互联,部署单点es或kibana都是运行上传的tar包将数据导入,再运行docker命令,即可访问,具体的命令整理如下,外加注释,清晰明了。
# 创建网络
docker network create es-net
# 关闭虚拟机后,查看局域网络(已配置过,重启后不影响)
docker network ls
# 导入数据
docker load -i es.tar
# 运行docker命令,部署单点es
docker run -d \
	--name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1
# 重启后,重启容器
docker start es
# 输入地址加端口即可访问es
http://192.168.226.139:9200

# 导入数据
docker load -i kibana.tar
# 运行docker命令,部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1
# 重启后重启容器
docker start kibana
# 输入地址加端口即可访问kibana
http://192.168.226.139:5601
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

ps:(1)重启后只需要重启容器即可;(2)如果没有删除容器,重新运行docker命令部署会报名字重复错误;(3)如果不删除容器,想修改名称创建容器的话,需要在ess和kibana中的请求连接要同步,否则会报Kibana server is not ready yet的问题。

  1. 分词器的作用–创建倒排索引时对文档分词、用户搜索时,对输入的内容分词。(P83)
  2. 测试分词器、安装IK分词器。
# 在kibana中测试分词器
# english-默认分词器、standard-标准分词器
POST /_analyze
{
	"text": "好好学习,天天向上",
	"analyzer": "english"
}

# 安装ik分词器
# 查看数据卷elasticsearch的plugins目录位置
docker volume inspect es-plugins
# 到这个目录下
cd /var/lib/docker/volumes/es-plugins/_data
# 上传elasticsearch-analysis-ik-7.12.1.zip,然后解压
unzip elasticsearch-analysis-ik-7.12.1.zip
# 不太建议上面的方式,我试过发现启动会报错,后面改了很久都是报错,不知道哪里的配置文件被修改了,然后恢复快照重新来过
# 使用FileZillar直接传输Windows下解压的文件夹,结果是成功的
# 重启es容器
docker restart es
# 查看es日志
docker logs -f es

# 测试ik分词器
# IK分词器包含两种模式
# ik_smart:最少切分   --  被搜索的概论低-粗粒度
# ik_max_word:最细切分 -- 内存占用高-细粒度
GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "好好学习天天向上,奥利给,噢噢点赞"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  1. ik分词器-扩展词库、停用词库。

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑

# 打开IK分词器config目录,在IKAnalyzer.cfg.xml配置文件内容添加
# 用户可以在这里配置自己的扩展字典
<entry key="ext_dict">ext.dic</entry>
#用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典
<entry key="ext_stopwords">stopword.dic</entry>

# 新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
奥利给
# 在 stopword.dic 添加停用词
噢噢
# 修改过看效果,重启es容器即可
docker restart es
# 查看 日志
docker logs -f es
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  1. 索引库操作(P85)
  2. mapping属性–mapping是对索引库中文档的约束。
  3. mapping属性包括:type:字段数据类型、index:是后创建索引-默认为true;analyzer:使用哪种分词器;properties:该字段的子字段。
  4. type简单类型–字符串:text(可分词的文本)、keyword(精确值);数值:long、integer、short、byte、double、float;布尔:boolean;日期:date;对象:object。
  5. 创建索引库、查看索引库、删除索引库、禁止修改索引库。(P86)
# DSL语法
# 创建索引库名
PUT /索引库名
# 创建索引库的DSL语法例子
PUT /a
{
	"mappings": {
		"properties": {
			"info": {
				"type": "text",
				"analyzer": "ik_smart"
			},
			"name":{
				"type": "object",
				"properties": {
					"firstName": {
						"type": "keyword",
						"index": false
					}
				}
			}
		}
	}
}

# 查看索引库
GET /索引库名
# 删除索引库
DELETE /索引库名
# 索引库和mapping一旦创建就无法修改,但是可以添加新的字段
PUT /索引库名/_mapping
{
	"properties":{
		"新字段名":{
			"type":"integer"
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  1. 文档操作–插入文档、查看文档、删除文档(P88)
  2. 修改文档–全量修改,会删除旧文档,添加新文档;局部修改,修改指定字段值。
# 插入文档
POST /索引库名/_doc/文档id
# 查看文档
GET /索引库名/_doc/文档id
# 删除文档
DELETE /索引库名/_doc/文档id

# 插入的DSL语法例子 -- 索引库名与上方创建相同
POST /a/_doc/1
{
	"info": "好好学习天天向上",
	"name": {
		"firstName": "小",
		"lastName": "盈"
	}
}

# 修改文档 -- 全量修改,会删除旧文档,添加新文档
PUT /索引库名/_doc/文档id
PUT /a/_doc/1
{
	"info": "好好学习天天向上",
	"email": "45543563.qq.com",
	"name": {
		"firstName": "小",
		"lastName": "盈"
	}
}
# 局部修改,修改指定字段值 -- 只能修改一个指段
POST /索引库名/_update/文档id
POST /a/_update/1
{
	"doc":{
		"email": "xiaoying@qq.com"
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  1. RestClient操作索引库(P90)
  2. 导入hotel-demo,分析hotel的mapping数据结构-字段名、数据类型、是否参与搜索、是否分词、分词器。
  3. tip:ES中支持两种地理坐标数据类型–geo_point:由维度和经度确定的一个点;geo_shape:有多个geo_point组成的复杂几何图形。
  4. 字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。
  5. 创建索引库、删除索引库、判断索引库是否存在。
  6. 索引库操作基本步骤:初始化RestHighLevelClient;创建XxxIndexRequest。xxx是Create、Get、Delete;准备DSL(Crete时需要);发送请求,调用RestHighLevelClient#indices().xxx()方法。
# 酒店的mapping
PUT /hotel
{
	"mappings":{
		"properties":{
			"id":{
				"type": "keyword"
			},
			"name":{
				"type": "text",
				"analyzer": "ik_max_word",
				"copy_to": "all"
			},
			"address":{
				"type": "keyword",
				"index": false
			},
			"price":{
				"type": "integer"
			},
			"score":{
				"type": "integer"
			},
			"brand":{
				"type": "keyword",
				"copy_to": "all"
			},
			"city":{
				"type": "keyword"
			},
			"starName":{
				"type": "keyword"
			},
			"business":{
				"type": "keyword",
				"copy_to": "all"
			},
			"location":{
				"type": "geo_point"
			},
			"pic":{
				"type": "keyword",
				"index": false
			},
			"all":{
				"type": "text",
				"analyzer": "ik_max_word"
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  1. RestClient操作文档–利用JavaRestClient实现文档的CRUD。(P95)
  2. 在数据库查询酒店数据,导入到hotel索引库,实现酒店数据的CRUD。
  3. 新增文档-index、根据id查询文档-get、根据id修改文档-update、根据id删除文档-delete。
  4. 文档操作的基本步骤:初始化RestHighLevelClient;创建XxxRequest;准备参数(Index和Update时需要);发送请求,调用RestHighLevelClient#.xxx()方法;解析结果(Get时需要)。
  5. 利用JavaRestClient批量导入酒店数据到ES。
DSL语法
  1. DSL查询语法(P101)
  2. 查询所有:查询出所有数据,一般测试用。例如:match_all
  3. 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:matchmulti_match;两者区别match根据一个字段查询,multi_match根据多个字段查询;参与查询的字段越多,查询性能越差。
  4. 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:idsrangeterm
  5. 地理(geo)查询:根据经纬度查询。例如:geo_distancegeo_bounding_box
  6. 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:Boolean Queryfunction_score
  7. 相关性打分算法(P105)
  8. TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
  9. BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长曲线会趋于水平。
# 查询所有
GET /hotel/_search
{
	"query": {
		"match_all": {}
	}
}

# 全文检索 -- match查询(效率高)
GET /hotel/_search
{
	"query": {
		"match": {
			"all": "外滩如家"
		}
	}
}
# 全文检索 -- multi_match
GET /hotel/_search
{
	"query": {
		"multi_match": {
			"query": "外滩如家",
			"fields": ["brand", "name", "business"]
		}
	}
}

# 精确查询 -- term查询 精确匹配
GET /hotel/_search
{
	"query": {
		"term": {
			"city": {
				"value": "上海"
			}
		}
	}
}
# 精确查询 -- range查询  范围过滤
GET /hotel/_search
{
	"query": {
		"range": {
			"price": {
				"gte": 100,
				"lte": 300
			}
		}
	}
}

# 地理查询 -- distance查询
GET /hotel/_search
{
	"query": {
		"geo_distance": {
			"distance": "2km",
			"location": "31.21, 121.5"
		}
	}
}

# 复合查询 -- function_score   参加打分
# 给“如家”这个品牌的酒店靠前一点
GET /hotel/_search
{
	"query": {
		"function_score": {
			"query": {
				"match": {
					"all": "外滩"
				}
			},
			"functions": [    //算分函数
				{
					"filter": {    //条件
						"term": {
							"brand": "如家"
						}
					},
					"weight": 10   //算分权重
				}
			],
			"boost_mode": "sum"  //加权分式
		}
	}
}
# 复合查询 -- Boolean Query 
# must:必须匹配的条件,可以理解为“与”
# should:选择性匹配的条件,可以理解为“或”
# must_not:必须不匹配的条件,不参与打分 - 提高效率
# filter:必须匹配的条件,不参与打分 - 提高效率
# 搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店
GET /hotel/_search
{
	"query": {
		"bool": {
			"must": [
				{"match":{"name": "如家"}}
			],
			"must_not": [
				{"range":{"price":{"gt": 400}}}
			],
			"filter":[
				{"geo_distance": {
					"distance": "20km",
					"location": {
						"lat": 31.21,
						"lon": 121.5
					}
				}}
			]
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  1. 搜索结果处理(P108)
  2. 排序 – 经过排序就不做相关性打分,提高查询效率。
  3. 分页–文档全部查询,然后截取当前文档的位置+显示的文档数;默认top10,查询更多修改参数-from、size。
  4. 深度分页问题–ES集群处理的时候,是将所有节点的结果聚合,在内存中排序,在选中相应的文档;搜索页数过深,或结果集(from+size)越大,对内存和CPU的消耗也越高。
  5. es设定结果集上限为10000。
  6. 分页方式(P109)
  7. from + size–优点:支持随机翻页;缺点:深度分页问题。场景:百度、谷歌、京东等的随机翻页搜索。
  8. after search–优点:没有查询上限(单词查询的size不超过10000),缺点:只能向后逐页查询,不支持随机翻页。场景:没有随机翻页需求的搜索,例如手机向下翻页。
  9. scroll:优点:没有查询上限(单词查询的size不超过10000),缺点:会额外消耗内存,搜索结果是非实时的,场景:海量数据的获取和迁移。(已弃用)
  10. 高亮–将搜索结果中把搜索关键字突出显示。
# 对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
GET /hotel/_search
{
	"query": {
		"match_all": {}
	},
	"sort": [
		{
			"score": "desc"
		},
		{
			"price": "asc"
		}
	]
}
# 对酒店数据数据按照你的位置坐标的距离升序排序
GET /hotel/_search
{
	"query": {
		"match_all": {}
	},
	"sort": [
		{
			"_geo_distance": {
				"location": {
					"lat": 31.034661,
					"lon": 121.612282
				},
				"order": "asc",
				"unit": "km"
			}
		}
	]
}

# 分页查询 -- from-分页当前的位置  size-显示文档的总数
GET /hotel/_search
{
	"query": {
		"match_all": {}
	},
	"sort":[
		{
			"price": "asc"
		}
	],
	"from": 0,
	"size": 10
}

# 高亮查询,默认情况下,ES搜索字段必须与高亮字段一致,可以将"require_field_match":"false"-关闭搜索字段和高亮字段匹配
GET /hotel/_search
{
	"query": {
		"match": {
			"all": "如家"
		}
	},
	"highlight":{
		"fields":{
			"name":{
				"require_field_match":"false"
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  1. RestClient查询文档–利用JavaRestClient查询文档。(P111)
  2. 基本步骤–创建SearchRequest对象-准备Request.source(),其中QueryBuilders来构建查询条件,再传入query()方法-发送请求,得到结果-解析结果(参考JSON结果,从外到内,逐层解析)。
  3. 全文检索 – 要构建条件只需要QueryBuilders。
  4. 高亮–高亮结果解析是参考JSON结果,逐层解析。
数据聚合
  1. 聚合–对文档数据的统计、分析、计算。(P120)
  2. 聚合的常见种类
  3. Bucket(桶聚合):对文档数据分组;TermAggregation:按照文档字段分组;Date Histogram:按日期阶梯分组,如一周或一月为一组。
  4. Metric(度量聚合或嵌套聚合):对文档数据做计算,例如avg、min、max、status(同时求sum、min等)等;
  5. Pipeline(管道聚合):基于其它聚合结果再做聚合。
  6. 参与聚合的字段类型必须为:keyword、数值、日期、布尔。
  7. DSL实现Bucket聚合(P121)
  8. aggs代表聚合,与query同级;query的作用:限定聚合的的文档范围。
  9. 聚合必须的三要素:聚合名称、聚合类型、聚合字段。
  10. 聚合可配置属性有:size:指定聚合结果数量;order:指定聚合结果排序方式;field:指定聚合字段。
  11. DSL实现Metrics聚合(P122)
  12. ResrClient实现聚合(P123)
# 统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合
# size-设置size为0,结果中不包含文档,只包含聚合结果
# aggs-定义聚合    brandAgg-给聚合起个名字
# terms-聚合的类型,按照品牌值聚合,所以选择
# field-参与聚合的字段  size- 希望获取的聚合结果数量
GET /hotel/_search
{
	"size": 0,
	"aggs": {
		"brandAgg": {
			"terms": {
				"field":"brand",
				"size": 10
			}
		}
	}
}
# Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序
GET /hotel/_search
{
	"size": 0,
	"aggs": {
		"brandAgg": {
			"terms": {
				"field":"brand",
				"size": 10,
				"order": {
					"_count": "asc"
				}
			}
		}
	}
}
# Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件
GET /hotel/_search
{
	"query": {
		"range": {
			"price": {
				"lte": 200
			}
		}
	},
	"size": 0,
	"aggs": {
		"brandAgg": {
			"terms": {
				"field":"brand",
				"size": 10
			}
		}
	}
}

# 获取每个品牌的用户评分的min、max、avg等值.
# aggs-brands聚合的子聚合,也就是分组后对每组分别计算
# scoreAgg-聚合名称
# stats-聚合类型,这里stats可以计算min、max、avg等
# field-聚合字段,这里是score
GET /hotel/_search
{
	"size": 0,
	"aggs": {
		"brandAgg": {
			"terms": {
				"field":"brand",
				"size": 10,
				"order": {
					"scoreAgg.avg": "desc"
				}
			},
			"aggs": {
				"scoreAgg": {
					"stats": {
						"field": "score"
					}
				}
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  1. 自动补全(P126)
  2. 安装拼音分词器,测试。
  3. 自定义分词器–elasticsearch中分词器(analyzer)的组成包含三部分:
  4. character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符。
  5. tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart。
  6. tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等。
# 安装pinyin分词器
# 查看数据卷elasticsearch的plugins目录位置
docker volume inspect es-plugins
# 到这个目录下
cd /var/lib/docker/volumes/es-plugins/_data
# 使用FileZillar直接传输Windows下解压的pinyin分词器的文件夹,结果是成功的
# 重启es容器
docker restart es
# 查看es日志
docker logs -f es
# 测试拼音分词器
GET /_analyze
{
  "text": ["如家酒店还不错"],
  "analyzer": "pinyin"
}

# 删除索引库
DELETE /test

# 自定义拼音分词器,创建索引库时,通过settings来配置自定义的analyzer(分词器);拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。--导致多音字都被搜出来
# 创建倒排索引时应该用my_analyzer分词器  -- analyzer;
# 字段在搜索时应该使用ik_smart分词器 -- search_analyzer;
PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": {
        "py": { 
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings":{
  	"properties":{
  		"name": {
  			"type": "text",
  			"analyzer": "my_analyzer",
  			"search_analyzer": "ik_smart"
  		}
  	}
  }
}
# 测试自定义分词器
GET /test/_analyze
{
  "text": ["如家酒店还不错"],
  "analyzer": "pinyin"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  1. 自动补全completion suggester查询-实现自动补全功能。(P128)
  2. 自动补全对字段的要求:类型是completion类型;字段值是多词条的数组。
  3. 案例:酒店数据自动补全–实现hotel索引库的自动补全、拼音搜索功能。(P130)
# 自动补全的索引库
PUT test1
{
	"mappings":{
        "properties":{
            "title": {
                "type": "completion"
            }
        }
  }
}
# 示例数据
POST test1/_doc
{
	"title":["Sony", "WH-1000XM3"]
}
POST test1/_doc
{
	"title":["SK-II", "PITERA"]
}
POST test1/_doc
{
	"title":["Nintendo", "switch"]
}
# 自动补全查询
POST /test1/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", # 关键字
      "completion": {
        "field": "title", # 补全字段
        "skip_duplicates": true, # 跳过重复的
        "size": 10 # 获取前10条结果
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  1. 数据同步elasticsearchmysql之间的数据同步(P132)
  2. 问题:微服务中,负责酒店管理(操作mysql )的业务与负责酒店搜索(操作elasticsearch )的业务可能在两个不同的微服务上,数据同步该如何实现呢? 解决办法
  3. 方式一:同步调用;优点:实现简单,粗暴;缺点:业务耦合度高。
  4. 方式二:异步通知;优点:低耦合,实现难度一般;缺点:依赖mq的可靠性。
  5. 方式三:监听binlog;优点:完全解除服务间耦合;缺点:开启binlog增加数据库负担、实现复杂度高。–使用canal中间件。
  6. ES集群结构(P138)
  7. 单机的elasticsearch做数据存储,必然面临两个问题
  8. 海量数据存储问题–将索引库从逻辑上拆分为N个分片(shard),存储到多个节点。
  9. 单点故障问题–将分片数据在不同节点备份(replica )。
  10. 每个索引库的分片数量、副本数量都是在创建索引库时指定的,并且分片数量一旦设置以后无法修改。
  11. elasticsearch中集群节点有不同的职责:
  12. master eligi(主节点)–备选主节点:主节点可以管理和记录集群状态、决定分片在哪个节点、处理创建和删除索引库的请求。
  13. data(数据节点)–数据节点:存储数据、搜索、聚合、CRUD。
  14. ingest–数据存储之前的预处理。
  15. coordinating(协调节点)–路由请求到其它节点合并其它节点处理的结果,返回给用户。
  16. ES集群的脑裂–当主节点与其他节点网络故障时,可能发生脑裂问题。
  17. 协调节点的作用
  18. 分布式新增如何确定分片–coordinating node根据idhash运算,得到结果对shard数量取余,余数就是对应的分片。
  19. 分布式查询的两个阶段。
  20. 分散阶段: coordinating node将查询请求分发给不同分片。
  21. 收集阶段:将查询结果汇总到coordinating node ,整理并返回给用户。
  22. 故障转移–master宕机后,EligibleMaster选举为新的主节点;master节点监控分片、节点状态,将故障节点上的分片转移到正常节点,确保数据安全。
黑马旅游
  1. 黑马旅游案例
  2. 基本搜索和分页–案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页。(P115)
  3. 条件过滤–案例2:添加品牌、城市、星级、价格等过滤功能。
  4. 附近的酒店–案例3:我附近的酒店。
  5. 广告置顶–案例4:让指定的酒店在搜索结果中排名置顶。
  6. 排序–给黑马旅游添加排序功能。
  7. 高亮显示–给黑马旅游添加搜索关键字高亮效果。
  8. 聚合-RestClient–在IUserService中定义方法,实现对品牌、城市、星级的聚合。(P124)
  9. 酒店搜索页面自动补全–实现酒店搜索页面输入框的自动补全。(P131)
  10. 消息同步–利用MQ实现mysql与elasticsearch数据同步。(P133)

->微服务技术栈课程视频

https://www.bilibili.com/video/BV1LQ4y127n4?p=1

<-

记录每一个学习瞬间

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/157913
推荐阅读
相关标签
  

闽ICP备14008679号