5、高级篇
5、高级篇
5.1、整合Elasticsearch
ElasticSearch概念-基础概念

ElasticSearch概念-倒排索引
词 | 记录 |
---|---|
红海 | 1,2,3,4,5 |
行动 | 1,2,3 |
探索 | 2,5 |
特别 | 3,5 |
记录篇 | 4 |
特工 | 5 |
分词
:将整句分拆为单词
保存的记录
红海行动
探索红海行动
红海特别行动
红海记录篇
特工红海特别探索
检索:
1、红海特工行动?
2、红海行动?
相关性得分:
ElasticSearch7-去掉type概念
关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES 中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同 的filed最终在Lucene中的处理方式是一样的。
两个不同type下的两个user_name,在ES同一个索引下其实被认为是同一个filed,你必 须在两个不同的type中定义相同的filed映射。否则,不同type中的相同字段名称就会在 处理中出现冲突的情况,导致Lucene处理效率下降。
去掉type就是为了提高ES处理数据的效率。
Elasticsearch 7.x
- URL中的type参数为可选。比如,索引一个文档不再要求提供文档类型。
Elasticsearch 8.x
- 不再支持URL中的type参数。
解决:将索引从多类型迁移到单类型,每种类型文档一个独立索引
5.1.1、简介
1、Elasticsearch 是什么?
Elasticsearch 是一个分布式的免费开源搜索和分析引擎,适用于包括文本、数字、地理空间、结构化和非结构化数据等在内的所有类型的数据。Elasticsearch 在 Apache Lucene 的基础上开发而成,由 Elasticsearch N.V.(即现在的 Elastic)于 2010 年首次发布。Elasticsearch 以其简单的 REST 风格 API、分布式特性、速度和可扩展性而闻名,是 Elastic Stack 的核心组件;Elastic Stack 是一套适用于数据采集、扩充、存储、分析和可视化的免费开源工具。人们通常将 Elastic Stack 称为 ELK Stack(代指 Elasticsearch、Logstash 和 Kibana),目前 Elastic Stack 包括一系列丰富的轻量型数据采集代理,这些代理统称为 Beats,可用来向 Elasticsearch 发送数据。
2、Elasticsearch 的用途是什么?
Elasticsearch 在速度和可扩展性方面都表现出色,而且还能够索引多种类型的内容,这意味着其可用于多种用例:
- 应用程序搜索
- 网站搜索
- 企业搜索
- 日志处理和分析
- 基础设施指标和容器监测
- 应用程序性能监测
- 地理空间数据分析和可视化
- 安全分析
- 业务分析
3、Elasticsearch 的工作原理是什么?
原始数据会从多个来源(包括日志、系统指标和网络应用程序)输入到 Elasticsearch 中。数据采集指在 Elasticsearch 中进行索引之前解析、标准化并充实这些原始数据的过程。这些数据在 Elasticsearch 中索引完成之后,用户便可针对他们的数据运行复杂的查询,并使用聚合来检索自身数据的复杂汇总。在 Kibana 中,用户可以基于自己的数据创建强大的可视化,分享仪表板,并对 Elastic Stack 进行管理。
4、Elasticsearch 索引是什么?
Elasticsearch 索引指相互关联的文档集合。Elasticsearch 会以 JSON 文档的形式存储数据。每个文档都会在一组键(字段或属性的名称)和它们对应的值(字符串、数字、布尔值、日期、数值组、地理位置或其他类型的数据)之间建立联系。
Elasticsearch 使用的是一种名为倒排索引的数据结构,这一结构的设计可以允许十分快速地进行全文本搜索。倒排索引会列出在所有文档中出现的每个特有词汇,并且可以找到包含每个词汇的全部文档。
在索引过程中,Elasticsearch 会存储文档并构建倒排索引,这样用户便可以近实时地对文档数据进行搜索。索引过程是在索引 API 中启动的,通过此 API 您既可向特定索引中添加 JSON 文档,也可更改特定索引中的 JSON 文档。
5、Logstash 的用途是什么?
Logstash 是 Elastic Stack 的核心产品之一,可用来对数据进行聚合和处理,并将数据发送到 Elasticsearch。Logstash 是一个开源的服务器端数据处理管道,允许您在将数据索引到 Elasticsearch 之前同时从多个来源采集数据,并对数据进行充实和转换。
6、Kibana 的用途是什么?
Kibana 是一款适用于 Elasticsearch 的数据可视化和管理工具,可以提供实时的直方图、线形图、饼状图和地图。Kibana 同时还包括诸如 Canvas 和 Elastic Maps 等高级应用程序;Canvas 允许用户基于自身数据创建定制的动态信息图表,而 Elastic Maps 则可用来对地理空间数据进行可视化。
7、为何使用 Elasticsearch?
**Elasticsearch 很快。**由于 Elasticsearch 是在 Lucene 基础上构建而成的,所以在全文本搜索方面表现十分出色。Elasticsearch 同时还是一个近实时的搜索平台,这意味着从文档索引操作到文档变为可搜索状态之间的延时很短,一般只有一秒。因此,Elasticsearch 非常适用于对时间有严苛要求的用例,例如安全分析和基础设施监测。
**Elasticsearch 具有分布式的本质特征。**Elasticsearch 中存储的文档分布在不同的容器中,这些容器称为分片,可以进行复制以提供数据冗余副本,以防发生硬件故障。Elasticsearch 的分布式特性使得它可以扩展至数百台(甚至数千台)服务器,并处理 PB 量级的数据。
**Elasticsearch 包含一系列广泛的功能。**除了速度、可扩展性和弹性等优势以外,Elasticsearch 还有大量强大的内置功能(例如数据汇总和索引生命周期管理),可以方便用户更加高效地存储和搜索数据。
**Elastic Stack 简化了数据采集、可视化和报告过程。**通过与 Beats 和 Logstash 进行集成,用户能够在向 Elasticsearch 中索引数据之前轻松地处理数据。同时,Kibana 不仅可针对 Elasticsearch 数据提供实时可视化,同时还提供 UI 以便用户快速访问应用程序性能监测 (APM)、日志和基础设施指标等数据。
5.1.2、安装Elasticsearch
1、准备工作
先把要使用的Oracle VM VirtualBox虚拟机内存调大,这里我调到1G

然后在VirtualBox VMs的安装目录
使用vagrant up
命令启动Oracle VM VirtualBox虚拟机

然后在VirtualBox VMs的安装目录
使用vagrant ssh
连接虚拟机

使用sudo docker ps
命令查看正在运行的镜像

使用sudo docker images
命令查看已下载的镜像

2、下载
elasticsearch
1、下载使用sudo docker pull elasticsearch:7.4.2
命令下载elasticsearch

kibana
2、下载使用sudo docker pull kibana:7.4.2
下载kibana

使用sudo docker images
命令查看已下载的镜像

使用free -m
查看内存使用情况,可以看到内存还剩403

3、运行
elasticsearch
1、运行完整命令(-e ES_JAVA_OPTS="-Xms64m -Xmx128m"
这里推荐修改为-e ES_JAVA_OPTS="-Xms64m -Xmx512m"
)
su root
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
cd /mydata/elasticsearch/config
ls
cat elasticsearch.yml
chmod -R 777 /mydata/elasticsearch
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
以下是对命令的解释
su root #提升至管理员权限,密码默认为 vagrant
mkdir -p /mydata/elasticsearch/config #在linux虚拟机里创建/mydata/elasticsearch/config目录,-p允许创建目录及子目录
mkdir -p /mydata/elasticsearch/data #在linux虚拟机里创建/mydata/elasticsearch/data目录,-p允许创建目录及子目录
#在/mydata/elasticsearch/config/elasticsearch.yml文件里写入数据 http.host: 0.0.0.0
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml #注意第二个"冒号后面有个空格
cd /mydata/elasticsearch/config #进入到/mydata/elasticsearch/config目录
ls #查看当前目录的文件及文件夹
cat elasticsearch.yml #查看elasticsearch.yml文件的内容
#9200是发送REST API类型的http请求所用的端口,9300是elasticsearch在分布式集群状态下节点之间通讯所用的端口
# \ 斜杠表示当前行没写完,下一行接着写
#-e "discovery.type=single-node" 单节点运行
#-e ES_JAVA_OPTS="-Xms64m -Xmx128m" 初始占用64m,最大占用128m. 这个配置非常重要,如果不配置会占用虚拟机的全部内存,最后直 接卡死了
#-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml 将 docker容器的/usr/share/elasticsearch/config/elasticsearch.yml文件与linux虚拟机 的/mydata/elasticsearch/config/elasticsearch.yml文件进行关联
#-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data 将docker的/usr/share/elasticsearch/data目录与 linux虚拟机的/mydata/elasticsearch/data目录进行关联
#-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins 将docker的/usr/share/elasticsearch/plugins 目录与linux虚拟机的/mydata/elasticsearch/plugins目录进行关联,以后装插件就不用进入docker容器内部了
#-d elasticsearch:7.4.2 用elasticsearch:7.4.2这个镜像后台启动elasticsearch
可以看到elasticsearch
并没有运行起来

使用docker logs elasticsearch
命令,查看elasticsearch
的日志
可以发现出现了拒绝访问异常
"Caused by: java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data/nodes"

修改/mydata/elasticsearch
目录下的所有子目录和文件的权限,让所有用户都有可执行权限
cd ..
ls
ll
chmod -R 777 /mydata/elasticsearh/
pwd
chmod -R 777 /mydata/elasticsearch
ll
最开始只有管理员用户有写权限
将/mydata/elasticsearch
目录下的所有子目录和文件的权限都修改为777,即可读可写可执行
可以看到所有用户都有可读可写可执行
权限

启动elasticsearch
docker ps
docker ps -a
docker start elasticsearch
docker ps

浏览器输入 http://192.168.56.10:9200/ 可以看到已经访问成功了
(记得要修改成自己在VirtualBox VMs\Vagrantfile
文件里配置的ip)
在VirtualBox VMs\Vagrantfile
文件里的 config.vm.network "private_network", ip: "192.168.56.10"
这个位置,这里的ip即为自己配置的ip

kibana
2、运行这里的http://192.168.56.10:9200
要改为自己配置的地址
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601 \
-d kibana:7.4.2

使用docker ps
命令,查看正在运行的镜像,可以看到kibana
已经运行起来了

浏览器输入: http://192.168.56.10:5601/ ,即可看到欢迎界面

选择No
->Explore on my own
即可进入到主界面

3、发送请求
url: http://192.168.56.10:9200/

输入url: http://192.168.56.10:9200/_cat/nodes
查看所有节点信息,返回的结果中有*
表明该节点是主节点,104b52df3ff1
即为节点的名字

4、常用命令
elasticsearch
1、启动使用docker start elasticsearch
命令启动elasticsearch
docker ps -a #显示所有的容器,包括未运行的
docker start elasticsearch #启动elasticsearch
docker ps -a

elasticsearch
2、重启使用docker restart elasticsearch
命令,重启elasticsearch
docker restart elasticsearch

elasticsearch
3、关闭使用docker stop elasticsearch
命令,关闭elasticsearch
docker stop elasticsearch

4、设置开机自启
设置elasticsearch
开机自启
sudo docker update elasticsearch --restart=always

设置kibana
开机自启
sudo docker update kibana --restart=always

重启linux
虚拟机,可以看到elasticsearch
和kibana
都已经自启了
vagrant ssh
sudo docker ps

5.1.3、Elasticsearch入门
Elasticsearch
信息
1、查看GET /_cat/nodes:查看所有节点
GET /_cat/health:查看 es 健康状况
GET /_cat/master:查看主节点
GET /_cat/indices:查看所有索引 相当于数据库的“show databases”
1、查看所有节点
http://192.168.56.10:9200/_cat/nodes
*表示的是主节点

2、查看健康状况
http://192.168.56.10:9200/_cat/health
green表示健康

3、查看主节点

4、查看所有索引

2、简单增查改
put
方式添加或修改数据
1、put
方式发送请求
1、在Postman中发送请求
输入
http://192.168.56.10:9200/customer/external/1
选择请求方式为PUT
点击Body
点击raw
选择JSON
点击Send
{
"name": "John Doe"
}
返回的数据信息
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
这些带_
的都称为元数据
"_index": "customer",
-> 在customer
索引下,相当于mysql中的数据库
"_type": "external",
-> 在external
类型下,相当于mysql中的表 (新版本已经没有_type
了,统一为_doc
)
"_id": "1",
-> id为1
"_version": 1,
-> 版本号为1
"result": "created",
-> _result为新建状态
_shards是集群信息

2、再次发送数据
再次点击Send
版本号就变为了2
状态就变为了updated

put
方式添加数据不允许不带id
3、uri [/customer/external] 和方法 [PUT] 的 HTTP 方法不正确,允许:[POST]
{
"error": "Incorrect HTTP method for uri [/customer/external] and method [PUT], allowed: [POST]",
"status": 405
}

post
方式添加或修改数据
2、新增: 不带id;带id但之前没数据 修改: 带id,并且有数据
post
方式发送请求
1、不带id用修改请求: http://192.168.56.10:9200/customer/external ,这次不指定id
请求方式改为
Post
再次点击Send
返回的数据
{
"_index": "customer",
"_type": "external",
"_id": "fUi6vIEBNNJSS-0BII0-",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1
}
这时会自动创建一个唯一id
版本号为1
状态为新建

2、再次发送数据
再次点击Send,又重新新建了一个数据

post
方式发送请求
3、带id用这次指定id,输入 http://192.168.56.10:9200/customer/external/2 ,点击Send
这时状态为新建

4、再次发送数据
这时状态为更新

3、查询和修改数据
1、基础查询数据
输入
http://192.168.56.10:9200/customer/external/1
请求方式选择
GET
点击
Send
返回的数据
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 2,
"_seq_no": 1,
"_primary_term": 1,
"found": true,
"_source": {
"name": "John Doe"
}
}
"_index": "customer",
-> 在customer
索引下,相当于mysql中的数据库"_type": "external",
-> 在external
类型下,相当于mysql中的表 (新版本已经没有_type
了,统一为_doc
)"_id": "2",
-> id为2"_version": 1,
-> 版本号为1"_seq_no": 1,
-> 做乐观锁操作的,只有数据一有改动_seq_no
相当于序列号,就会往上加"_primary_term": 1,
-> 分片发生了变化_primary_term
也会往上加"found": true,
-> 表明找到了数据_source
-> 内容在_source
里面

2、乐观锁修改数据
开两个请求窗口
请求url都输入: http://192.168.56.10:9200/customer/external/1?if_seq_no=1&if_primary_term=1
请求方式都为PUT
第一个请求里输入以下json,然后点击Send
{
"name": "1"
}
以下是响应内容,可以看到修改成功了
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 6,
"_primary_term": 1
}

第二个请求里输入以下json,然后点击Send
{
"name": "2"
}
以下是响应内容,可以看到修改失败了
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [6] and primary term [1]",
"index_uuid": "M5lWculHRVWJOSy2fVN_3Q",
"shard": "0",
"index": "customer"
}
],
"type": "version_conflict_engine_exception",
"reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [6] and primary term [1]",
"index_uuid": "M5lWculHRVWJOSy2fVN_3Q",
"shard": "0",
"index": "customer"
},
"status": 409
}

这时需要再次查询_seq_no

修改_seq_no
为刚才查到的_seq_no
的值,再次发送数据,可以看到这次查询成功了

_update
修改数据
3、带带
_update
方式会对比原来数据,与原来一样就什么都不做,version, seq_no都不变
适用于 对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则
请求的url: http://192.168.56.10:9200/customer/external/1/_update
请求方式为POST
,输入以下json
{
"doc":{
"name": "John"
}
}
不管发送多少次,只要内容不变响应数据都为以下内容
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 7,
"result": "noop",
"_shards": {
"total": 0,
"successful": 0,
"failed": 0
},
"_seq_no": 10,
"_primary_term": 1
}

_update
修改数据
4、不带put和post (不带_update) 都会直接更新数据, 对于大并发更新,不带 update
请求的url: http://192.168.56.10:9200/customer/external/1
请求方式为POST
,输入以下json
{
"name": "John"
}
每发送一次请求,_version
都会加一
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 8,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 11,
"_primary_term": 1
}

{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 9,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 12,
"_primary_term": 1
}

put
请求也一样不比较,直接更新
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 10,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 13,
"_primary_term": 1
}

{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 11,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 14,
"_primary_term": 1
}

4、更新同时增加属性
_update
使用PUT
方式添加属性
1、不带url输入:http://192.168.56.10:9200/customer/external/1
请求方式选择
PUT
请求体输入以下json
{
"name": "John",
"age":20
}
- 点击Send
可以看到请求成功了
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 12,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 15,
"_primary_term": 1
}

url输入:http://192.168.56.10:9200/customer/external/1
请求方式选择
GET
可以看到更新成功了
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 12,
"_seq_no": 15,
"_primary_term": 1,
"found": true,
"_source": {
"name": "John",
"age": 20
}
}

_update
使用POST
方式添加属性
2、带url输入: http://192.168.56.10:9200/customer/external/1/_update
请求方式选择
POST
输入以下json,带
_update
方式需要使用"doc"
{
"doc":{
"name": "John",
"age":20
}
}
- 点击Send
响应为以下内容
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 12,
"result": "noop",
"_shards": {
"total": 0,
"successful": 0,
"failed": 0
},
"_seq_no": 15,
"_primary_term": 1
}

📝PUT 和 POST 不带_updat
5、删除数据
1、删除某个数据
url输入: http://192.168.56.10:9200/customer/external/1
请求方式选择
DELETE
点击Send
可以看到删除成功了,响应数据为以下内容
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 13,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 16,
"_primary_term": 1
}

查询数据可以发现,响应状态为404
, "found"为false
{
"_index": "customer",
"_type": "external",
"_id": "1",
"found": false
}

2、删除索引
url输入: http://192.168.56.10:9200/customer
请求方式选择
DELETE
点击Send
响应的数据为以下内容
{
"acknowledged": true
}

再次查询索引为1
的数据,可以看到这次直接说索引没有找到
{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [customer]",
"resource.type": "index_expression",
"resource.id": "customer",
"index_uuid": "_na_",
"index": "customer"
}
],
"type": "index_not_found_exception",
"reason": "no such index [customer]",
"resource.type": "index_expression",
"resource.id": "customer",
"index_uuid": "_na_",
"index": "customer"
},
"status": 404
}

6、批量添加数据
Dev Tools
1、打开浏览器输入url: http://192.168.56.10:5601/ ,在菜单栏点击Dev Tools

2、发送简单请求
批量添加数据格式
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
执行以下语句,这些批量操作都是独立的,上一个失败并不会影响下一个的执行
index
表示的是新增
或修改
POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
响应的结果为
#! Deprecation: [types removal] Specifying types in bulk requests is deprecated.
{
"took" : 427,
"errors" : false,
"items" : [
{
"index" : {
"_index" : "customer",
"_type" : "external",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1,
"status" : 201
}
},
{
"index" : {
"_index" : "customer",
"_type" : "external",
"_id" : "2",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 201
}
}
]
}

3、发送复杂请求
发送以下复杂请求
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"}}
响应的内容为
#! Deprecation: [types removal] Specifying types in bulk requests is deprecated.
{
"took" : 286,
"errors" : false,
"items" : [
{
"delete" : {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1,
"status" : 404
}
},
{
"create" : {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 2,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 201
}
},
{
"index" : {
"_index" : "website",
"_type" : "blog",
"_id" : "f0hgvYEBNNJSS-0BkY3i",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1,
"status" : 201
}
},
{
"update" : {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1,
"status" : 200
}
}
]
}

4、批量添加测试数据
输入以下数据,然后点击运行

5、查看索引
url输入: http://192.168.56.10:9200/_cat/indices
请求方式选择GET
点击Send
可以看到添加了以下索引
yellow open website wXyluvLtRWike6KwFEsecw 1 1 2 2 8.6kb 8.6kb
yellow open bank NP_JYORgQlSTFQyh1b3TzA 1 1 1000 0 427.9kb 427.9kb
green open .kibana_task_manager_1 sl-lzQgzRF2K2l6P5FmlFQ 1 0 2 0 30.4kb 30.4kb
green open .apm-agent-configuration pG-exF26R4yCs2jJOSwAcA 1 0 0 0 283b 283b
green open .kibana_1 SSGlLaInQl-tlmJqLJA6Fw 1 0 8 0 25.3kb 25.3kb
yellow open customer EKZizrVgRfGj8NP-mDH3qw 1 1 2 0 3.5kb 3.5kb

5.1.4、Elasticsearch进阶
参考文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.5/getting-started-search.html

1、基本方式检索
ES 支持两种基本方式检索 :
一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
另一个是通过使用 REST request body 来发送它们(uri+请求体)
请求命令
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}
响应格式
{
"took" : 63,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value": 1000,
"relation": "eq"
},
"max_score" : null,
"hits" : [ {
"_index" : "bank",
"_type" : "_doc",
"_id" : "0",
"sort": [0],
"_score" : null,
"_source" : {"account_number":0,"balance":16623,"firstname":"Bradshaw","lastname":"Mckenzie","age":29,"gender":"F","address":"244 Columbus Place","employer":"Euron","email":"[email protected]","city":"Hobucken","state":"CO"}
}, {
"_index" : "bank",
"_type" : "_doc",
"_id" : "1",
"sort": [1],
"_score" : null,
"_source" : {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"[email protected]","city":"Brogan","state":"IL"}
}, ...
]
}
}
The response also provides the following information about the search request:
took
– how long it took Elasticsearch to run the query, in millisecondstimed_out
– whether or not the search request timed out_shards
– how many shards were searched and a breakdown of how many shards succeeded, failed, or were skipped.max_score
– the score of the most relevant document foundhits.total.value
- how many matching documents were foundhits.sort
- the document’s sort position (when not sorting by relevance score)hits._score
- the document’s relevance score (not applicable when usingmatch_all
)
该响应还提供有关搜索请求的以下信息:
took
– Elasticsearch 运行查询需要多长时间,以毫秒为单位timed_out
– 搜索请求是否超时_shards
– 搜索了多少分片,以及多少分片成功、失败或被跳过的细分。max_score
– 找到的最相关文档的分数hits.total.value
- 找到了多少匹配的文档hits.sort
- 文档的排序位置(不按相关性分数排序时)hits._score
- 文档的相关性分数(使用时不适用match_all
)
uri+检索参数
检索
1、使用通过使用 REST request URI 发送搜索参数(uri+检索参数)
在kibana
中输入以下命令
GET bank/_search?q=*&sort=account_number:asc
q=*
表示查询所有
sort=account_number:asc
表示按照account_number
升序排列

uri+请求体
检索
2、使用通过使用 REST request body 来发送它们(uri+请求体)
在kibana
中输入以下命令
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}

3、排序查询
也可以先按account_number
升序,再按balance
降序
GET /bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{ "account_number": "asc" },
{"balance": "desc"}
]
}

4、分页查询某些字段
"from": 5,"size": 5,
:从第5条数据开始,查询5条数据
"_source": ["balance", "firstname"]
:查询balance和 firstname字段
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"balance": {
"order": "desc"
}
}
],
"from": 5,
"size": 5,
"_source": ["balance", "firstname"]
}

match
匹配查询
2、基本类型(非字符串),精确匹配 ->
"account_number": "20"
match 返回 account_number=20 的字符串,全文检索 ->
"address": "mill"
match 当搜索字符串类型的时候,会进行全文检索,并且每条记录有相关性得分。字符串,多个单词(分词+全文检索)->
"address": "mill road"
最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录,并给出相关性得分
1、精确匹配
GET bank/_search
{
"query": {
"match": {
"account_number": 20
}
}
}

2、模糊匹配1
GET bank/_search
{
"query": {
"match": {
"address": "Kings"
}
}
}
可以看到address
中包含有Kings
的都检索出来了

3、模糊匹配2
GET bank/_search
{
"query": {
"match": {
"address": "mill lane"
}
}
}
可以看到,只要包含了mill
或lane
的都能查出来,其中198 Mill Lane
的得分最高,默认按照评分由高到低排列

match_phrase
短语(不分词)匹配
3、将需要匹配的值当成一个整体单词(不分词)进行检索 -> "address": "mill
查出 address 中包含 mill road 的所有记录,并给出相关性得
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill lane"
}
}
}
此时只能查询出多个单词全存在的数据

使用match_phrase
匹配时必须指定字段包含该不分词的短语,但该字段不必须只包含该短语
GET bank/_search
{
"query": {
"match_phrase": {
"address": "789 Madison"
}
}
}

使用FIELD.keyword
匹配必须指定字段为该不分词的短语
GET bank/_search
{
"query": {
"match": {
"address.keyword": "789 Madison"
}
}
}

multi_match
多字段匹配
4、多个字段其中有任何一个字段匹配
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["address","city"]
}
}
}

GET bank/_search
{
"query": {
"multi_match": {
"query": "mill movico",
"fields": ["address","city"]
}
}
}
可以看到多字段匹配用的也是match匹配查询
,也查询出了包含分词的所有数据;包含的分词越多,得分越高

bool
复合查询
5、To construct more complex queries, you can use a
bool
query to combine multiple query criteria. You can designate criteria as required (must match), desirable (should match), or undesirable (must not match).
要构造更复杂的查询,您可以使用一个bool
查询来组合多个查询条件。您可以根据需要(必须匹配)、需要(应该匹配)或不需要(必须不匹配)指定条件。
bool 用来做复合查询: 复合语句可以合并任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
Each
must
,should
, andmust_not
element in a Boolean query is referred to as a query clause. How well a document meets the criteria in eachmust
orshould
clause contributes to the document’s relevance score. The higher the score, the better the document matches your search criteria. By default, Elasticsearch returns documents ranked by these relevance scores.The criteria in a
must_not
clause is treated as a filter. It affects whether or not the document is included in the results, but does not contribute to how documents are scored. You can also explicitly specify arbitrary filters to include or exclude documents based on structured data.
布尔查询中的每个must
、should
和must_not
元素都称为查询子句。文档满足每个**must
或 should
子句中的标准的程度会影响文档的相关性评分**。分数越高,文档越符合您的搜索条件。默认情况下,Elasticsearch 返回按这些相关性分数排序的文档。
子句中的条件**must_not
被视为过滤器。它影响文档是否包含在结果中,但不影响文档的评分**方式。您还可以显式指定任意过滤器以根据结构化数据包含或排除文档。
1、必须满足&必须不满足
must:必须达到 must 列举的所有条件
必须满足"gender": "M"
和 "address": "mill"
必须不满足 "age": "38"
GET bank/_search
{
"query": {
"bool": {
"must":[
{"match": {
"gender": "M"
}},
{"match": {
"address": "mill"
}}
],
"must_not":[
{"match": {
"age": "38"
}}
]
}
}
}

2、应该匹配
should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会被作为默认匹配条件而去改变查询结果
GET bank/_search
{
"query": {
"bool": {
"must":[
{"match": {
"gender": "M"
}},
{"match": {
"address": "mill"
}}
],
"must_not":[
{"match": {
"age": "18"
}}
],
"should": [
{"match": {
"lastname": "Wallace"
}}
]
}
}
}

filter
结果过滤
6、可以看到使用must
会有相关性得分
GET bank/_search
{
"query": {
"bool": {
"must": [
{"range": {
"age": {
"gte": 18,
"lte": 30
}
}
},
{"match": {
"address": "mill"
}}
]
}
}
}

而使用filter
没有相关性得分
GET bank/_search
{
"query": {
"bool": {
"filter": [
{"range": {
"age": {
"gte": 18,
"lte": 30
}
}
},
{"match": {
"address": "mill"
}}
]
}
}
}

Term query
7、term
和match
一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配(精确的字段匹配)用 term。
参考文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.5/query-dsl-term-query.html

Term query
Returns documents that contain an exact term in a provided field.
You can use the
term
query to find documents based on a precise value such as a price, a product ID, or a username.Avoid using the
term
query fortext
fields.By default, Elasticsearch changes the values of
text
fields as part of analysis. This can make finding exact matches fortext
field values difficult.To search
text
field values, use thematch
query instead.
Term查询
返回在提供的字段中包含确切术语的文档。
您可以使用term
查询根据价格、产品 ID 或用户名等精确值查找文档。
避免使用字段term
查询text
默认情况下,Elasticsearch 会在分析text
过程中更改字段的值。这会使查找字段值的精确匹配变得困难text
要搜索text
字段值,请改用match
查询。
使用match
可以查询"age": "28"
的数据
GET bank/_search
{
"query": {
"match": {
"age": "28"
}
}
}

使用term
也可以查询"age": "28"
的数据,推荐使用term
GET bank/_search
{
"query": {
"term": {
"age": "28"
}
}
}

使用term
查询多字段text
会查询不到
GET bank/_search
{
"query": {
"term": {
"address": "789 Madison Street"
}
}
}

aggregations
执行聚合
8、参考文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.5/search-aggregations.html

The aggregations framework helps provide aggregated data based on a search query. It is based on simple building blocks called aggregations, that can be composed in order to build complex summaries of the data.
An aggregation can be seen as a unit-of-work that builds analytic information over a set of documents. The context of the execution defines what this document set is (e.g. a top-level aggregation executes within the context of the executed query/filters of the search request).
There are many different types of aggregations, each with its own purpose and output. To better understand these types, it is often easier to break them into four main families:
A family of aggregations that build buckets, where each bucket is associated with a key and a document criterion. When the aggregation is executed, all the buckets criteria are evaluated on every document in the context and when a criterion matches, the document is considered to "fall in" the relevant bucket. By the end of the aggregation process, we’ll end up with a list of buckets - each one with a set of documents that "belong" to it.
Aggregations that keep track and compute metrics over a set of documents.
A family of aggregations that operate on multiple fields and produce a matrix result based on the values extracted from the requested document fields. Unlike metric and bucket aggregations, this aggregation family does not yet support scripting.
Aggregations that aggregate the output of other aggregations and their associated metrics
The interesting part comes next. Since each bucket effectively defines a document set (all documents belonging to the bucket), one can potentially associate aggregations on the bucket level, and those will execute within the context of that bucket. This is where the real power of aggregations kicks in: aggregations can be nested!
聚合框架有助于提供基于搜索查询的聚合数据。它基于称为聚合的简单构建块,可以组合这些块以构建复杂的数据摘要。
聚合可以看作是在一组文档上构建分析信息*的工作单元。*执行的上下文定义了该文档集是什么(例如,顶级聚合在搜索请求的已执行查询/过滤器的上下文中执行)。
有许多不同类型的聚合,每种都有自己的目的和输出。为了更好地理解这些类型,通常更容易将它们分为四个主要系列:
- *分桶*:构建存储桶的聚合系列,其中每个存储桶都与一个键和一个文档标准相关联。执行聚合时,将对上下文中的每个文档评估所有存储桶标准,并且当标准匹配时,该文档被认为“落入”相关存储桶中。在聚合过程结束时,我们将得到一个桶列表——每个桶都有一组“属于”它的文档。
- *公制*:跟踪和计算一组文档的指标的聚合。
- *矩阵*:对多个字段进行操作并根据从请求的文档字段中提取的值生成矩阵结果的聚合系列。与度量和桶聚合不同,这个聚合系列还不支持脚本。
- *管道*:聚合其他聚合及其相关指标的输出的聚合
接下来是有趣的部分。由于每个存储桶有效地定义了一个文档集(属于该存储桶的所有文档),因此可以潜在地将存储桶级别的聚合关联起来,并且这些聚合将在该存储桶的上下文中执行。这就是聚合真正强大的地方:聚合可以嵌套!
分桶聚合可以有子聚合(分桶或度量)。将为它们的父聚合生成的桶计算子聚合。嵌套聚合的级别/深度没有硬性限制(可以将聚合嵌套在“父”聚合下,该聚合本身是另一个更高级别聚合的子聚合)。
聚合对double
数据的表示进行操作。因此,在绝对值大于 的 long 上运行时,结果可能是近似的2^53
。
结构化聚合
以下代码片段捕获了聚合的基本结构:
"aggregations" : {
"<aggregation_name>" : {
"<aggregation_type>" : {
<aggregation_body>
}
[,"meta" : { [<meta_data_body>] } ]?
[,"aggregations" : { [<sub_aggregation>]+ } ]?
}
[,"<aggregation_name_2>" : { ... } ]*
}
JSON 中的aggregations
对象(aggs
也可以使用键)保存要计算的聚合。每个聚合都与用户定义的逻辑名称相关联(例如,如果聚合计算平均价格,那么命名它是有意义的avg_price
)。这些逻辑名称也将用于唯一标识响应中的聚合。每个聚合都有一个特定的类型(<aggregation_type>
在上面的代码片段中),并且通常是命名聚合体中的第一个键。每种类型的聚合都定义了自己的主体,具体取决于聚合的性质(例如avg
特定字段的聚合将定义计算平均值的字段)。在聚合类型定义的同一级别,可以选择定义一组附加聚合,尽管这仅在您定义的聚合具有分桶性质时才有意义。在这种情况下,您在桶聚合级别上定义的子聚合将为桶聚合构建的所有桶计算。例如,如果您在聚合下定义了一组range
聚合,则将为定义的范围存储桶计算子聚合。
解释:
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。在 Elasticsearch 中,您有执行搜索返回 hits(命中结果),并且同时返 回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的, 您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用 一次简洁和简化的 API 来避免网络往返。
1、简单聚合
搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
- 搜索 address 中包含 mill的
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
}
}

- 搜索 address 中包含 mill 的所有人的年龄分布
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"age_agg": {
"terms": {
"field": "age"
}
}
}
}

- 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"age_agg": {
"terms": {
"field": "age"
}
},
"avg_age": {
"avg": {
"field": "age"
}
}
}
}

- 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"age_agg": {
"terms": {
"field": "age"
}
},
"avg_age": {
"avg": {
"field": "age"
}
}
},
"size": 0
}

size:0 不显示搜索数据
aggs:执行聚合。
聚合语法如下
"aggs": {
"aggs_name 这次聚合的名字,方便展示在结果集中":
{ "AGG_TYPE 聚合的类型(avg,term,terms)": {}
}
},
2、复杂聚合1
按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/account/_search
{
"query": {
"match_all": {}
},
"aggs": {
"age_avg": {
"terms": {
"field": "age",
"size": 1000
},
"aggs": {
"banlances_avg": {
"avg": {
"field": "balance"
}
}
}
}
}
,"size": 1000
}

3、复杂聚合2
查出所有年龄分布,并且这些年龄段中性别为 M 的平均薪资和 性别为F 的平均薪资以及这个年龄段的总体平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"age_agg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"balance_agg": {
"terms": {
"field": "gender.keyword",
"size": 100
},
"aggs": {
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
},
"balance_agg2": {
"avg": {
"field": "balance"
}
}
}
}
}
}

5.1.5、Elasticsearch映射
参考文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.5/mapping.html

Mapping is the process of defining how a document, and the fields it contains, are stored and indexed. For instance, use mappings to define:
- which string fields should be treated as full text fields.
- which fields contain numbers, dates, or geolocations.
- the format of date values.
- custom rules to control the mapping for dynamically added fields.
A mapping definition has:
Meta-fields are used to customize how a document’s metadata associated is treated. Examples of meta-fields include the document’s
_index
,_id
, and_source
fields.Fields or *properties*
A mapping contains a list of fields or
properties
pertinent to the document.Each field has a data
type
which can be:
- a simple type like
text
,keyword
,date
,long
,double
,boolean
orip
.- a type which supports the hierarchical nature of JSON such as
object
ornested
.- or a specialised type like
geo_point
,geo_shape
, orcompletion
.It is often useful to index the same field in different ways for different purposes. For instance, a
string
field could be indexed as atext
field for full-text search, and as akeyword
field for sorting or aggregations. Alternatively, you could index a string field with thestandard
analyzer, theenglish
analyzer, and thefrench
analyzer.This is the purpose of multi-fields. Most datatypes support multi-fields via the
fields
parameter.
映射是定义文档及其包含的字段如何存储和索引的过程。例如,使用映射来定义:
映射定义具有:
每个字段都有一个数据type
,可以是:
- 一个简单的类型,如
text
,keyword
,date
,long
,double
,boolean
或ip
. - 一种支持 JSON 分层特性的类型,例如
object
或nested
. - 或特殊类型,如
geo_point
,geo_shape
或completion
.
出于不同目的以不同方式索引同一字段通常很有用。例如,一个string
字段可以被索引为一个text
用于全文搜索的字段,也可以作为一个keyword
用于排序或聚合的字段。standard
或者,您可以使用分析器、 english
分析器和 french
分析器索引字符串字段。
这就是多领域的目的。fields
大多数数据类型通过参数支持多字段。
1、查询映射
View the mapping of an index
You can use the get mapping API to view the mapping of an existing index.
GET bank/_mapping

2、创建索引
新特性:Es7 及以上移除了 type 的概念。
关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用, 但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type 下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
- 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed, 你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段 名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
- 去掉 type 就是为了提高 ES 处理数据的效率。
数据类型参考文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.5/mapping-types.html
Core datatypes
- string:
text
andkeyword
- Numeric:
long
,integer
,short
,byte
,double
,float
,half_float
,scaled_float
- Date:
date
- Date nanoseconds:
date_nanos
- Boolean:
boolean
- Binary:
binary
- Range:
integer_range
,float_range
,long_range
,double_range
,date_range
Complex datatypes
Geo datatypes
Specialised datatypes
- IP:
ip
for IPv4 and IPv6 addresses - Completion datatype:
completion
to provide auto-complete suggestions - Token count:
token_count
to count the number of tokens in a string mapper-murmur3
:murmur3
to compute hashes of values at index-time and store them in the indexmapper-annotated-text
:annotated-text
to index text containing special markup (typically used for identifying named entities)- Percolator:Accepts queries from the query-dsl
- Join:Defines parent/child relation for documents within the same index
- Rank feature:Record numeric feature to boost hits at query time.
- Rank features:Record numeric features to boost hits at query time.
- Dense vector:Record dense vectors of float values.
- Sparse vector:Record sparse vectors of float values.
- Search-as-you-type:A text-like field optimized for queries to implement as-you-type completion
- Alias:Defines an alias to an existing field.
- Flattened:Allows an entire JSON object to be indexed as a single field.
- Shape:
shape
for arbitrary cartesian geometries.
Arrays
In Elasticsearch, arrays do not require a dedicated field datatype. Any field can contain zero or more values by default, however, all values in the array must be of the same datatype. See Arrays.
Multi-fields
It is often useful to index the same field in different ways for different purposes. For instance, a string
field could be mapped as a text
field for full-text search, and as a keyword
field for sorting or aggregations. Alternatively, you could index a text field with the standard
analyzer, the english
analyzer, and the french
analyzer.
This is the purpose of multi-fields. Most datatypes support multi-fields via the fields
parameter.
Create an index with an explicit mapping
You can use the create index API to create a new index with an explicit mapping.
PUT /my_index
{
"mappings": {
"properties": {
"age": { "type": "integer" },
"email": { "type": "keyword" },
"name": { "type": "text" }
}
}
}

3、添加新的字段映射
Add a field to an existing mapping
You can use the put mapping API to add one or more new fields to an existing index.
The following example adds
employee-id
, akeyword
field with anindex
mapping parameter value offalse
. This means values for theemployee-id
field are stored but not indexed or available for search.
PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}

4、更新映射
Update the mapping of a field
Except for supported mapping parameters, you can’t change the mapping or field type of an existing field. Changing an existing field could invalidate data that’s already indexed.
If you need to change the mapping of a field, create a new index with the correct mapping and reindex your data into that index.
Renaming a field would invalidate data already indexed under the old field name. Instead, add an
alias
field to create an alternate field name.
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移
5、数据迁移
1、创建新的索引
可以使用GET bank/_mapping
查看bank
的映射信息,把结果复制下来,输入PUT newbank
,并粘贴刚刚复制的结果,再删除粘贴过来的结果
里的"product" :{ }
(右括号随便在最后删除一个就行了)。然后点击工具
图标,选择Auto indent
格式化一下代码,再在里面修改映射信息
PUT newbank
{
"mappings" : {
"properties" : {
"account_number" : {
"type" : "long"
},
"address" : {
"type" : "text"
},
"age" : {
"type" : "integer"
},
"balance" : {
"type" : "long"
},
"city" : {
"type" : "keyword"
},
"email" : {
"type" : "keyword"
},
"employer" : {
"type" : "keyword"
},
"firstname" : {
"type" : "text"
},
"gender" : {
"type" : "keyword"
},
"lastname" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"state" : {
"type" : "keyword"
}
}
}
}

2、迁移数据
可以不用"type": "account"
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newbank"
}
}

3、查询迁移的数据
GET newbank/_search
可以看到都把数据放到"_type" : "_doc"
默认类型上了

6、分词器
1、内置分词器
Built-in analyzer reference
Elasticsearch ships with a wide range of built-in analyzers, which can be used in any index without further configuration:
The
standard
analyzer divides text into terms on word boundaries, as defined by the Unicode Text Segmentation algorithm. It removes most punctuation, lowercases terms, and supports removing stop words.The
simple
analyzer divides text into terms whenever it encounters a character which is not a letter. It lowercases all terms.The
whitespace
analyzer divides text into terms whenever it encounters any whitespace character. It does not lowercase terms.The
stop
analyzer is like thesimple
analyzer, but also supports removal of stop words.The
keyword
analyzer is a “noop” analyzer that accepts whatever text it is given and outputs the exact same text as a single term.The
pattern
analyzer uses a regular expression to split the text into terms. It supports lower-casing and stop words.Elasticsearch provides many language-specific analyzers like
english
orfrench
.The
fingerprint
analyzer is a specialist analyzer which creates a fingerprint which can be used for duplicate detection.
参考文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.5/analysis-analyzers.html

2、测试分词器
参考文档: https://www.elastic.co/guide/en/elasticsearch/reference/7.5/test-analyzer.html

英文使用默认分词器可以正常分词
POST _analyze
{
"analyzer": "whitespace",
"text": "The quick brown fox."
}

中文使用默认分词器进行分词会出现问题,每个字会分成一个词
POST _analyze
{
"analyzer": "standard",
"text": "尚硅谷电商项目"
}

3、安装ik分词器
1、下载地址
7.4.2版本的elasticsearch
下载地址为: https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip


2、下载插件
vagrant ssh
su root #使用root用户,密码默认为vagrant
docker ps #查看docker运行的容器
docker exec -it elasticsearch /bin/bash #以交互模式进入容器内部
pwd #查看当前当前路径的完整路径
ls #查看当前目录的子目录和文件
cd plugins/ #进入到plugins目录
ls #查看当前目录的子目录和文件,可以看到什么都没有
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip #下载插件
exit #退出elasticsearch容器
下载插件提示bash: wget: command not found
,这是因为容器内部非常纯净,没有这些命令

由于设置了elasticsearch容器
和外部linux虚拟机
进行了关联,因此可以在linux虚拟机
里下载插件
pwd
cd /mydata/elasticsearch/
ls
cd plugins/
wget
yum install wget #安装wget
ls
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip –no-check-certificate #添加参数 --no-check-certificate
systemctl stop firewalld #关闭防火墙
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip -no-check-certificate #重新下载插件

ls #elasticsearch-analysis-ik-7.4.2.zip
pwd #/mydata/elasticsearch/plugins
docker exec -it elasticsearch /bin/bash
pwd #/usr/share/elasticsearch
ls
cd plugins/
ls #elasticsearch-analysis-ik-7.4.2.zip
exit;

3、安装插件
ll
unzip elasticsearch-analysis-ik-7.4.2.zip -d ./ik #解压到当前目录的下的ik目录下,没有ik目录会自动创建
ll #有elasticsearch-analysis-ik-7.4.2.zip 和 ik
cd ik #进入ik目录
ll #可以看到以成功解压到ik目录下了
cd .. #退回到上级目录
rm -rf elasticsearch-analysis-ik-7.4.2.zip #删除elasticsearch-analysis-ik-7.4.2.zip压缩包
ll # ik目录的权限为 drwxr-xr-x.
chmod -R 777 ik/ #改变uk目录及子目录的权限
ll #此时ik目录的权限为 drwxrwxrwx.

docker exec -it elasticsearch /bin/bash #进入bash控制台
pwd
cd plugins/
ll
cd ../
ls
cd bin/
ls #查看可执行文件
elasticsearch-plugin #直接打出该可执行文件名,会提示一些信息
elasticsearch-plugin -h #加 -h 显示帮助信息
#如果这一步报错,删掉 elasticsearch-analysis-ik-7.4.2.zip 压缩包
elasticsearch-plugin list #列出已安装的elasticsearch插件,可以看到ik分词器已经安装成功了

4、测试
发送以下数据
POST _analyze
{
"analyzer": "ik_smart",
"text": "尚硅谷电商项目"
}
响应了错误信息,failed to find global analyzer [ik_smart]
{
"error": {
"root_cause": [
{
"type": "remote_transport_exception",
"reason": "[104b52df3ff1][127.0.0.1:9300][indices:admin/analyze[s]]"
}
],
"type": "illegal_argument_exception",
"reason": "failed to find global analyzer [ik_smart]"
},
"status": 400
}

查看正在运行的dorcker容器,可以看到elasticsearch
并没有停止运行
重启elasticsearch
,再次发送请求看看还报不报错

打开kibana
可以看到报了以下错误,这个不用管,等一会就能加载出来了
Cannot connect to the Elasticsearch cluster
See the Kibana logs for details and try reloading the page.

重新发送请求,可以看到已经可以分词了

{
"tokens" : [
{
"token" : "尚",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "硅",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "谷",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "电",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "商",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "项",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "目",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<IDEOGRAPHIC>",
"position" : 6
}
]
}
Xshell
连接linux虚拟机
4、使用1、设置可以使用root用户登录
#注意要在管理员方式修改
vi /etc/ssh/sshd_config #修改 PasswordAuthentication no 这一行为 PasswordAuthentication yes
#如果是生产环境可以先执行 service sshd reload ,不行再执行以下命令
service sshd restart


Xshell
连接
2、使用
ping
不通
3、如果ping baidu.com #ping百度
cd /etc/sysconfig/network-scripts/
ls
ip addr #查看ip配置
vi ifcfg-eth1 #编辑eth1网卡配置,内容在后面
service network restart #重启服务
ping baidu.com #再次ping百度

修改ifcfg-eth1
为以下内容
#VAGRANT-BEGIN
# The contents below are automatically generated by Vagrant. Do not modify.
NM_CONTROLLED=yes
BOOTPROTO=none
ONBOOT=yes
IPADDR=192.168.56.10
NETMASK=255.255.255.0
GATEWAY=192.168.56.1
DNS1=114.114.114.114
DNS2=8.8.8.8
DEVICE=eth1
PEERDNS=no
#VAGRANT-END

4、修改yum源
如果不能正常使用yum,可以修改yum源 如果可以正常使用就不用修改
老师提供的方法
#备份原 yum 源,如果失败了也不要紧,直接使用新 yum 源
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
#使用新 yum 源
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.163.com/.help/CentOS7-Base-163.repo
yum makecach #生成缓存
我的方法
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
如果找不到yum-config-manager
可以先安装yum-utils
yum install -y yum-utils
ik分词器
5、使用ik_smart
1、POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
响应内容为
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
}
]
}

ik_max_word
2、POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
响应内容为
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "中国",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "国人",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 4
}
]
}

7 、自定义分词
1、修改docker启动参数
docker ps #查看elasticsearch的id 也可以使用 docker ps -a 命令
systemctl stop docker.socket #停止这个服务,否则docker是没办法停止的
systemctl stop docker #停止docker
docker ps #再次查看是否有启动的docker容器
cd /var/lib/docker/containers/ #进入到该目录
ll
cd 104b52df3ff1fe34c3373deab5c2b4248accd8113ab302092124b8e33abd1936/ #进入到elasticsearch的配置目录
ll
vi config.v2.json #修改该文件

在命令模式
下输入/Env
,即可找到Env
的配置

将
"Env":["discovery.type=single-node","ES_JAVA_OPTS=-Xms64m -Xmx128m"
中的第二个-Xmx128m
修改为-Xmx512m
"Env":["discovery.type=single-node","ES_JAVA_OPTS=-Xms64m -Xmx512m"

启动docker
systemctl start docker
docker ps

方法二:删掉容器,重新运行(设置目录挂载后,linux虚拟机保存的有elasticsearch的数据)
docker stop elasticsearch
docker rm elasticsearch
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
2、安装nginx
- 随便启动一个 nginx 实例,只是为了复制出配置
cd /mydata/
pwd
mkdir nginx
ls
docker images
docker run -p 80:80 --name nginx -d nginx:1.10
docker ps
docker container cp nginx:/etc/nginx .
ls
cd nginx/
ls
docker stop nginx
docker rm nginx
cd ..
ls
mv nginx conf
ls
mkdir nginx
mv conf nginx/
ls
cd nginx/
ls

docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10

直接访问成功了

可以新建个index.html
页面,nginx
会默认展示
cd html/
ls
vi index.html

index.html
内容如下
<h1>Gulimall</h1>

刷新网页,可以看到已经访问成功了

3、创建文件
mkdir es
ls
cd es
ls
vi fenci.txt #修改的内容在下面
ls
pwd

尚硅谷
乔碧罗

通过浏览器已经访问到了,就是会乱码
http://192.168.56.10/es/fenci.txt

4、修改分词器设置
- 修改配置
cd /mydata/
ls
cd elasticsearch/
ls
cd plugins/
ls
cd ik/
cd config/
ls
vi IKAnalyzer.cfg.xml #修改的内容在下面
docker ps
docker restart elasticsearch

- 修改以下配置
将IKAnalyzer.cfg.xml
文件的以下配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
修改为:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://192.168.56.10/es/fenci.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

- 设置
nginx
开机自启
docker update nginx --restart=always

4、发送请求测试
在kibana
里发送请求
POST _analyze
{
"analyzer": "ik_max_word",
"text": "尚硅谷电商项目"
}
响应了以下内容,可以看到尚硅谷
已经变为一个词了
{
"tokens" : [
{
"token" : "尚硅谷",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "硅谷",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "电",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "商",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "项目",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
}
]
}

5.1.6、ElasticSearch整合SpringBoot
1、创建模块
1、新建模块
使用Spring Initializr
新建gulimall-search
模块
com.atguigu.gulimall
gulimall-search
ElasticSearch检索服务
com.atguigu.gulimall.search

勾选Web
里的Spring Web

点击Finish

pom
文件
2、修改删除刚刚新建的gulimall-search
模块,只保留以下内容
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-search</name>
<description>ElasticSearch检索服务</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
复制别的模块的pom
文件,粘贴到此模块,并用刚刚复制到本模块的部分替换掉复制的别的pom
文件的以上部分

GulimallSearchApplicationTests
3、修改复制别的模块的测试类,替换掉本模块的gulimall-search
的测试类
package com.atguigu.gulimall.search;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {
@Test
void contextLoads() {
}
}

2、添加依赖
elasticsearch
依赖
1、添加方法一:可以添加以下依赖
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.4.2</version>
</dependency>

方法二:也可以添加以下依赖(老师的做法)
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
添加这个依赖时,elasticsearch
的版本不对,这是因为父工程(spring-boot-starter-parent
工程)指定了版本

可以在properties
标签内添加如下配置,可以看到版本已经为7.4.2
了
<elasticsearch.version>7.4.2</elasticsearch.version>

gulimall-common
依赖
2、添加<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

nacos
3、整合在src/main/resources/application.properties
文件内添加如下配置
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search

在gulimall-search
模块的com.atguigu.gulimall.search.GulimallSearchApplication
启动类上添加如下注解
@EnableDiscoveryClient

4、添加配置类
参考文档:[Java REST Client 7.17] | Elastic
在com.atguigu.gulimall.search
包下新建config
文件夹,在config
文件夹下新建GulimallElasticSearchConfig
配置类
配置类写以下代码:
参考文档:[Initialization | Java REST Client 7.17] | Elastic
package com.atguigu.gulimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 无名氏
* @date 2022/7/5
* @Description:
*/
@Configuration
public class GulimallElasticSearchConfig {
@Bean
public RestHighLevelClient esRestClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.56.10",9200,"http")
)
);
return client;
}
}

查看官方文档:[Initialization | Java REST Client 7.17] | Elastic


RestHighLevelClient
5、测试在com.atguigu.gulimall.search.GulimallSearchApplicationTests
里对RestHighLevelClient
进行测试
package com.atguigu.gulimall.search;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {
@Autowired
RestHighLevelClient client;
@Test
public void contextLoads() {
System.out.println(client);
}
}
报了以下错误
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc
Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class at org.springframework.boot.autoconfigure.jdbc.DataSourceProperties.determineDriverClassName(DataSourceProperties.java:233)

在gulimall-search
模块的com.atguigu.gulimall.search.GulimallSearchApplication
启动类里排除数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

再次测试,就成功了

RequestOptions
6、测试public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
//builder.addHeader("Authorization", "Bearer", TOCKEN);
//builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024)
//);
COMMON_OPTIONS = builder.build();
}

查看官方文档:[Performing requests | Elasticsearch Java API Client 7.17] | Elastic

7、存储或更新数据到es
先使用kibana
发送请求查询users
,可以看到没有查到数据
GET users/_search

添加测试方法,然后运行测试方法
/**
* 存储或更新数据到es
* @throws IOException
*/
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
//数据的id
indexRequest.id("1");
//indexRequest.source("userName","zhangsan","age",18,"gender","男");
User user = new User();
String jsonString = JSON.toJSONString(user);
//要保存的内容
indexRequest.source(jsonString, XContentType.JSON);
//执行操作
IndexResponse indexResponse = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应信息
System.out.println(indexResponse);
}
@Data
class User{
private String userName;
private String gender;
private Integer age;
}

再次发送请求,可以看到已经查到数据了
GET users/_search

查看官方文档:[Index API | Java REST Client 7.17] | Elastic

完整GulimallSearchApplicationTests
类代码
package com.atguigu.gulimall.search;
import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {
@Autowired
RestHighLevelClient client;
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
//数据的id
indexRequest.id("1");
//indexRequest.source("userName","zhangsan","age",18,"gender","男");
User user = new User();
String jsonString = JSON.toJSONString(user);
//要保存的内容
indexRequest.source(jsonString, XContentType.JSON);
//执行操作
IndexResponse indexResponse = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应信息
System.out.println(indexResponse);
}
@Data
class User{
private String userName;
private String gender;
private Integer age;
}
@Test
public void contextLoads() {
System.out.println(client);
}
}
8、从es中查询数据
查看官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html

1、想要完成的请求
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg": {
"avg": {
"field": "age"
}
},
"balanceAvg": {
"avg": {
"field": "balance"
}
}
},
"size": 0
}
searchData
方法
2、添加在gulimall-search
模块的com.atguigu.gulimall.search.GulimallSearchApplicationTests
测试类中添加以下方法
@Test
public void searchData() throws IOException {
//1、创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//sourceBuilder.query();
//sourceBuilder.from();
//sourceBuilder.size();
//sourceBuilder.aggregation()
sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
sourceBuilder.aggregation(ageAgg);
AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg").field("age");
sourceBuilder.aggregation(ageAvg);
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
sourceBuilder.aggregation(balanceAvg);
sourceBuilder.size(0);
System.out.println(sourceBuilder);
searchRequest.source(sourceBuilder);
//2、执行检索
SearchResponse searchResponse = client.search(searchRequest,GulimallElasticSearchConfig.COMMON_OPTIONS);
//3、分析结果
System.out.println(searchResponse);
}

3、对比请求
1、想要完成的请求
# 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg": {
"avg": {
"field": "age"
}
},
"balanceAvg": {
"avg": {
"field": "balance"
}
}
},
"size": 0
}

2、实际发送的请求
可以看到,除了 添加了一些 默认查询条件 和 顺序 不同外其他想要的查询条件都正确
GET bank/_search
{
"size": 0,
"query": {
"match": {
"address": {
"query": "mill",
"operator": "OR",
"prefix_length": 0,
"max_expansions": 50,
"fuzzy_transpositions": true,
"lenient": false,
"zero_terms_query": "NONE",
"auto_generate_synonyms_phrase_query": true,
"boost": 1.0
}
}
},
"aggregations": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [{
"_count": "desc"
}, {
"_key": "asc"
}]
}
},
"ageAvg": {
"avg": {
"field": "age"
}
},
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}

4、响应的数据
可以看到 想要的响应 和 实际接收的响应 一样
1、想要的响应
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 38,
"doc_count" : 2
},
{
"key" : 28,
"doc_count" : 1
},
{
"key" : 32,
"doc_count" : 1
}
]
},
"ageAvg" : {
"value" : 34.0
},
"balanceAvg" : {
"value" : 25208.0
}
}
}
2、实际接收的响应
{
"took": 9,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"lterms#ageAgg": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [{
"key": 38,
"doc_count": 2
}, {
"key": 28,
"doc_count": 1
}, {
"key": 32,
"doc_count": 1
}]
},
"avg#ageAvg": {
"value": 34.0
},
"avg#balanceAvg": {
"value": 25208.0
}
}
}
5、获取响应的实体类信息
1、修改代码
修改gulimall-search
模块的com.atguigu.gulimall.search.GulimallSearchApplicationTests
测试类中searchData
方法
将sourceBuilder.size(0);
注释掉,并添加获取响应信息的代码
@Test
public void searchData() throws IOException {
//1、创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//sourceBuilder.query();
//sourceBuilder.from();
//sourceBuilder.size();
//sourceBuilder.aggregation()
sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
sourceBuilder.aggregation(ageAgg);
AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg").field("age");
sourceBuilder.aggregation(ageAvg);
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
sourceBuilder.aggregation(balanceAvg);
//sourceBuilder.size(0);
System.out.println(sourceBuilder);
searchRequest.source(sourceBuilder);
//2、执行检索
SearchResponse searchResponse = client.search(searchRequest,GulimallElasticSearchConfig.COMMON_OPTIONS);
//3、分析结果
System.out.println(searchResponse);
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
/*
{"_index" : "bank",
"_type" : "account",
"_id" : "472",
"_score" : 5.4032025,
"_source" : {}}
*/
for (SearchHit hit : searchHits) {
String string = hit.getSourceAsString();
Account account = JSON.parseObject(string, Account.class);
System.out.println("account:"+account);
}
}
@ToString
@Data
static class Account{
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}

2、测试
可以看到已经封装到Account
类里了

6、获取响应的分析数据
在gulimall-search
模块的com.atguigu.gulimall.search.GulimallSearchApplicationTests
测试类中searchData
方法里添加如下代码:
//3.2、获取这次检索的分析信息
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:"+keyAsString+"==>"+bucket.getDocCount());
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:"+balanceAvg1.getValue());
进行测试,可以发现已经打印出数据了

7、完整代码
gulimall-search
模块的com.atguigu.gulimall.search.GulimallSearchApplicationTests
测试类的完整代码
点击查看GulimallSearchApplicationTests
类完整代码
5.2、商城业务-商品上架
5.2.1、sku在es中存储模型分析
"index": false,
不可以被检索,但可以查询(可以通过skuTitle
检索查出skuImg
,但不可以通过skuImg
检索)
"doc_values": false
不可以做聚合、排序、脚本等操作,这样会省一些内存
"type": "nested",
该字段是集合,防止扁平化处理
这里的(在skuPrice
的type
不要设置为double
,在SkuEsModel
类里的skuPrice
设置的类型为BigDecimal
,Elasticsearch
不支持java
里的BigDecimal
转换为Elasticsearch
里的double
Elasticsearch
里,设置的,设置的skuPrice
的属性类型为keyword
也不行skuPrice
的属性类型为keyword
可以,我代码里没有设置skuImg
和skuPrice
所以不行)
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}

5.2.2、扁平化处理

查看官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/8.3/nested.html

nested
1、不加入PUT my_index2/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
在索引index中,存入user的数据,最终 es 会将上述数据,扁平化处理,实际存储如下这样子:
{
"group" : "fans",
"user.first" : [ "alice", "john" ],
"user.last" : [ "smith", "white" ]
}
很明显,数据存储成这样子,丢失了first 和 last 之间关系。从这样的存储中,我们无法确定,first 为 “alice” ,对应的 last 是 “smith” 还是 “white”。

执行如下查询:
GET my_index2/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
查询到的结果:
{
"took" : 6,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642,
"_source" : {
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
}
]
}
}
查询到一条数据,而这样的数据并不是我们想要的。因为通过 "first" 为 "Alice"
和 "last" 为 "Smith"
,不应该查询到数据。之所以查询到数据,是因为数组中存储的对象被扁平化处理了。

获取映射信息
GET my_index2/_mapping
{
"my_index2" : {
"mappings" : {
"properties" : {
"group" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"user" : {
"properties" : {
"first" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"last" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
}
}

nested
2、加入如果数组里存储的是对象,那么数组的类型应该是 nested,这样,再将对象数据存入数组中时,对象便不会被扁平化处理。
修改索引的mapping信息,将user数组的类型定义为 nested 并存入数据,重新执行检索。
PUT my_index3
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
PUT my_index3/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}


执行同样查询:
GET my_index3/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
获取到的结果为空:
{
"took" : 11,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
}
}
这样的结果才是我们想要的,这样防止了 frist 和 last 之间关系的丢失。

neated
增删查改
3、ES的Nested数据类型允许我们存储一对多的数据,例如一个文章可以对应多个评论等,在正式开始之前,我们先生成一个用于测试的索引:
PUT /test_article
{
"mappings": {
"test_article": {
"properties": {
"id": {
"type": "keyword"
},
"title": {
"type": "text"
},
"tags": {
"type": "text",
"analyzer": "whitespace"
},
"data": {
"type": "nested", # 注意要指定type值
"properties": {
"system_type": {
"type": "integer"
},
"affections": {
"type": "keyword"
},
"themes": {
"type": "text",
"analyzer": "whitespace"
}
} }
}
}
}
}
这是一个简化的文章表,data字段就是一个nested嵌套类型,存储不同平台(system_type)的标注数据(在一个文章内,system_type的值是唯一的),如倾向性(affections)、主题(themes)等。如果需要,nested类型是可以进行嵌套的。
然后插入一些测试数据:
POST /test_article/test_article/1
{
"id": "1",
"title": "标题1",
"tags": "tag1 tag2 tag3",
"data": []
}
POST /test_article/test_article/2
{
"id": "2",
"title": "标题2",
"tags": "tag1 tag2 tag3",
"data": [
{
"system_type": 1,
"affections": "正面",
"themes": "1 2"
},
{
"system_type": 2,
"affections": "中性",
"themes": "1"
}
]
}
POST /test_article/test_article
{
"id": "3",
"title": "标题4",
"tags": "tag1 tag3",
"data": [
{
"system_type": 1,
"affections": "中性",
"themes": "1 2"
},
{
"system_type": 2,
"affections": "负面",
"themes": "1"
}
]
}
POST /test_article/test_article
{
"id": "5",
"title": "标题5",
"tags": "tag1 tag3",
"data": [
{
"system_type": 2,
"affections": "正面",
"themes": "3 1"
}
]
}
POST /test_article/test_article
{
"id": "6",
"title": "标题6",
"tags": "tag2",
"data": []
}
01 删除数据
这是比较简单的:
POST /test_article/test_article/2/_update
{
"script": {
"source": """
ctx._source.data.removeIf(item -> item.system_type == 4)
"""
}
}
使用脚本删除满足特定条件的数据,主要就是removeIf函数,该函数的参数应该是一个匿名函数(比较接近JS的匿名函数写法,就是一个语法糖),表示成python大概是这样:
lambda item: item.system_type == 4
item就是data中的元素,removeIf会把每个item都调用该匿名函数,如果得到true值就删除该元素。
02 修改数据
修改数据应该先判断数据是否已经存在:
POST /test_article/test_article/2/_update
{
"script": {
"source": """
if (ctx._source.data != null) {
for(e in ctx._source.data) {
if (e.system_type == 2) {
e.affections = "正面";
}
}
}
"""
}
}
上面的语句会删除data数据里,system_type值为2的记录。
修改数据成功之后,数据的版本号(_version)就会加1。
03 增加数据
增加数据的时候,先判断数据是否已经存在,不存在才执行增加,如果已经存在了,则执行修改:
POST /test_article/test_article/2/_update
{
"script": {
"source": """
def is_in = false;
if (ctx._source.data == null) {
List ls = new ArrayList();
ls.add(params.article);
} else {
for(e in ctx._source.data) {
if (e.system_type == params.article.system_type) {
is_in = true;
for (String key: params.article.keySet()) {
if (key != "system_type") {
e[key] = params.article[key];
}
}
break;
}
}
if (is_in == false) {
ctx._source.data.add(params.article);
}
}
""",
"params": {
"article": {
"system_type": 3,
"affections": "负面",
"themes": "3 2"
}
},
"lang": "painless"
}
}
这里比较特别的语法是:for (String key: params.article.keySet())
找了半天才发现对象可以使用keySet方法来获取key值,类似python中的dict.keys()。
另外,脚本中有参数需要使用的时候,比较好的实现应该是通过params进行传递,而不是硬编码到脚本中。
04 查询
nested数据的查询跟普通的查询有点不一样:
GET /test_article/_search
{
"query": {
"nested": {
"path": "data",
"query": {
"term": {
"data.system_type": 1
}
}
}
}
}
使用使用nested,并指定对应的path。但是要注意,这个查询只会对外层的记录进行过滤,并不会对nested内部的数据进行过滤。例如对于"data.system_type": 1,则data字段里有一条记录满足这个条件的,这个文章就会整体返回(当然可以通过_source命令进行筛选)。
如果说只想得到命中的nested数据,则可以使用inner_hits:
GET /test_article/_search
{
"query": {
"nested": {
"path": "data",
"query": {
"bool": {
"must": [
{
"term": {
"data.system_type": {
"value": 2
}
}
}
]
}
},
"inner_hits": {} # 返回满足条件的查询
}
},
"size": 10
}
这时返回数据里就会增加一个inner_hits的字段:
{
"hits" : {
"total" : 3,
"max_score" : 1.0,
"hits" : [
{
"_index" : "test_article",
"_type" : "test_article",
"_id" : "aqjGXH4BZeFFYagKZU_i",
"_score" : 1.0,
"_source" : {
"id" : "5",
"title" : "标题5",
"tags" : "tag1 tag3",
"data" : [ # 这里可以使用_source命令进行过滤掉
{
"system_type" : 2,
"affections" : "正面",
"themes" : "3 1"
}, ......
]
},
"inner_hits" : { # 这里只会返回命中的记录
"data" : {
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "test_article",
"_type" : "test_article",
"_id" : "aqjGXH4BZeFFYagKZU_i",
"_nested" : {
"field" : "data",
"offset" : 0
},
"_score" : 1.0,
"_source" : {
"system_type" : 2,
"affections" : "正面",
"themes" : "3 1"
}
}
]
}
}
}
},
......
]
}
}
05 聚合统计
在我们的场景中,场景的一个需要是,统计某个平台(system_type)下文章的倾向性的分布情况。开始的实现是这样:
GET /test_article/_search
{
"size": 0,
"aggs": {
"positive": {
"filter": {
"nested": {
"path": "data",
"query": {
"bool": {
"must": [
{
"term": {
"data.system_type": 2
}
},
{
"term": {
"data.affections": "正面"
}
}
]
}
}
}
}
},
"negative": {
"filter": {
"nested": {
"path": "data",
"query": {
"bool": {
"must": [
{
"term": {
"data.system_type": 2
}
},
{
"term": {
"data.affections": "负面"
}
}
]
}
}
}
}
},
"neutral": {
"filter": {
"nested": {
"path": "data",
"query": {
"bool": {
"must": [
{
"term": {
"data.system_type": 2
}
},
{
"term": {
"data.affections": "中性"
}
}
]
}
}
}
}
},
"sensitive": {
"filter": {
"nested": {
"path": "data",
"query": {
"bool": {
"must": [
{
"term": {
"data.system_type": 2
}
},
{
"term": {
"data.affections": "敏感"
}
}
]
}
}
}
}
}
}
}
上面的语句是可以工作的,但是很罗嗦,差不多有100行,很多重复的代码,现在倾向性只有4个还勉强可以,如果有10个呢,那就这个语句就有两三百行。。。
于是优化成这样:
GET /test_article/_search
{
"size": 0,
"aggs": {
"name": {
"nested": {
"path": "data"
},
"aggs": {
"system_type_value": {
"terms": {
"field": "data.system_type"
},
"aggs": {
"affections_value": {
"terms": {
"field": "data.affections"
}
}
}
}
}
}
}
}
思路是先按data.system_type进行分桶,然后再按data.affections进行分桶,简洁了很多,但是这样的弊端是,我们本来只想统计某个平台下的数据,这里却会把所有平台的数据都进行统计了,浪费资源。
再优化:
GET /test_article/_search
{
"size": 0,
"aggs": {
"nested_data": {
"nested": {
"path": "data"
},
"aggs": {
"filter_data": {
"filter": {
"term": {
"data.system_type": 2
}
},
"aggs": {
"affections_value": {
"terms": {
"field": "data.affections"
}
}
}
}
}
}
}
}
聚合里有一个filter的类型,之前居然没有注意到。通过filter过滤出满足条件的数据,再对data.affections进行分桶,完美解决。
5.2.3、构造基本数据
1、查看接口
请求url: http://localhost:88/api/product/spuinfo/1/up

接口文档: https://easydoc.net/s/78237135/ZUqEdvA4/DhOtFr4A

spuUp
方法
2、添加在gulimall-product
模块的com.atguigu.gulimall.product.controller.SpuInfoController
类里添加spuUp
方法
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId) {
spuInfoService.up(spuId);
return R.ok();
}

up
抽象方法
3、添加在gulimall-product
模块的com.atguigu.gulimall.product.service.SpuInfoService
抽象接口里添加up
抽象方法
/**
* 商品上架
* @param spuId
*/
void up(Long spuId);

SkuEsModel
类
4、新建在gulimall-common
模块的com.atguigu.common.to
包下新建es
文件夹,在es
文件夹下新建SkuEsModel
类
package com.atguigu.common.to.es;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/5
* @Description:
*/
@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;
/**
* 是否还有库存
*/
private Boolean hasStock;
/**
* 热度评分
*/
private Long hotScore;
/**
* 品牌id
*/
private Long brandId;
/**
* 分类id
*/
private Long catalogId;
/**
* 品牌名
*/
private String brandName;
/**
* 品牌图片
*/
private String brandImg;
/**
* 分类名
*/
private String catalogName;
private List<Attr> attrs;
@Data
public static class Attr{
private Long attrId;
private String attrName;
private String attrValue;
}
}

up
抽象方法
5、实现up
方法
1、修改修改gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法
@Override
public void up(Long spuId) {
List<SkuEsModel> upProducts = new ArrayList<>();
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
//1、查出当前spuId对应的所有sku信息,包括品牌的名字。
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
}

getSkusBySpuId
抽象方法
2、添加在gulimall-product
模块的com.atguigu.gulimall.product.service.SkuInfoService
抽象类里添加getSkusBySpuId
抽象方法
List<SkuInfoEntity> getSkusBySpuId(Long spuId);

getSkusBySpuId
抽象方法
3、实现在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl
类里实现getSkusBySpuId
抽象方法
(这里的lambdaQueryWrapper.eq(SkuInfoEntity::getSKuId,spuId);
写错了,应该为lambdaQueryWrapper.eq(SkuInfoEntity::getSpuId,spuId);
)
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
LambdaQueryWrapper<SkuInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(SkuInfoEntity::getSKuId,spuId);
List<SkuInfoEntity> list = this.list(lambdaQueryWrapper);
return list;
}

SkuEsModel
类与SkuInfoEntity
类
4、对比SkuEsModel
类与SkuInfoEntity
类共同有的属性,但属性名不一样的数据的对比
SkuEsModel类中的属性名 | SkuInfoEntity类中的属性名 |
---|---|
skuPrice | price |
skuImg | skuDefaultImg |

SkuEsModel
类有的属性,而SkuInfoEntity
类没有的属性
hasStock //是否还有库存
hotScore //热度评分
brandName //品牌名
BrandImg //品牌图片
catalogName //分类名
attrs

up
方法
6、修改up
方法
1、修改修改gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法
@Autowired
BrandService brandService;
@Autowired
CategoryService categoryService;
@Override
public void up(Long spuId) {
//attrs
//TODO 4、查询当前sku的所有可以被用来检索的规格属性。
List<ProductAttrValueEntity> productAttrValueEntities = productAttrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = productAttrValueEntities.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
List<SkuEsModel.Attr> attrList = productAttrValueEntities.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attr attr = new SkuEsModel.Attr();
BeanUtils.copyProperties(item, attr);
return attr;
}).collect(Collectors.toList());
//1、查出当前spuId对应的所有sku信息,包括品牌的名字。
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
List<SkuEsModel> collect = skuInfoEntities.stream().map(skuInfoEntity -> {
//组装需要的数据
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(skuInfoEntity, skuEsModel);
//skuPrice
//skuImg
//hasStock 是否还有库存
//TODO 1、发送远程调用,在库存系统中查询是否有库存,并不用知道库存是多少
//hotScore 热度评分
//TODO 2、热度评分,默认为 0
skuEsModel.setHotScore(0L);
//brandName:品牌名 BrandImg:品牌图片
//TODO 3、查询品牌和分类的名字信息
BrandEntity brandEntity = brandService.getById(skuInfoEntity.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
//catalogName 分类名
CategoryEntity categoryEntity = categoryService.getById(skuInfoEntity.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
//设置检索属性
skuEsModel.setAttrs(attrList);
return skuEsModel;
}).collect(Collectors.toList());
//TODO 5、将数据发送给es进行保存
}

selectSearchAttrIds
抽象方法
2、添加在gulimall-product
模块的com.atguigu.gulimall.product.service.AttrService
抽象类里添加selectSearchAttrIds
抽象方法
/**
* 在指定的所有属性集合里面挑出检索属性
* @param attrIds
* @return
*/
List<Long> selectSearchAttrIds(List<Long> attrIds);

selectSearchAttrIds
方法
3、添加在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.AttrServiceImpl
类里添加selectSearchAttrIds
方法
@Override
public List<Long> selectSearchAttrIds(List<Long> attrIds) {
//select attr_id from `pms_attr` where attr_id in(?) and search_type = 1
return this.baseMapper.selectSearchAttrIds(attrIds);
}

selectSearchAttrIds
抽象方法
4、添加在gulimall-product
模块的com.atguigu.gulimall.product.dao.AttrDao
接口里添加selectSearchAttrIds
抽象方法
List<Long> selectSearchAttrIds(@Param("attrIds") List<Long> attrIds);

AttrDao.xml
文件
5、修改在gulimall-product
模块的src/main/resources/mapper/product/AttrDao.xml
文件里添加如下代码
<select id="selectSearchAttrIds" resultType="java.lang.Long">
select attr_id from gulimall_pms.pms_attr
<where>
attr_id in
<foreach collection="attrIds" item="attrId" separator="," open="(" close=")">
#{attrId}
</foreach>
and search_type = 1
</where>
</select>

5.2.4、远程查询库存&泛型结果封装
SkuHasStock
类
1、新建在gulimall-ware
模块的com.atguigu.gulimall.ware.vo
里新建SkuHasStock
类
package com.atguigu.gulimall.ware.vo;
import lombok.Data;
/**
* @author 无名氏
* @date 2022/7/6
* @Description:
*/
@Data
public class SkuHasStock {
private Long skuId;
private Boolean hasStock;
}

getSkuHasStock
方法
2、添加在gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类里添加getSkuHasStock
方法

getSkuHasStock
抽象方法
3、添加在gulimall-ware
模块的com.atguigu.gulimall.ware.service.WareSkuService
接口里添加getSkuHasStock
抽象方法
List<SkuHasStock> getSkuHasStock(List<Long> skuIds);

getSkuHasStock
方法
4、添加在gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类里添加getSkuHasStock
方法
@Override
public List<SkuHasStock> getSkuHasStock(List<Long> skuIds) {
List<SkuHasStock> collect = skuIds.stream().map(skuId -> {
SkuHasStock vo = new SkuHasStock();
//SELECT SUM (stock) FROM、 wms ware skui WHERE sku id=1
long count = this.baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count > 0);
return vo;
}).collect(Collectors.toList());
return collect;
}

getSkuStock
方法
5、添加在gulimall-ware
模块的com.atguigu.gulimall.ware.dao.WareSkuDao
类里添加getSkuStock
方法
long getSkuStock(Long skuId);

getSkuStock
6、添加在gulimall-ware
模块的src/main/resources/mapper/ware/WareSkuDao.xml
文件里添加getSkuStock
<!-- 库存数 = 剩余库存 - 已被锁定的件数(已生成订单但未付款,已下单等) -->
<select id="getSkuStock" resultType="java.lang.Long">
select sum(stock-stock_locked) from gulimall_wms.wms_ware_sku where sku_id=#{skuId}
</select>

SkuHasStock
7、重命名重新将SkuHasStock
重命名为SkuHasStockVo

5.2.5、调用远程库存服务
1、导入依赖
在gulimall-product
模块的pom
文件里添加如下依赖(已经添加过了)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、开启远程调用
在gulimall-product
模块的com.atguigu.gulimall.product.GulimallProductApplication
启动类上添加如下注解(已经添加过了)
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")

getSkuHasStock
抽象方法
3、添加在gulimall-product
模块的com.atguigu.gulimall.product.feign
包下新建WareFelginService
接口,在WareFelginService
接口里添加getSkuHasStock
抽象方法
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
@FeignClient("gulimall-ware")
public interface WareFelginService {
@PostMapping("/ware/waresku/hasStock")
public R getSkuHasStock(@RequestBody List<Long> skuIds);
}

4、修改RS
方法一:(不推荐)
可以在gulimall-common
模块的com.atguigu.common.utils.R
类里添加泛型,用于封装数据(但我不想用这种方式)

方法二:(推荐)
重新在gulimall-common
模块的com.atguigu.common.utils
包里新建RS
类用于后端数据传输

getSkuHasStock
方法
5、修改修改gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法
/**
* 查询sku是否有库存
* @return
*/
@PostMapping("/hasStock")
public RS<List<SkuHasStockVo>> getSkuHasStock(@RequestBody List<Long> skuIds){
List<SkuHasStockVo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
return new RS<>(skuHasStocks);
}

SkuHasStockVo
类
6、移动并重命名把gulimall-ware
模块的com.atguigu.gulimall.ware.vo.SkuHasStockVo
移动到gulimall-common
模块的com.atguigu.common.to
类下,并将SkuHasStockVo
重命名为SkuHasStockTo

getSkuHasStock
抽象方法
7、修改修改gulimall-product
模块的com.atguigu.gulimall.product.feign.WareFelginService
接口的getSkuHasStock
抽象方法
(这里的@PostMapping("/hasStock")
写错了,应该为@PostMapping("/ware/waresku/hasStock")
@PostMapping("/hasStock")
public RS<List<SkuHasStockTo>> getSkuHasStock(List<Long> skuIds);

up
方法
8、修改修改gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法
@Override
public void up(Long spuId) {
//1、查出当前spuId对应的所有sku信息,包括品牌的名字。
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
//attrs
//TODO 4、查询当前sku的所有可以被用来检索的规格属性。
List<ProductAttrValueEntity> productAttrValueEntities = productAttrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = productAttrValueEntities.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
List<SkuEsModel.Attr> attrList = productAttrValueEntities.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attr attr = new SkuEsModel.Attr();
BeanUtils.copyProperties(item, attr);
return attr;
}).collect(Collectors.toList());
//TODO 1、发送远程调用,在库存系统中查询是否有库存,并不用知道库存是多少
//hotScore 热度评分
Map<Long, Boolean> isSkuStock = null;
try {
List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
RS<List<SkuHasStockTo>> skuHasStock = wareFelginService.getSkuHasStock(skuIdList);
isSkuStock = skuHasStock.getData().stream()
.collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}
Map<Long, Boolean> finalIsSkuStock = isSkuStock;
List<SkuEsModel> collect = skuInfoEntities.stream().map(skuInfoEntity -> {
//组装需要的数据
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(skuInfoEntity, skuEsModel);
//TODO 1、发送远程调用,在库存系统中查询是否有库存,并不用知道库存是多少
//hotScore 热度评分
boolean hasStock = false;
//设置库存信息
//如果远程调用失败,则默认有库存
if (finalIsSkuStock ==null || !finalIsSkuStock.containsKey(skuInfoEntity.getSkuId())){
skuEsModel.setHasStock(true);
}else {
skuEsModel.setHasStock(finalIsSkuStock.get(skuInfoEntity.getSkuId()));
}
//skuPrice
//skuImg
//hasStock 是否还有库存
//TODO 2、热度评分,默认为 0
skuEsModel.setHotScore(0L);
//brandName:品牌名 BrandImg:品牌图片
//TODO 3、查询品牌和分类的名字信息
BrandEntity brandEntity = brandService.getById(skuInfoEntity.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
//catalogName 分类名
CategoryEntity categoryEntity = categoryService.getById(skuInfoEntity.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
//设置检索属性
skuEsModel.setAttrs(attrList);
return skuEsModel;
}).collect(Collectors.toList());
//TODO 5、将数据发送给es进行保存
}

5.2.6、使用ES远程上架
ElasticSaveController
类
1、新建在gulimall-search
模块的com.atguigu.gulimall.search
包下新建controller
文件夹,在controller
文件夹下新建ElasticSaveController
类,在ElasticSaveController
里新建productStatusUp
方法
package com.atguigu.gulimall.search.controller;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
@RestController
@RequestMapping("/search")
public class ElasticSaveController {
/**
* 上架商品
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
return R.ok();
}
}

ProductSaveService
类
2、新建在gulimall-search
模块的com.atguigu.gulimall.search
包下新建service
文件夹,在service
文件夹下新建ProductSaveService
类
package com.atguigu.gulimall.search.service;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
public interface ProductSaveService {
}

productStatusUp
方法
3、修改修改gulimall-search
模块的com.atguigu.gulimall.search.controller.ElasticSaveController
类的productStatusUp
方法
@Autowired
ProductSaveService productSaveService;
/**
* 上架商品
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
productSaveService.productStatusUp(skuEsModels);
return R.ok();
}

productStatusUp
抽象方法
4、添加在gulimall-search
模块的com.atguigu.gulimall.search.service.ProductSaveService
接口里添加productStatusUp
抽象方法
void productStatusUp(List<SkuEsModel> skuEsModels);

EsConstant
常量类
5、新建在gulimall-search
模块的com.atguigu.gulimall.search
包里新建constant
文件夹,在constant
文件夹里新建EsConstant
常量类
package com.atguigu.gulimall.search.constant;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
public class EsConstant {
/**
* sku数据在es中的索引
*/
public static final String PRODUCT_INDEX = "product";
}

ProductSaveServiceImpl
类
6、新建1、向上抛出异常
在gulimall-search
模块的com.atguigu.gulimall.search.service
包里新建impl
文件夹,在impl
文件夹里新建ProductSaveServiceImpl
类,实现ProductSaveService
接口
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.ProductSaveService;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public void productStatusUp(List<SkuEsModel> skuEsModels) {
//保存到es
//1、给es中建立索引,product,建立好映射关系。
//2、给es中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
String json = JSON.toJSONString(skuEsModel);
indexRequest.source(json, XContentType.JSON);
bulkRequest.add(indexRequest);
}
restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
}
}

2、修改返回类型
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public void productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1、给es中建立索引,product,建立好映射关系。(kibana里已经建立过了)
//2、给es中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
String json = JSON.toJSONString(skuEsModel);
indexRequest.source(json, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 如果批量操作有错误
boolean b = bulk.hasFailures();
if (b){
List<String> collect = Arrays.stream(bulk.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());
log.error("商品上架错误:{}",collect);
}
}
}

3、完整代码
点击查看ProductSaveServiceImpl
类完整代码

严重性
级别(解决restHighLevelClient
报红)
7、修改依次点击File
-> Settings
-> Editor
-> Inspections
-> Spring
-> Spring Core
-> Autowiring for bean class
在右侧 Severity
里选择 Warining

BizCodeException
枚举类里添加实例
8、在在gulimall-common
模块的com.atguigu.common.exception.BizCodeException
枚举类里添加实例
/**
* 商品上架异常(向ElasticSearch里保存数据出错)
*/
PRODUCT_UP_EXCEPTION(11000,"商品上架异常");

完整代码:
package com.atguigu.common.exception;
/**
* @author 无名氏
* @date 2022/5/7
* @Description:
* 错误码和错误信息定义类
* 1. 错误码定义规则为 5 为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeException {
/**
* 系统未知异常
*/
UNKNOW_EXCEPTION(10000,"系统未知异常"),
/**
* 参数格式校验失败
*/
VALID_EXCEPTION(10001,"参数格式校验失败"),
/**
* 商品上架异常(向ElasticSearch里保存数据出错)
*/
PRODUCT_UP_EXCEPTION(11000,"商品上架异常");
private int code;
private String msg;
BizCodeException(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
productStatusUp
方法
9、修改在gulimall-search
模块的com.atguigu.gulimall.search.controller.ElasticSaveController
类里修改productStatusUp
方法
package com.atguigu.gulimall.search.controller;
import com.atguigu.common.exception.BizCodeException;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
@Slf4j
@RestController
@RequestMapping("/search")
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
/**
* 上架商品
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
}catch (Exception e){
log.error("ElasticSaveController商品上架错误:{}",e);
return R.error(BizCodeException.PRODUCT_UP_EXCEPTION);
}
if (b){
return R.ok();
}else {
return R.error(BizCodeException.PRODUCT_UP_EXCEPTION);
}
}
}

@RequestMapping
注解
10、修改在gulimall-search
模块的com.atguigu.gulimall.search.controller.ElasticSaveController
类上修改@RequestMapping("/search")
注解
@RequestMapping("/search/save")

productStatusUp
方法的返回值
11、修改修改gulimall-search
模块的com.atguigu.gulimall.search.service.impl.ProductSaveServiceImpl
类的productStatusUp
方法的返回值,bulk.hasFailures()
方法有错误了才返回true
,而productStatusUp
方法没有错误了才返回true
,所以返回值应取反
点击查看ProductSaveServiceImpl
类完整代码

SearchFeignService
类
12、新建在gulimall-product
模块的com.atguigu.gulimall.product.feign
包里新建SearchFeignService
类,在SearchFeignService
类里添加productStatusUp
方法
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/7
* @Description:
*/
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
public R productStatusUp(List<SkuEsModel> skuEsModels);
}

StatusEnum
枚举类
13、新建在gulimall-common
模块的com.atguigu.common.constant.product
包里新建StatusEnum
枚举类
package com.atguigu.common.constant.product;
/**
* @author 无名氏
* @date 2022/5/17
* @Description: spu的状态
*/
public enum StatusEnum {
/**
* 新建商品
*/
NEW_SPU(0,"新建商品"),
/**
* 上架商品
*/
SPU_UP(1,"上架商品"),
/**
* 下架商品
*/
SPU_DOWN(2,"下架商品");
private int code;
private String msg;
StatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

up
方法
14、修改修改gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法
//TODO 5、将数据发送给es进行保存
R r = searchFeignService.productStatusUp(collect);
if (r.getCode()==0){
//远程调用成功
//TODO 6、修改当前spu的状态(变为上架状态)
this.baseMapper.updateSpuStatus(spuId, StatusEnum.SPU_UP.getCode());
}else {
//远程调用失败
}

updateSpuStatus
15、添加在gulimall-product
模块的src/main/resources/mapper/product/SpuInfoDao.xml
文件里添加updateSpuStatus
<update id="updateSpuStatus">
update gulimall_pms.pms_spu_info set publish_status=#{code},update_time = now() where id = #{spuId}
</update>

SpuInfoServiceImpl
类代码
16、完整
17、设置端口
在gulimall-search
模块的src/main/resources
文件夹里新建application.yml
文件,在application.yml
配置文件里添加配置
server:
port: 12000
spring:
application:
name: gulimall-search

Product
服务
5.2.7、测试(1)1、准备工作
GulimallSearchApplication
服务到Unnamed
1、添加添加GulimallSearchApplication
服务到Unnamed
,并指定GulimallSearchApplication
服务的最大占用为100m
-Xmx100m

2、启动各个模块
启动gulimall-coupon
、gulimall-gateway
、gulimall-member
、gulimall-product
、gulimall-third-party
、 gulimall-ware
、renren-fast
、gulimall-search
模块
其中gulimall-product
、 gulimall-ware
、gulimall-search
模块以debug
方式启动

SpuInfoServiceImpl
类的up
方法里打断点
3、在在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法有效的第一行List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
上打断点

ElasticSaveController
类的productStatusUp
方法里打断点
4、在在gulimall-search
模块的com.atguigu.gulimall.search.controller.ElasticSaveController
类的productStatusUp
方法有效的第一行boolean b = false;
上打断点

WareSkuController
类的getSkuHasStock
方法里打断点
5、在在gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法有效的第一行List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
上打断点

2、进行测试
1、点击上架
运行前端项目,然后在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架

Step Over
2、点击切换到IDEA
,点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
可以看到List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
这一行只查出了一行数据,应该是有8行数据的

可以看到,gulimall_pms
数据库的pms_sku_info
表中的spu_id
为1
的有8条数据

getSkusBySpuId
方法
3、修改修改gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl
类的getSkusBySpuId
方法
这里错把SkuInfoEntity::getSpuId
写成SkuInfoEntity::getSkuId
了,改过来就行了
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
LambdaQueryWrapper<SkuInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(SkuInfoEntity::getSpuId,spuId);
List<SkuInfoEntity> list = this.list(lambdaQueryWrapper);
return list;
}

3、重新测试
product
服务
1、重启重新以debug方式启动GulimallProductApplication
服务

上架
2、再次点击刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架

spuId
查询所有sku
3、根据切换到IDEA
,点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
根据spuId
查询所有sku
数据(共8条),还差品牌的名字没有查出来。

与gulimall_pms
数据库的pms_sku_info
表中的spu_id
为1
的8条数据相同

4、查出所有的规格属性
点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
List<ProductAttrValueEntity> productAttrValueEntities = productAttrValueService.baseAttrlistforspu(spuId);
根据spuId
查询出该该spu
的所有基础属性

attrId
集合
5、获取点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
List<Long> attrIds = productAttrValueEntities.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
根据ProductAttrValueEntity
集合获取attrId
集合

6、筛选出可检索规格属性
点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
根据attrIds
(当前spuId
的所有规格属性的集合)过滤出所有可以被用来检索的规格属性
可以看到查询到了一条attrId
为1
的一条数据

与gulimall_pms
数据库的pms_attr
表的指定attr_id
集合的search_type
为1
的数据一致

List<Long>
转为Set<Long>
7、把点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
Set<Long> idSet = new HashSet<>(searchAttrIds);
把查出的可检索的规格属性放到了HashSet
集合里,方便查询(这样查询的效率为O(1)
)

8、打断点
在SkuEsModel.Attr attr = new SkuEsModel.Attr();
这里打个断点

Step Over
9、点击三次点击GulimallProductApplication
服务的Step Over
(步过)按钮三次,执行当前方法的下三个语句,跳到SkuEsModel.Attr attr = new SkuEsModel.Attr();
这里,可以看到当前遍历的元素就是一个规格属性

Step Over
10、点击两次点击GulimallProductApplication
服务的Step Over
(步过)按钮两次,执行当前方法的下两个语句,跳到return attr;
语句
这时已经把attrId
、attrName
、attrValue
封装到gulimall-common
模块的com.atguigu.common.to.es.SkuEsModel.Attr
内部类里了

stream
11、执行完该一直点击GulimallProductApplication
项目的Step Over
(步过)按钮,直到执行完该stream
,准备执行Map<Long, Boolean> isSkuStock = null;
语句
这时已经处理完所有attrId
,并把他们放到attrList
集合里了

12、打断点
在Map<Long, Boolean> finalIsSkuStock = isSkuStock;
这里打个断点

ware
模块
13、准备调用点击GulimallProductApplication
服务的Step Over
(步过)按钮两次,执行当前方法的下两个语句,跳到RS<List<SkuHasStockTo>> skuHasStock = wareFelginService.getSkuHasStock(skuIdList);
这里
准备远程调用GulimallWareApplication
服务,在库存系统里查询这些sku
是否还有库存

Ware
服务
5.2.8、测试(2)ware
模块
1、准备转到点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句,准备跳转到GulimallWareApplication
服务
ware
模块
1、没有跳转到并没有按预想的跳转到GulimallWareApplication
服务,而是跳转到了异常里面,可以看到是没有找到WareFelginService
模块
detailMessage = "status 404 reading WareFelginService#getSkuHasStock(List)"
详细信息 = "状态 404 读取 WareFelginService#getSkuHasStock(List)"

2、修改注解
修改gulimall-product
模块的com.atguigu.gulimall.product.feign.WareFelginService
类的getSkuHasStock
方法
把@PostMapping("/hasStock")
注解修改成@PostMapping("/ware/waresku/hasStock")
@PostMapping("/ware/waresku/hasStock")

2、再次测试
再次以debug方式启动GulimallProductApplication
服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
ware
模块
1、跳转到这次就成功跳转到GulimallWareApplication
服务了
就来到了gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
这条语句

2、跳转到异常了
点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
这次就到了一个异常类里,很明显这行代码出错了

3、试图从具有原始返回类型(long)的方法返回 null
点击Pause Program
按钮,执行完这些异常处理类
可以看到ibatis
报错,这应该就是mybatis
相关的错误了
在gulimall-ware
模块的com.atguigu.gulimall.ware.dao.WareSkuDao.getSkuStock
方法应该返回long
类型,而实际返回null
org.apache.ibatis.binding.BindingException: Mapper method 'com.atguigu.gulimall.ware.dao.WareSkuDao.getSkuStock attempted to return null from a method with a primitive return type (long).
org.apache.ibatis.binding.BindingException:映射器方法 'com.atguigu.gulimall.ware.dao.WareSkuDao.getSkuStock 试图从具有原始返回类型(long)的方法返回 null。

是gulimall-ware
模块的src/main/resources/mapper/ware/WareSkuDao.xml
文件的如下代码报错
<select id="getSkuStock" resultType="java.lang.Long">
select sum(stock-stock_locked) from gulimall_wms.wms_ware_sku where sku_id=#{skuId}
</select>

查看gulimall_wms
数据库的wms_ware_sku
表,可以看到这里没有skuId
为5
的库存信息

执行如下sql
语句,由于查不到skuId
为5
的库存信息,所以就返回了null
,而long
基本类型不能接收null
select sum(stock-stock_locked) from gulimall_wms.wms_ware_sku where sku_id=5

4、换成包装类型
将gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类的getSkuHasStock
方法的long count = this.baseMapper.getSkuStock(skuId);
替换为Long count = this.baseMapper.getSkuStock(skuId);
@Override
public List<SkuHasStockTo> getSkuHasStock(List<Long> skuIds) {
List<SkuHasStockTo> collect = skuIds.stream().map(skuId -> {
SkuHasStockTo vo = new SkuHasStockTo();
//SELECT SUM (stock) FROM、 wms ware skui WHERE sku id=1
Long count = this.baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count != null && count > 0);
return vo;
}).collect(Collectors.toList());
return collect;
}

将gulimall-ware
模块的com.atguigu.gulimall.ware.dao.WareSkuDao
接口的getSkuStock
抽象方法的long getSkuStock(Long skuId);
替换为Long getSkuStock(Long skuId);

5、超时异常
这时如果点击GulimallProductApplication
服务,这时 报的是超时异常而不是原本的异常
detailMessage = "Read timed out executing POST http://gulimall-ware/ware/waresku/hasStock"

3、再次测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
1、打断点
取消gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法的List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
前面的断点,并在List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
这里打一个断点

Resume Program
2、点击点击Resume Program
或按F9
,执行到List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
这个断点

Step Over
(步过)按钮
3、点击点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
把SkuInfoEntity
集合的skuId
收集起来,准备发送给远程的GulimallWareApplication
服务

gulimall-ware
模块
4、跳转到点击GulimallProductApplication
服务的Step Over
(步过)按钮,就来到了gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
这条语句

5、又跳转到异常了
点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句,又跳转到异常了

6、空指针异常
再次点点击Resume Program
或按F9
放行这些异常处理类,可以看到报了空指针异常
java.lang.NullPointerException: null
at com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl.lambda$getSkuHasStock$0(WareSkuServiceImpl.java:114)

getSkuHasStock
方法
7、修改在gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类的getSkuHasStock
方法里
vo.setHasStock(count > 0);
这条语句,由于刚刚没有查询到库存信息,所以null > 0
由于null
不能和0
相比,所以就报异常了
修改vo.setHasStock(count > 0);
为vo.setHasStock(count != null && count > 0);
即可

4、重新测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
getSkuHasStock
方法
1、运行到不断点击Resume Program
和Step Over
按钮,一直运行到gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
这里

2、打两个断点
在gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类的getSkuHasStock
方法的List<SkuHasStockTo> collect = skuIds.stream().map(skuId -> {
和vo.setHasStock(count != null && count > 0);
这里共打两个断点

Step Over
按钮
3、点击点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
可以看到,已经运行到gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类的getSkuHasStock
方法的List<SkuHasStockTo> collect = skuIds.stream().map(skuId -> {
这里了
skuIds
的数据也正确传过来了

Resume Program
4、点击点击Resume Program
或按F9
,执行到vo.setHasStock(count != null && count > 0);
这里
此时vo
的skuId
已赋值进去了,hasStock
还为null
(这里的vo
其实应该改名为to
)

Step Over
按钮
5、点击点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
这时vo
的hasStock
已经正确赋值了

Resume Program
6、点击点击Resume Program
或按F9
,执行到skuId
为2
的SkuHasStockTo vo = new SkuHasStockTo();
这里
再次点击Resume Program
或按F9
,执行到skuId
为2
的vo.setHasStock(count != null && count > 0);
这里

Step Over
按钮
7、点击点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
这时skuId
为2
的vo
的hasStock
已经正确赋值了

8、取消断点并打新断点
取消vo.setHasStock(count != null && count > 0);
上的断点,并在return vo;
上打断点

Resume Program
9、点击点击Resume Program
或按F9
,执行到skuId
为3
的return vo;
这里
这时skuId
为3
的vo
的skuId
和hasStock
已经正确赋值了

Resume Program
10、点击点击Resume Program
或按F9
,执行到skuId
为4
的return vo;
这里
这时skuId
为4
的vo
的skuId
和hasStock
已经正确赋值了

Resume Program
11、点击点击Resume Program
或按F9
,执行到skuId
为4
的return vo;
这里
这时skuId
为4
的vo
的skuId
和hasStock
已经正确赋值了,此时的hasStock
的值为false

WareSkuServiceImpl
类的断点
12、取消取消gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类的getSkuHasStock
方法的return vo;
这个断点

WareSkuController
类打断点
13、在在gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的return new RS<>(skuHasStocks);
这里打个断点

14、对比数据
点击Resume Program
或按F9
,执行到skuId
为4
的return new RS<>(skuHasStocks);
这里
可以看到skuHasStocks
对象的elementData
里已经封装了这些数据

这些数据与gulimall_wms
数据库的wms_ware_sku
表里数据一致

Resume Program
15、点击点击Resume Program
或按F9
,放行gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法,准备跳转到gulimall-product
模块

5.2.9、测试(3)
1、准备工作
1、超时异常
点击Services
里的GulimallProductApplication
服务,这时已经报了超时异常
这是因为刚刚调试用了很多时间,所以出现了超时异常
detailMessage ="Read timed out executing POST http://gulimall-ware/ware/waresku/hasStock"

ware
模块的所有断点
2、去掉去掉gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法上的全部断点

去掉gulimall-ware
模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl
类的getSkuHasStock
方法的所有断点

2、再次测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
1、准备远程调用
这次直接来到了gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法的List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
上

isSkuStock
还是为null
2、点击Resume Program
或按F9
,执行完远程调用,来到Map<Long, Boolean> finalIsSkuStock = isSkuStock;
这里,可以看到isSkuStock
还是为null
,很明显写的还是有问题,这里先不管,继续向后执行
在SkuEsModel skuEsModel = new SkuEsModel();
上打个断点

3、打个断点
点击Resume Program
或按F9
,来到SkuEsModel skuEsModel = new SkuEsModel();
这里
在return skuEsModel;
这行打个断点(不要点击放行)

Step Over
(步过)按钮
4、点击两次点击GulimallProductApplication
服务的Step Over
(步过)按钮两次,执行当前方法的下两个语句
来到boolean hasStock = false;
这里,可以看到skuInfoEntity
里与skuEsModel
相同名称的属性已经拷到skuEsModel
里了

这与gulimall_pms
数据库的pms_sku_info
表的sku_id
为1
的数据一致

finalIsSkuStock
也为null
5、点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
由于isSkuStock
为null
,所以finalIsSkuStock
也为null
,因此if (finalIsSkuStock ==null || !finalIsSkuStock.containsKey(skuInfoEntity.getSkuId())){
的第一的条件成立,默认被设置成了有库存

hasStock
属性已修改为true
6、一直点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的后面语句
执行到skuEsModel.setHotScore(0L);
这里可以看到,skuEsModel
的hasStock
属性已修改为true

Step Over
7、点击一直点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的后面语句
执行到CategoryEntity categoryEntity = categoryService.getById(skuInfoEntity.getCatalogId());
这里
可以看到BrandEntity brandEntity = brandService.getById(skuInfoEntity.getBrandId());
这行语句已经查询出数据了,到这里已经正确把brandEntity
对象的name
属性和logo
属性赋值给skuEsModel
的brandName
属性和brandImg
属性

Resume Program
8、点击点击Resume Program
或按F9
,执行到return skuEsModel;
这里
可以看到skuEsModel
对象的catalogName
属性和attrs
属性也正确赋值了

9、重新打断点
取消SkuEsModel skuEsModel = new SkuEsModel();
这个断点

取消return skuEsModel;
这个断点,并在R r = searchFeignService.productStatusUp(collect);
这里打上断点

Resume Program
10、点击点击Resume Program
或按F9
,执行到R r = searchFeignService.productStatusUp(collect);
这里
可以看到已经把SkuEsModel
集合收集到collect
里了

feign
源码
3、进入Step Into
1、点击点击Step Into
或按 F7
,进入R r = searchFeignService.productStatusUp(collect);
方法内部,查看feign
的调用过程

2、判断方法名称
在feign-core-10.2.3
jar包的feign.ReflectiveFeign
类里的invoke
方法里面
首先判断这个方法是不是equals
、 hashCode
、toString
,如果是这些方法直接调用本类(feign.ReflectiveFeign
类)的这些方法

传入的Method
方法的完整内容如下所示,可以看到name
属性的值为productStatusUp
,并不是这些方法

3、准备跳到同步方法处理器
一直点击Step Over
按钮步过,到return "toString".equals(method.getName()) ? this.toString() : ((MethodHandler)this.dispatch.get(method)).invoke(args);
这里

4、跳到同步方法处理器
点击Step Into
或按 F7
,进入((MethodHandler)this.dispatch.get(method)).invoke(args);
里面
这是跳到了SynchronousMethodHandler
类的内部(同步方法处理器)
传入的Object[] argv
参数的argv[0]
的数据为远程调用的collect

5、构造请求模块
在feign-core-10.2.3
jar包的feign.SynchronousMethodHandler
类的invoke
方法里的
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
这里构造了一个请求模板,用于封装发送请求的信息

template
对象属性
6、点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句,跳到Retryer retryer = this.retryer.clone();
这里
template
对象含有以下属性
uriTemplate = "/search/save/product"
指出要请求的路径
method = "POST"
指出请求方式为POST
charset = "UTF-8"
设置字符编码为UTF-8

template
对象的data
字段
7、查看点击template
-> body
-> data
右键选择View as
-> String
,即可查看template
对象的body
属性的data
字段的数据
可以发现feign
在底层会将数据转成json

点击data
属性右侧的...View
,即可查看详细json
,以下是这个json
的详细数据

8、拿到重试器
Retryer retryer = this.retryer.clone();
这里拿到了一个重试器

9、准备执行和解码
点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
return this.executeAndDecode(template);
执行和解码
这行代码会远程执行请求,然后将响应拿过来,将响应数据解码

Step Into
10、点击点击Step Into
或按 F7
,进入return this.executeAndDecode(template);
这个调用的方法内部
Request request = this.targetRequest(template);
使用刚刚创建的template
构造一个目标请求

11、有日志时会记录日志
点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
if (this.logLevel != Level.NONE) {
这里如果有日志的话,也会记录日志

Step Over
(步过)按钮
12、点击点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
long start = System.nanoTime();
获取开始时间
返回正在运行的Java虚拟机的高分辨率时间源的当前值,以纳秒为单位。
此方法只能用于测量经过的时间,并且与系统或挂钟时间的任何其他概念无关。返回的值表示纳秒,因为某些固定的任意原点时间 (可能在将来,因此值可能为负)。
在Java虚拟机的实例中,所有对此方法的调用都使用相同的原点;其他虚拟机实例可能使用不同的来源。 此方法提供纳秒级精度,但不一定是纳秒级分辨率(即,值的变化频率) - 除了分辨率至少与currentTimeMillis()的分辨率一样好之外,不做任何保证。
连续调用的差异超过大约292年(2^63纳秒)将无法正确计算由于数值溢出而导致的经过时间。 只有在计算在Java虚拟机的同一实例中获得的两个此类值之间的差异时,此方法返回的值才有意义。
例如,要测量某些代码执行所需的时间:
long startTime = System.nanoTime();
// ...正在测量的代码......
long estimatedTime = System.nanoTime() - startTime;
比较两个nanoTime值
long t0 = System.nanoTime(); ... long t1 = System.nanoTime();
一个应该使用t1 - t0 <0,而不是t1 <t0,因为数字溢出的可能性.
Returns:正在运行的Java虚拟机的高分辨率时间源的当前值,以纳秒为单位
从以下版本开始:1.5

feign
客户端
13、负载均衡的点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
response = this.client.execute(request, this.options);
这里就是正真的执行
this.client
为LoadBalancerFeignClient
负载均衡的feign
客户端,会正真负载均衡的执行这个请求,该调用哪个服务就调用那个服务

14、发送http请求
这个执行请求的过程就是发送http请求的过程,这里就不看了

search
项目
4、来到ElasticSaveController
类
1、进入直接点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句,由于response = this.client.execute(request, this.options);
方法会调用GulimallSearchApplication
服务
所以就来到了gulimall-search
模块的com.atguigu.gulimall.search.controller.ElasticSaveController
类的productStatusUp
方法

可以看到skuEsModels
已经封装好了数据,这里封装的数据得益于Spring MVC
而发送htpp
请求时发送的数据得益于felgn

2、打两个断点
在gulimall-search
模块的com.atguigu.gulimall.search.service.impl.ProductSaveServiceImpl
类的productStatusUp
方法的BulkRequest bulkRequest = new BulkRequest();
和BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
各打一个断点,共打两个断点

Step Over
(步过)按钮
3、点击点击GulimallSearchApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
b = productSaveService.productStatusUp(skuEsModels);
这里准备调用ProductSaveServiceImpl
类的productStatusUp
方法

Step Into
4、点击点击Step Into
或按 F7
,进入b = productSaveService.productStatusUp(skuEsModels);
调用的方法内部
然后就进入ProductSaveServiceImpl
类的productStatusUp
方法,这里比较简单就不看了

productStatusUp
方法里打断点
5、在gulimall-search
模块的com.atguigu.gulimall.search.controller.ElasticSaveController
类的productStatusUp
方法的if (b){
这里打个断点

Resume Program
6、点击点击Resume Program
或按F9
,执行到if (b){
这里,可以看到此时的b
的值为true
,证明执行成功了

Read timed out
7、点击Resume Program
或按F9
,切换到GulimallProductApplication
服务,点击控制台就可以看到抛出了读取超时异常
Caused by: java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_301]

8、总结
Feign调用流程
1、构造请求数据,将对象转为json
RequestTemplate template = buildTemplateFromArgs.create(argv);
2、发送请求进行执行(执行成功会解码响应数据)
executeAndDecode(template);
3、执行请求会有重试机制
while(true){
try{
executeAndDecode(template);
}catch(){
try{retryer.continueOrPropagate(e);}catch(){throw ex;}
continue;
}
}

5.2.10、解决bug
1、准备工作
1、重新打断点

随便右击一个断点,也可以来到断点管理界面

上架
2、点击刷新前端项目,然后在商品系统/商品维护/spu管理
里的id为2
的商品里点击右侧操作
里的上架

2、开始测试
IDEA
1、切换到切换到IDEA
,来到gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);

Resume Program
2、点击点击Resume Program
或按F9
,来到RS<List<SkuHasStockTo>> skuHasStock = wareFelginService.getSkuHasStock(skuIdList);
这里

Step Over
(步过)按钮
3、点击点击GulimallSearchApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
可以看到skuHasStock
的size = 0

可以看到skuHasStock
对象的值为null

4、打断点
在gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的return new RS<>(skuHasStocks);
这里打上断点

3、重新测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
Resume Program
1、点击点击Resume Program
或按F9
,来到RS<List<SkuHasStockTo>> skuHasStock = wareFelginService.getSkuHasStock(skuIdList);
这里

Step Over
(步过)按钮
2、点击点击GulimallProductApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
来到gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的return new RS<>(skuHasStocks);
这里
可以看到skuHasStocks
已经正确查询出数据

Step Into
3、点击点击Step Into
或按 F7
,进入 return new RS<>(skuHasStocks);
调用的方法内部

RS
类
4、来到可以看到来到了gulimall-common
模块的com.atguigu.common.utils.RS
类的public R2(T t) {
这里

Step Over
(步过)按钮
5、点击点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句,可以看到this.setData(data);
这里写错了,应该先调用ok()
方法,不过这也不影响结果,只是不符合规范,也不会导致获取不到data
public R2(T t) {
this.ok();
this.setData(data);
}

Step Over
(步过)按钮
6、点击点击GulimallWareApplication
服务的Step Over
(步过)按钮,执行当前方法的下一个语句
执行完public R2(T t) {
这个构造器

可以看到此时该对象的size = 0

这里没用是因为Jackson对于HashMap类型会有特殊的处理方式,具体来说就是会对类进行向上转型为Map,导致子类的私有属性消失,IDEA以debug
方式也看不到这些属性的信息
父类实现序列化,子类就不需要实现序列化(也相当于实现了序列化),这里存不进去值是因为R的保存属性是一个复杂类型,这个类型(SkuHasStockTo)没有实现序列化接口
类继承于map或者list或者set后,在其他对于这个类的操作时需要特别注意,比如,使用fastjson等工具类进行转换成json时将不会转换map、list、set之外的属性。举例,A类继承list后,又在A类中添加了一个name属性,如果将A类转换成json将不会包含A类的属性,就是json中不包含name属性,只会调用list的中的比如迭代器进行遍历list的方式查询list中的数据。如果A类是自己set属性后自己get属性,那么没有影响。

7、修改返回的对象
方法一:不继承HashMap(推荐)
package com.atguigu.common.utils;
import lombok.Data;
@Data
public class RS<T> {
private static final long serialVersionUID = 1L;
private int code;
private String msg;
private boolean success;
private T data;
public RS<T> ok(String msg) {
return this.ok(0,msg);
}
public RS<T> ok() {
return this.ok(0,"success");
}
public RS<T> ok(int code, String msg) {
this.code = code;
this.msg = msg;
this.success = true;
return this;
}
public RS<T> ok(T t) {
this.data = t;
return this;
}
public RS<T> setData(T data) {
this.data = data;
return this;
}
public T getData() {
return data;
}
public RS<T> error(int code, String msg) {
this.code = code;
this.msg = msg;
this.success = false;
return this;
}
}

get
和set
方法
方法二:写/**
* Copyright (c) 2016-2019 人人开源 All rights reserved.
*
* https://www.renren.io
*
* 版权所有,侵权必究!
*/
package com.atguigu.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.exception.BizCodeException;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
public class R2 extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public <T> T getData(TypeReference<T> tTypeReference) {
Object data = get("data");
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s,tTypeReference);
return t;
}
public R2 setData(Object data) {
this.put("data",data);
return this;
}
public R2(){
}
public R2 error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public R2 error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public R2 error(int code, String msg) {
this.put("code", code);
this.put("msg", msg);
return this;
}
public R2 error(BizCodeException bizCodeException){
return error(bizCodeException.getCode(),bizCodeException.getMsg());
}
public R2 ok() {
this.put("code", 0);
this.put("msg", "success");
return this;
}
public R2 ok(String msg) {
this.put("code", 0);
this.put("msg", msg);
return this;
}
public R2 ok(int code, String msg) {
this.put("code", code);
this.put("msg", msg);
return this;
}
public R2 ok(Map<String, Object> map) {
this.ok();
this.putAll(map);
return this;
}
@Override
public R2 put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode(){
return (Integer) this.get("code");
}
}

getSkuHasStock
方法
8、修改修改gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法
并在RS<List<SkuHasStockTo>> rs = new RS<List<SkuHasStockTo>>();
打上断点
@PostMapping("/hasStock")
public RS<List<SkuHasStockTo>> getSkuHasStock(@RequestBody List<Long> skuIds) {
List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
RS<List<SkuHasStockTo>> rs = new RS<List<SkuHasStockTo>>();
RS<List<SkuHasStockTo>> ok = rs.ok();
ok.setData(skuHasStocks);
return rs;
}

4、重新测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
WareSkuController
类
1、执行到不断执行断点,直到执行到gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法的return rs;
这里
可以看到已经正确封装对象了

2、超时异常
点击Services
里的GulimallProductApplication
服务,这时已经报了超时异常
这是因为刚刚调试用了很多时间,所以出现了超时异常
detailMessage ="Read timed out executing POST http://gulimall-ware/ware/waresku/hasStock"

ware
模块的断点
3、取消取消RS<List<SkuHasStockTo>> rs = new RS<List<SkuHasStockTo>>();
上的断点
修改gulimall-ware
模块的com.atguigu.gulimall.ware.controller.WareSkuController
类的getSkuHasStock
方法,变为更加精简的方式
@PostMapping("/hasStock")
public RS<List<SkuHasStockTo>> getSkuHasStock(@RequestBody List<Long> skuIds) {
List<SkuHasStockTo> skuHasStocks = wareSkuService.getSkuHasStock(skuIds);
RS<List<SkuHasStockTo>> rs = new RS<List<SkuHasStockTo>>();
return rs.ok().setData(skuHasStocks);
}

5、再次测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端页面,再次在商品系统/商品维护/spu管理
里的id为1
的商品里点击右侧操作
里的上架
在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl
类的up
方法
执行完RS<List<SkuHasStockTo>> skuHasStock = wareFelginService.getSkuHasStock(skuIdList);
来到isSkuStock = skuHasStock.getData().stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
这里可以看到以成功封装数据

6、 整体测试
重新以debug
方式启动GulimallProductApplication
商品服务和GulimallWareApplication
库存服务
刷新前端项目,然后在商品系统/商品维护/spu管理
里的id为2
的商品里点击右侧操作
里的上架
然后不断点击点击Resume Program
或按F9
,直到运行结束
切换到前端,可以看到在商品系统/商品维护/spu管理
里的id为2
的商品里上架状
态已变为已上架

5.3、商城业务-首页&nginx
5.3.1、打开首页
1、引入依赖
thymeleaf
依赖
1、引入在gulimall-product
模块的pom.xml
文件里引入thymeleaf
依赖
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>

devtools
依赖
2、引入在gulimall-product
模块的pom.xml
文件里引入devtools
依赖,该依赖可以在不重启服务的情况下动态刷新静态资源
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.1.5.RELEASE</version>
<optional>true</optional>
</dependency>

点击Build
-> Build Project
或按快捷键Ctrl+F9
,可以刷新gulimall-product
项目的静态文件
(刷新java代码会有一些问题,修改代码后建议重新运行项目)

点击Build
-> Recompile "index.html'
或按快捷键Ctrl+ Shift+F9
,可以重新编译当前静态文件

3、去掉版本后报错
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
去掉thymeleaf
依赖和devtools
依赖的<version>2.1.5.RELEASE</version>
后保存
Failed to read artifact descriptor for org.springframework.boot:spring-boot-starter-thymeleaf:jar:2.1.8.RELEASE
无法读取 org.springframework.boot:spring-boot-starter-thymeleaf:jar:2.1.8.RELEASE 的工件描述符

clean
&install
4、clean
1、点击点击IDEA
右侧的Maven
,选择gulimall-product
,点击Lifecycle
(生命周期),点击clean
删除由项目编译创建的target文件夹

install
2、点击点击IDEA
右侧的Maven
,选择gulimall-product
,点击Lifecycle
(生命周期),点击install
将当前项目jar包安装(放置)至Maven的本地仓库中。供其他项目使用
此时报了一个错
Error:(5,32) java: 程序包com.atguigu.common.valid不存在

3、查看依赖
点击IDEA
右侧的Maven
,选择gulimall-product
,点击Dependencies
,可以看到com.atquiqu.qulimall:qulimall-common:0.0.1-SNAPSHOT
报红了,所有找不到com.atguigu.common.valid
包

4、重新加载
点击IDEA
右侧的Maven
,点击刷新按钮Reload All Maven Projects
重新加载所有maven
项目

install
5、再次点击点击IDEA
右侧的Maven
,选择gulimall-product
,点击Lifecycle
(生命周期),点击install
又报了同样的错误
Error:(6,32) java: 程序包com.atguigu.common.valid不存在

clean
&install
common模块
6、如果安装的有Maven Helper
插件,可以选择Project
里的gulimall-common
模块,然后右键选择Run Maven
-> clean install

该插件如下图所示

或者点击IDEA
右侧的Maven
,选择gulimall-common
,点击Lifecycle
(生命周期),点击clean
删除由项目编译创建的target文件夹
点击install
将当前项目jar包安装(放置)至Maven的本地仓库中。供其他项目使用
这样也可以

install
7、再次点击点击IDEA
右侧的Maven
,选择gulimall-product
,点击Lifecycle
(生命周期),点击install
,这样就成功了

Dependencies
报红
8、ponx.ml
不报错了,install
也成功了,但点击IDEA
右侧的Maven
可以看到gulimall-product
模块的Dependencies
里的org.springframework. boot:spring-boot-starter-thymeleaf:2.1 .8.RELEASE
和org.springframework.boot:spring-boot-devtools:2.1.8.RELEASE
还是报红

5、清除缓存
点击File
-> invalidate caches/restart...
清除缓存并重新打开项目
当maven
项目依赖问题怎么都解决不了的时候,这种方式时最有效的
(还有一种方式是手动安装maven
依赖)

2、导入资源
在gulimall-product
模块的src/main/resources
目录下新建static
文件夹和templates
文件夹
把2.分布式高级篇(微服务架构篇)\资料源码\代码\html\首页资源
里的index
文件夹及其子文件复制到gulimall-product
模块的src/main/resources/static
目录里
把2.分布式高级篇(微服务架构篇)\资料源码\代码\html\首页资源
里的index.html
复制到gulimall-product
模块的src/main/resources/templates
目录里


3、关闭thymeleaf的缓存
在gulimall-product
模块的src/main/resources/application.yml
配置文件里添加配置,关闭thymeleaf的缓存
spring:
thymeleaf:
#关闭thymeleaf的缓存
cache: false
prefix: classpath:/templates/
suffix: .html

4、查看前端页面
直接通过浏览器访问: http://localhost:10000/

5.3.2、查询所有一级分类
IndexController
类
1、新建在gulimall-product
模块的com.atguigu.gulimall.product
包下新建web
文件夹,在web
文件夹里新建IndexController
类
package com.atguigu.gulimall.product.web;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/10
* @Description:
*/
@Controller
public class IndexController {
@Autowired
CategoryService categoryService;
@GetMapping(value = {"/","index.html"})
public String indexPage(Model model){
//查出所有的一级分类
List<CategoryEntity> categoryEntities = categoryService.getLevel1Categories();
model.addAttribute("categories",categoryEntities);
//classpath:/templates/ + index + .html
return "index";
}
}

getLevel1Categories
抽象方法
2、新建在gulimall-product
模块的com.atguigu.gulimall.product.service.CategoryService
接口里新建getLevel1Categories
抽象方法
List<CategoryEntity> getLevel1Categories();

getLevel1Categories
抽象方法
3、实现在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
类里实现getLevel1Categories
抽象方法
@Override
public List<CategoryEntity> getLevel1Categories() {
LambdaQueryWrapper<CategoryEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(CategoryEntity::getParentCid,0);
lambdaQueryWrapper.select(CategoryEntity::getCatId,CategoryEntity::getName);
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(lambdaQueryWrapper);
return categoryEntities;
}

ndex.html
文件
4、修改ndex.html
文件
1、修改修改gulimall-product
模块的src/main/resources/templates/index.html
文件

thymeleaf
步骤
2、使用1、引入命名空间
把
<html lang="en">
替换为
<html lang="en" xmlns:th="http://www.thymeleaf.org">


th
标签
2、使用把
<ul>
<li>
<a href="#" class="header_main_left_a" ctg-data="3"><b>家用电器</b></a>
</li>
<li class="header_li2">
<a href="#" class="header_main_left_a" ctg-data="2,4"><b>手机</b> / <b>运营商</b> / <b>数码</b></a>
</li>
<li class="header_li2">
<a href="#" class="header_main_left_a" ctg-data="6"><b>电脑</b> / <b>办公</b></a>
</li>
</ul>
替换为
<ul>
<li th:each="category : ${categories}">
<a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器</b></a>
</li>
</ul>

th:each="category : ${categories}"
遍历集合categories
,每次遍历的单个对象命名为category
th:text="${category.name}"
获取category
对象的name
属性的值,并相当于value
属性,替换掉原本标签的内容

th:attr="ctg-data=${category.catId}
自定义ctg-data
属性,其值为动态获取的category.catId

5、浏览器访问
浏览器访问:http://localhost:10000

5.3.3、渲染二级三级分类数据
1、查看数据的结构
1、模拟数据
模拟的数据在gulimall-product
模块的src/main/resources/static/index/json/catalog.json
文件里,复制该模拟数据

2、发送请求的文件
发送请求的文件在gulimall-product
模块的在src/main/resources/static/index/js/catalogLoader.js
里面。把第二行的$.getJSON("index/json/catalog.json",function (data) {
修改为$.getJSON("index/catalog.json",function (data) {

3、格式化后数据的结构
粘贴刚刚复制的模拟数据,粘贴到 www.json.cn 这个网站,查看json的结构

Catelog2Vo
类
2、新建在gulimall-product
模块的com.atguigu.gulimall.product.vo
包下,新建Catelog2Vo
类
package com.atguigu.gulimall.product.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author 无名氏
* @date 2022/7/11
* @Description: 二级分类vo
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
/**
* 该二级分类对应的一级分类的id
*/
private String catalog1Id;
/**
* 当前二级分类的id
*/
private String id;
/**
* 当前二级分类的名字
*/
private String name;
/**
* 该二级分类对应的所有三级分类
*/
private List<Catelog3Vo> catalog3List;
/**
* 三级分类vo
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Catelog3Vo{
/**
* 该三级分类对应的二级分类
*/
private String catalog2Id;
/**
* 该三级分类的id
*/
private String id;
/**
* 该三级分类的名字
*/
private String name;
}
}

getCatalogJson
方法
3、添加在gulimall-product
模块的com.atguigu.gulimall.product.web.IndexController
类里添加getCatalogJson
方法
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String,List<Catelog2Vo>> getCatalogJson(){
return categoryService.getCatalogJson();
}

getCatalogJson
抽象方法
4、添加在gulimall-product
模块的com.atguigu.gulimall.product.service.CategoryService
接口里添加getCatalogJson
抽象方法
/**
* 查出所有的分类
* @return k为一级分类的id,v为二级分类及其子分类的信息
*/
Map<String, List<Catelog2Vo>> getCatalogJson();

getCatalogJson
抽象方法
5、实现在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
类里实现getCatalogJson
抽象方法
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//1、查出所有一级分类
List<CategoryEntity> level1Categories = this.getLevel1Categories();
Map<String, List<Catelog2Vo>> result = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
//2、该一级分类的所有二级分类
LambdaQueryWrapper<CategoryEntity> category2QueryWrapper = new LambdaQueryWrapper<>();
category2QueryWrapper.eq(CategoryEntity::getParentCid, l1.getCatId());
List<CategoryEntity> category2Entities = this.baseMapper.selectList(category2QueryWrapper);
List<Catelog2Vo> catelog2VoList = null;
if (category2Entities != null) {
catelog2VoList = category2Entities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo();
catelog2Vo.setCatalog1Id(l1.getCatId().toString());
catelog2Vo.setId(l2.getCatId().toString());
catelog2Vo.setName(l2.getName());
//3、当前二级分类的所有三级分类
LambdaQueryWrapper<CategoryEntity> category3QueryWrapper = new LambdaQueryWrapper<>();
category3QueryWrapper.eq(CategoryEntity::getParentCid,l2.getCatId());
List<CategoryEntity> category3Entities = this.baseMapper.selectList(category3QueryWrapper);
List<Catelog2Vo.Catelog3Vo> catelog3VoList = null;
if (category3Entities!=null){
catelog3VoList = category3Entities.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
catelog3Vo.setId(l3.getCatId().toString());
catelog3Vo.setName(l3.getName());
catelog3Vo.setCatalog2Id(l2.getCatId().toString());
return catelog3Vo;
}).collect(Collectors.toList());
}
catelog2Vo.setCatalog3List(catelog3VoList);
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2VoList;
}));
return result;
}

6、测试
重启GulimallProductApplication
服务,访问: http://localhost:10000/index/catalog.json

可以看到返回的结构与模拟数据的结构一致

首页已经显示数据了

测试以下是不是重数据库中查询来的
修改gulimall_pms
数据库的pms_category
表的name
为电子书
的字段,修改其为电子书11
。刷新首页,可以看到已经更新了

如果没有更新,可以清除浏览器的缓存,再次刷新页面
依次点击 chrome
浏览器右侧的竖着的三个点 -> 设置 -> 隐私设置和安全性 -> 清除浏览数据 -> 清除数据

5.3.4、nginx反向代理
正向代理:如科学上网,隐藏客户端信息

反向代理:屏蔽内网服务器信息,负载均衡访问

hsot
文件添加域名
1、host
文件的方式
1、可修改可以在C:\Windows\System32\drivers\etc
目录下的hosts
文件进行修改
第一次使用记事本
会无法修改,可以使用sublime text
或notepad++
进行修改

也可以使用SwitchHosts
工具修改

2、使用自定义方案
打开SwitchHosts
工具
选择
本地方案
点击
+
号在弹出的对话框的
本地方案
的方案名
里输入gulimall
点击
OK

选择本地方案里的
gulimall
在里面输入
192.168.56.10 gulimall.com
点击
对勾
即可使用当前方案

3、访问
访问虚拟机: http://gulimall.com:9200/
可以看到访问成功了

2、查看niginx配置
1、nginx配置的目录结构

2、查看ngix配置
1、总配置
docker ps
cd /mydata/nginx/
ls
cd conf/
ls
vi nginx.conf


2、server块配置
cd conf.d/
ls
vi default.conf


3、修改nginx配置
cp default.conf gulimall.conf
ls
vi gulimall.conf

配置所有请求都到商品服务
server {
listen 80;
server_name gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
proxy_pass http://192.168.56.1:10000;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

server_name
配置的就是通过浏览器访问的请求的Request Headers
请求头的nama
属性

4、重启nginx
docker restart nginx

5、查看配置是否生效
浏览器访问: http://gulimall.com/

nginx
负载均衡到网关
5.3.5、
动静分离静:图片,js、css等静态资源(以实际文件存在的方式) 动:服务器需要处理的请求
每一个微服务都可以独立部署、运行、升级、独立自治;技术,架构,业务
Nginx+Windows搭建域名访问环境

让nginx帮我们进行反向代理,所有来自原gulimall.com
的请求,都转到商品服务
nginx代理给网关的时候,会丢失请求的host信息,需要在location
里添加 proxy_set_header Host $host;
1、查看文档
收购前网址: https://nginx.org/
收购后网址: https://www.nginx.com/
The simplest configuration for load balancing with nginx may look like the following:
使用 nginx 进行负载平衡的最简单配置可能如下所示:(添加上游服务器)
http {
upstream myapp1 {
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp1;
}
}
}

2、上游服务器配置
cd ../
ls
vi nginx.conf

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
upstream gulimall{
server 192.168.56.1:88;
}
include /etc/nginx/conf.d/*.conf;
}

3、负载均衡到上游服务器
cd conf.d/
ls
vi gulimall.conf

server {
listen 80;
server_name gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

4、重启nginx
docker restart nginx

5、浏览器访问首页
浏览器访问首页:http://gulimall.com/
访问失败的原因是nginx
转给了后端的网关,而网关没有配置

6、配置网关
1、官方文档
官方文档:https://spring.io/projects/spring-cloud-gateway/
域名配置文档:https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#the-host-route-predicate-factory

2、配置网关
在gulimall-gateway
模块的src/main/resources/application.yml
配置文件中配置
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com

7、查看首页是否显示
重启GulimallGatewayApplication
服务,刷新前端页面
1、浏览器访问
打开 http://gulimall.com/ 页面,可以看到没有访问成功

debug
方式启动网关
2、以以debug
方式重新启动GulimallGatewayApplication
服务,此时报了一个错
javax.management.InstanceNotFoundException: org.springframework.boot:type=Admin,name=SpringApplication

Enable JMX agent
3、取消勾选点击Unnamed
,点击Edit Configurations...
选择GulimallGatewayApplication
服务,点击编辑
按钮,在Configuration
的Spring boot
里取消勾选Enable JMX agent
再次以debug
方式重新启动GulimallGatewayApplication
服务

4、又报了个错
Positive matches:
-----------------
ArchaiusAutoConfiguration matched:
- @ConditionalOnClass found required classes 'com.netflix.config.ConcurrentCompositeConfiguration', 'org.apache.commons.configuration.ConfigurationBuilder' (OnClassCondition)

5、向左缩进两格
修改gulimall-gateway
模块的src/main/resources/application.yml
配置文件的id
为gulimall_host_route
的配置
让其相关配置向左缩进两格

6、路径匹配
重新以debug
方式重新启动GulimallGatewayApplication
服务,刷新 http://gulimall.com/ 页面
可以看到匹配了id
为gulimall_host_route
的规则,但是还是无法访问

这是因为nginx
在转发的时候,没有把Request Headers
里的Host: gulimall.com
带上

8、nginx添加头信息
ls
vi gulimall.conf

在location / {
里添加配置
proxy_set_header Host $host;

9、重启nginx
docker restart nginx

10、刷新页面
刷新 http://gulimall.com/ 页面,已经可以正常访问了

5.3.6、压力测试
压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都 是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是: 内存泄漏,并发与同步。
有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。
1、性能指标
响应时间(Response Time: RT)
响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响应结束,整个过程所耗费的时间。
HPS(Hits Per Second) :每秒点击次数,单位是次/秒。
TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。
QPS(Query per Second):系统每秒处理查询次数,单位是次/秒。
对于互联网业务中,如果某些业务有且仅有一个请求连接,那么 TPS=QPS=HPS,一般情况下用 TPS 来衡量整个业务流程,用 QPS 来衡量接口查询次数,用 HPS 来表示对服务器单击请求。
无论 TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经验,一般情况下:
- 金融行业:1000TPS~50000TPS,不包括互联网化的活动
- 保险行业:100TPS~100000TPS,不包括互联网化的活动
- 制造行业:10TPS~5000TPS
- 互联网电子商务:10000TPS~1000000TPS
- 互联网中型网站:1000TPS~50000TPS
- 互联网小型网站:500TPS~10000TPS
最大响应时间(Max Response Time) 指用户发出请求或者指令到系统做出反应(响应)的最大时间。
最少响应时间(Mininum ResponseTime) 指用户发出请求或者指令到系统做出反应(响应)的最少时间。
90%响应时间(90% Response Time) 是指所有用户的响应时间进行排序,第 90%的响应时间。
从外部看,性能测试主要关注如下三个指标
吞吐量:每秒钟系统能够处理的请求数、任务数。
响应时间:服务处理一个请求或一个任务的耗时。
错误率:一批请求中结果出错的请求所占比例。
2、安装JMeter
官方网址:https://jmeter.apache.org/download_jmeter.cgi
点击Download
里的Download Releases
,在Binaries
里选择apache-jmeter-5.5.zip 即可下载
历史版本下载地址: Index of /dist/jmeter/binaries (apache.org)

解压后,在apache-jmeter-5.2.1\bin
目录下,点击jmeter.bat
可执行文件即可打开JMeter

中文显示
依次点击Options
-> Choose Language
-> Chinese (Simplified)

设置背景色为白色
依次点击选项
-> 外观
-> Nimbus
-> Yes
,然后重新打开即可

可以看到再次打开后还是英文显示,这是因为JMeter
使用界面设置中文下次打开JMeter
仍然会显示为英文
在apache-jmeter-5.2.1\bin
目录的jmeter.properties
配置文件里添加如下配置
language=zh_CN

3、添加线程组
1、添加取样器
选中测试计划
,右键依次选择添加
-> 线程(用户)
-> 线程组

在线程属性
里的线程数
里输入200
,表示启动200个线程
在线程属性
里的Ramp-Up时间(秒) :
里输入1
,表示1秒内启动完成
在循环次数
里输入100
,表示每个线程循环100次,共发送200x100 =20000个请求

察看结果树
2、添加选中线程组
,右键依次选择 添加
-> 监听器
-> 察看结果树

汇总报告
3、添加选中线程组
,右键依次选择 添加
-> 监听器
-> 汇总报告

汇总图
4、添加选中线程组
,右键依次选择 添加
-> 监听器
-> 汇总图

http请求
5、添加选中线程组
,右键依次选择 添加
-> 取样器
-> http请求

在Web服务器
里 协议:
输入http
,服务器名称或IP:
输入www.baidu.com
,端口号:
输入80

4、对百度进行测试
1、执行测试

2、察看结果树
可以看到都请求成功了

3、汇总报告

4、汇总图

5、对本项目进行测试
1、执行测试
在http请求
里的服务器名称或IP:
修改为gulimall.com
,点击工具栏的清除全部
按钮,然后点击启动
按钮

2、察看结果树

3、汇总报告

4、汇总图

想要显示图像,可以在设置中勾选想要显示的列显示
,点击显示图表
即可

6、重新测试
1、调大内存
在GulimallGatewayApplication
模块的运行配置里的Environment
栏的VM options
里的右方框里输入-Xmx100m
,限制GulimallGatewayApplication
模块的最大内存占用为100m
-Xmx500m

在GulimallProductApplication
模块的运行配置里的Environment
栏的VM options
里的右方框里输入-Xmx100m
,限制GulimallProductApplication
模块的最大内存占用为100m
-Xmx500m

2、执行测试
点击工具栏的清除全部
按钮,然后点击启动
按钮

3、察看结果树

4、汇总报告

5、汇总图

可以看到除了执行时间,其他性能并没有明显的改善
6、JMeter Address Already in use
1、问题描述
在windows
系统访问有可能报Address Already in use
异常(我的没报这个异常)
在线程组
里的线程属性
里,将线程数
修改为50
,循环次数
勾选永远

在HTTP请求
的基本
里的Web服务器
里 服务器名称或IP:
修改为127.0.0.1
,端口号:
修改为10000

点击工具栏的清除全部
按钮,然后点击启动
按钮

进行测试

有可能出现大量异常请求

在察看结果树
里可以看到报的是Address Already in use
错误
这是windows 本身提供的端口访问机制的问题。 Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致 我们在短时间内跑大量的请求时将端口占满了。

2、解决方案
cmd 中,用 regedit 命令打开注册表
在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 下,
右击 parameters,添加一个新的 DWORD,名字为
MaxUserPort
然后双击 MaxUserPort,基数选择十进制,输入数值数据为
65534
(如果是分布式运 行的话,控制机器和负载机器都需要这样操作哦)右击 parameters,添加一个新的 DWORD,名字为
TCPTimedWaitDelay
然后双击
TCPTimedWaitDelay
,基数选择十进制,输入数值数据为30
修改配置完毕之后记得重启机器才会生效


官方文档:错误 WSAENOBUFS (10055) - Windows Client | Microsoft Docs

5.3.7、性能监控
1、jvm 内存模型

- 程序计数器 Program Counter Register:
- 记录的是正在执行的虚拟机字节码指令的地址,
- 此内存区域是唯一一个在 JAVA 虚拟机规范中没有规定任何OutOfMemoryError 的区域
- 虚拟机:VM Stack
- 描述的是 JAVA 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法接口等信息
- 局部变量表存储了编译期可知的各种基本数据类型、对象引用
- 线程请求的栈深度不够会报 StackOverflowError 异常
- 栈动态扩展的容量不够会报 OutOfMemoryError 异常
- 虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈
- 本地方法:Native Stack
- 本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法
- 堆:Heap
- 几乎所有的对象实例都在堆上分配内存

2、堆
所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为“GC 堆”;也是我们优化最多考虑的地方。
堆可以细分为:
- 新生代
- Eden 空间
- From Survivor 空间
- To Survivor 空间
- 老年代
- 永久代/元空间
- Java8 以前永久代,受 jvm 管理,java8 以后元空间,直接使用物理内存。因此,默认情况下,元空间的大小仅受本地内存限制。
垃圾回收

从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一个新的区域—元空间(MetaSpace)


3、jconsole 与 jvisualvm
Jdk 的两个小工具 jconsole、jvisualvm(升级版的 jconsole);通过命令行启动,可监控本地和
远程应用。远程应用需要配置
1、jconsole使用
1、打开
打开cmd
,输入jconsole
,选择一个服务后即可查看内存
、线程
、类
、vm
等信息

2、概要

3、内存

4、线程

5、类

6、VM 概要

7、MBean

jvisualvm
使用
2、1、打开
开cmd
,输入jvisualvm
,选择一个服务后即可查看各种信息

2、概要

3、监视

4、线程

5、抽样器

6、Profiler

Visual GC
3、安装1、下载插件
点击工具
,选择插件
,在可用插件
里点击检查最新版本(F)
,然后选中Visual GC
点击安装(I)
即可


2、检查最新版本错误
如果点击检查最新版本
出现了错误,可以在cmd
输入以下命令,查看jdk详细版本
java -version
进入该网站:https://visualvm.github.io/pluginscenters.html ,查询该版本对应的更新地址
例如我的java版本为java version "1.8.0_301"
,后面的小版本为301
因此对应该网站显示的JDK 8 Update 131 - 331
点进去,复制里面显示的url即可(url以xml.gz
结尾)
https://visualvm.github.io/uc/8u131/updates.xml.gz

3、下载插件错误

https://github.com/visualvm/visualvm.src/releases/download/1.3.9/com-sun-tools-visualvm-modules-visualgc.nbm中出现网络问题
检查代理设置或稍后重试。服务器目前可能不可用。 您可能还需要确保防火墙不会阻塞网络通信。 您的高速缓存可能已过期。请单击“检查更新”以刷新内容。
这种是githu的这个网址访问不了了,可以换个时间下载,或者使用某些方法下载,也可以通过迅雷下载
依次点击工具(T)
-> 插件(G)
-> 已下载
-> 添加插件(A)..
即可添加离线插件

可以在刚刚那个网址 https://visualvm.github.io/pluginscenters.html 里点击对应版本的链接后,在下面即可看到各个工具的下载地址

Visual GC
4、查看
4、测试Nginx
1、准备工作
1、查看docker资源使用情况
使用docker stats
命令可以查看docker
里,各容器的cpu
、内存等的占用情况
docker stats

2、设置线程数
选中测试计划
,右键依次选择添加
-> 线程(用户)
-> 线程组
点击刚刚创建的线程组
,在线程属性
里的线程数
里输入50
,表示启动50个线程
在线程属性
里的Ramp-Up时间(秒) :
里输入1
,表示1秒内启动完成
在循环次数
里勾选永远
,表示如果不点击停止
按钮,就一直执行

http
请求信息
3、修改选中线程组
,右键依次选择 添加
-> 取样器
-> http请求
,
在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入192.168.56.10
,端口号:
输入80
,路径
输入/

4、查看要压测的页面
http://192.168.56.10/

察看结果树
、汇总报告
、聚合报告
5、添加选中线程组
,右键依次选择 添加
-> 监听器
-> 察看结果树
选中线程组
,右键依次选择 添加
-> 监听器
-> 汇总报告
选中线程组
,右键依次选择 添加
-> 监听器
-> 聚合报告

2、执行测试
先打开docker
资源页面查看nginx
的资源占用情况,然后点击工具栏的清除全部
按钮,然后点击启动
按钮,切换到docker容器资源页面,查看nginx
的资源使用情况

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
1、察看结果树

2、汇总报告

3、聚合报告

5、测试Gateway
1、准备工作
准备压测gulimall-gateway
模块

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入88
,路径
输入/

要压测的页面: http://localhost:88/

2、执行测试
先打开jvisualvm
查看GulimallGatewayApplication
服务的资源占用情况,然后点击工具栏的清除全部
按钮,然后点击启动
按钮,切换到jvisualvm
查看GulimallGatewayApplication
服务的资源占用情况,可以看到网关和nginx差不多,都是消耗cpu型

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

6、测试简单服务
1、准备工作
在gulimall-product
模块的com.atguigu.gulimall.product.web.IndexController
类里添加hello
方法
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}

重启GulimallProductApplication
服务
要压测的页面: http://localhost:10000/hello

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/hello

2、执行测试
先打开jvisualvm
查看GulimallProductApplication
服务的资源占用情况,然后点击工具栏的清除全部
按钮,然后点击启动
按钮,切换到jvisualvm
查看GulimallProductApplication
服务的资源占用情况

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

Gateway
+简单服务
7、测试1、准备工作
在gulimall-gateway
模块的src/main/resources/application.yml
配置文件添加路径映射
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**,/hello
filters:
# (?<segment>/?.*) 和 $\{segment} 为固定写法
#http://localhost:88/api/product/category/list/tree 变为 http://localhost:10000/product/category/list/tree
- RewritePath=/api/(?<segment>/?.*),/$\{segment}

重启GulimallGatewayApplication
服务,刷新前端页面: http://localhost:88/hello

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入88
,路径
输入/hello

2、执行测试

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

8、测试全链路简单服务
1、准备工作
在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入gulimall.com
,端口号:
输入80
,路径
输入/hello

要压测的页面: http://gulimall.com/hello

2、执行测试

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

9、首页一级菜单渲染
1、准备工作
在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/

要压测的页面: http://localhost:10000/

2、执行测试

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

10、三级分类数据获取
1、准备工作
在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/index/catalog.json

要压测的页面是:http://localhost:10000/index/catalog.json
而不是:http://localhost:10000/index/json/catalog.json

2、执行测试

3、察看结果树
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
汇总报告

汇总报告

聚合报告

11、首页全量数据获取
1、准备工作
在HTTP请求
的基本
里的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/

在HTTP请求
的高级
里,勾选从HTML文件获取所有内含的资源
和并行下载
(并行下载数量为6)

要压测的页面: http://localhost:10000/

2、执行测试

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树
在察看结果树
的取样器结果
里报了404
的错误

在察看结果树
的响应数据
里的Response Body
里可以看到页面请求成功了,报404
是因为有些图片
、javascript
没有请求成功

汇总报告

聚合报告

12、首页渲染(开缓存)
1、准备工作
在gulimall-product
模块的src/main/resources/application.yml
配置文件里,把thymeleaf
缓存打开
spring:
thymeleaf:
cache: true
重启GulimallProductApplication
服务

在线程组
的线程属性
里的线程数
里输入50
,表示启动50个线程
在线程组
的线程属性
里的Ramp-Up时间(秒) :
里输入1
,表示1秒内启动完成
在线程组
的循环次数
里勾选永远

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/

在HTTP请求
的高级
里,取消勾选从HTML文件获取所有内含的资源

要压测的页面: http://localhost:10000/

2、执行测试

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

13、首页渲染(开缓存、优化数据库、关日志)
1、修改日志级别
修改gulimall-product
模块的src/main/resources/application.yml
配置文件的日志级别
logging:
level:
com.atguigu.gulimall: error

2、使用索引
1、没加索引
修改gulimall-product
模块的com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
类的getLevel1Categories
方法
@Override
public List<CategoryEntity> getLevel1Categories() {
LambdaQueryWrapper<CategoryEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(CategoryEntity::getParentCid, 0);
lambdaQueryWrapper.select(CategoryEntity::getCatId, CategoryEntity::getName);
long start = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(lambdaQueryWrapper);
long end = System.currentTimeMillis();
System.out.println("消耗时间:"+(end-start));
return categoryEntities;
}
重启GulimallProductApplication
服务

多刷新几次主页后再测试:http://localhost:10000/
可以看到,消耗的时间都在2~5
秒

2、加索引
打开navicat
,选择gulimall_pms
数据库的pms_category
表,右键选择设计表
点击索引
,在字段
里选择parent_cid
,然后点击保存
,这样就给parent_cid
添加了一个默认的索引

名
、索引类型
、索引方法
都为系统自动生成的

多刷新几次主页后再测试:http://localhost:10000/ (不用重启服务。保险起见,也可以重启GulimallProductApplication
服务)
可以看到,消耗的时间都在1~3
秒,加索引后速度还是有明显提升的

3、执行测试

4、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

14、三级分类(优化业务)
1、准备工作
在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/index/catalog.json

要压测的页面是: http://localhost:10000/index/catalog.json
而不是: http://localhost:10000/index/json/catalog.json

2、执行测试

3、查看压测报告
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

查询数据库消耗的时间:

15、压测报告表
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间/ms | 99%响应时间/ms |
---|---|---|---|---|
Nginx | 50 | 882 | 15 | 1899 |
Gateway | 50 | 1552 | 47 | 149 |
简单服务 | 50 | 11431 | 8 | 24 |
首页一级菜单渲染 | 50 | 202 | 381 | 645 |
首页渲染(开缓存) | 50 | 229 | 286 | 497 |
首页渲染(开缓存、优化数据库、关日志) | 50 | 510 | 174 | 333 |
三级分类数据获取 | 50 | 1.5 | 33628 | 33789 |
三级分类(优化业务) | 50 | 4.9 | 13230 | 14285 |
优化三级分类查询(查询一次数据库) | 5 | 89.3 | 839 | 2047 |
三 级 分 类 ( 使用 redis 作为缓存) | 50 | 159 | 422 | 842 |
首页全量数据获取 | 50 | 3 | 33699 | 38237 |
Nginx+Gateway | 50 | |||
Gateway+简单服务 | 50 | 793 | 98 | 159 |
全链路 | 50 | 346 | 171 | 1240 |
5.3.8、配置nginx
1、nginx动静分离

1、以后将所有项目的静态资源都应该放在nginx里面
2、规则:/static/**所有请求都由nginx直接返回
1、复制文件
在虚拟机的/mydata/nginx/html/
目录里新建static
目录,用于存放静态文件
cd /mydata/nginx/
ls
cd html/
ls
mkdir static
ls
cd static/
ls

将gulimall-product\src\main\resources\static
目录下的index
文件夹复制到虚拟机的/mydata/nginx/html/static
目录里
复制完后,删除gulimall-product\src\main\resources\static
目录下的index
文件夹

index.html
2、修改在gulimall-product
模块的src/main/resources/templates/index.html
文件里,修改部分代码,在所有请求的路径前都加上/static/
(第一个/
表示在当前项目下),使静态资源访问nigix

3、关闭缓存
在gulimall-product
模块的src/main/resources/application.yml
配置文件里,把thymeleaf
缓存关闭
spring:
thymeleaf:
cache: false
重启GulimallProductApplication
服务

niginx
配置
4、修改查看虚拟机的/mydata/nginx/conf/conf.d/default.conf
文件的配置
cd /mydata/nginx/conf/
ls
cd conf.d/
ls
cat default.conf

复制default.conf
文件里的root /usr/share/nginx/html;

选中1 电商
,右键选中复制会话(D)

编辑/mydata/nginx/conf/conf.d/gulimall.conf
文件,然后重启nginx
cd "/mydata/nginx/conf/conf.d"
ls
vi gulimall.conf
docker restart nginx
docker ps

在/mydata/nginx/conf/conf.d/gulimall.conf
文件里配置以/static/
开头的路径的访问规则
location /static/ {
root /usr/share/nginx/html;
}

/mydata/nginx/conf/conf.d/gulimall.conf
文件的完整内容:
server {
listen 80;
server_name gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /static/ {
root /usr/share/nginx/html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
5、访问首页
首先清除浏览器缓存,依次点击chrome
浏览器的 竖着的三个点 -> 设置
-> 隐私设置和安全性
-> 清吟浏览数据

访问: http://gulimall.com/
可以看到,绝大部分都可以访问,只有两个不能访问,这两个文件本来就没有,当然无法访问,不用管这两个

可以删掉这两个不能访问的请求
注释掉<img src="/static/index/img/top_find_logo.png" alt="">

注释掉<script type="text/javascript" src="/static/index/jsindex.js"></script>

nginx
进行压力测试
2、对1、准备工作
在gulimall-product
模块的src/main/resources/application.yml
配置文件里,把thymeleaf
缓存打开
spring:
thymeleaf:
cache: true
重启GulimallProductApplication
服务

在线程组
的线程属性
里的线程数
里输入50
,表示启动50个线程
在线程组
的线程属性
里的Ramp-Up时间(秒) :
里输入1
,表示1秒内启动完成
在线程组
的循环次数
里勾选永远

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入gulimall.com
,端口号:
输入80
,路径
输入/

在HTTP请求
的高级
里,勾选从HTML文件获取所有内含的资源
和并行下载
(并行下载数量为6)

要压测的页面: http://gulimall.com/

2、执行测试
如果在查看结果树
里全是异常也不要紧,只是没有把想要访问不存在的资源
的语句删掉而已

在测试期间,使用jvisualvm
查看CPU
、堆
等的资源消耗情况

3、查看压测报告
这次结果虽然比以前好一点,但是jmeter
卡死了几次,以前都没卡死过
把压测报告写入压测报告表中(在5.3.7.15、压测报告表
里面)
察看结果树

汇总报告

聚合报告

3、OOM
1、调大线程数
在线程组
的线程属性
里的线程数
里输入200
,表示启动200个线程
在线程组
的线程属性
里的Ramp-Up时间(秒) :
里输入1
,表示1秒内启动完成
在线程组
的循环次数
里勾选永远

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/

在HTTP请求
的高级
里,勾选从HTML文件获取所有内含的资源
和并行下载
(并行下载数量为6)

要压测的页面: http://localhost:10000/

2、执行测试


3、查看压测报告
报java.lang.OutOfMemoryError
异常
Exception in thread "RMI TCP Connection(idle)" java.lang.OutOfMemoryError: Java heap space
Exception in thread "http-nio-10000-Acceptor" java.lang.OutOfMemoryError: Java heap space
Exception in thread "http-nio-10000-BlockPoller" Exception in thread "File Watcher" java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Java heap space
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create name string at JPLISAgent.c line: 807
Exception in thread "http-nio-10000-ClientPoller" java.lang.OutOfMemoryError: Java heap space
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "com.alibaba.nacos.client.Worker.fixed-127.0.0.1_8848-d6d03bd1-5815-4fa1-8caf-93b09462fd45"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "http-nio-10000-exec-206"
Exception in thread "http-nio-10000-exec-490"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "http-nio-10000-exec-329"
java.lang.OutOfMemoryError: Java heap space
Exception in thread "http-nio-10000-exec-304" java.lang.OutOfMemoryError: Java heap space

刷新 http://localhost:10000/ 页面,可以发现无法访问

此时堆使用的内存已满

CPU
和堆
资源占用都非常高

4、调大堆内存
-Xss 设置每个线程可使用的内存大小,即栈的大小。 -Xms 堆内存的最小值,默认为物理内存的1/64。 -Xmx 堆内存的最大值,默认为物理内存的1/4。 -Xmn 堆内新生代(Eden+两个Survior)的大小,一般占据堆的 1/3 空间。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn。
-Xmx1024m -Xms1024m -Xmn512m
在GulimallProductApplication
模块的运行配置里的Environment
栏的VM options
里的右方框里输入-Xmx1024m -Xms1024m -Xmn512m
,限制GulimallProductApplication
模块的最大内存占用为1024m
,最小内存占用为1024m
,新生代的内存占用为512m

5、重新测试

6、查看压测报告
察看结果树

汇总报告

聚合报告

Visual GC
里显示,过了很久也没出现内存不足的情况

堆
在使用峰值时,也没有超过规定的1024m

GulimallProductApplication
服务的控制台也没报任何异常,说明调大内存可以很好地解决请求数过多的情况

4、优化三级分类查询
getCatalogJson
方法
1、修改在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
类里修改getCatalogJson
方法
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//一次查询所有
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
//1、查出所有一级分类
List<CategoryEntity> level1Categories = this.getLevel1Categories();
Map<String, List<Catelog2Vo>> result = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
//2、该一级分类的所有二级分类
List<CategoryEntity> category2Entities = getCategoryEntities(categoryEntities,l1);
List<Catelog2Vo> catelog2VoList = null;
if (category2Entities != null) {
catelog2VoList = category2Entities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo();
catelog2Vo.setCatalog1Id(l1.getCatId().toString());
catelog2Vo.setId(l2.getCatId().toString());
catelog2Vo.setName(l2.getName());
//3、当前二级分类的所有三级分类
List<CategoryEntity> category3Entities = getCategoryEntities(categoryEntities,l2);
List<Catelog2Vo.Catelog3Vo> catelog3VoList = null;
if (category3Entities!=null){
catelog3VoList = category3Entities.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
catelog3Vo.setId(l3.getCatId().toString());
catelog3Vo.setName(l3.getName());
catelog3Vo.setCatalog2Id(l2.getCatId().toString());
return catelog3Vo;
}).collect(Collectors.toList());
}
catelog2Vo.setCatalog3List(catelog3VoList);
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2VoList;
}));
return result;
}
private List<CategoryEntity> getCategoryEntities(List<CategoryEntity> categoryEntities,CategoryEntity l) {
//LambdaQueryWrapper<CategoryEntity> category2QueryWrapper = new LambdaQueryWrapper<>();
//category2QueryWrapper.eq(CategoryEntity::getParentCid, l1.getCatId());
//return this.baseMapper.selectList(category2QueryWrapper);
List<CategoryEntity> collect = categoryEntities.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(l.getCatId());
}).collect(Collectors.toList());
return collect;
}

2、准备工作
在GulimallProductApplication
模块的运行配置里的Environment
栏的VM options
里的右方框里输入-Xmx100m
,重新限制GulimallProductApplication
模块的最大内存占用为100m
-Xmx100m

要压测的请求: http://localhost:10000/index/catalog.json

在线程组
的线程属性
里的线程数
里输入50
,表示启动50个线程
在线程组
的线程属性
里的Ramp-Up时间(秒) :
里输入1
,表示1秒内启动完成
在线程组
的循环次数
里勾选永远

在HTTP请求
的基本
里的Web服务器
里 协议:
输入http
,服务器名称或IP:
输入localhost
,端口号:
输入10000
,路径
输入/index/catalog.json

在HTTP请求
的高级
里,取消勾选从HTML文件获取所有内含的资源

3、执行测试

4、查看压测报告
察看结果树

汇总报告

聚合报告
