分布式电商项目-高级篇
center-sept Lv2

全文检索

ElasticSearch

简介

1.一个分布式的开源搜索和分析引擎,适用于所有类型的数据,包括文本,数字,地理空间,结构化和非结构化数据。

2.全文搜索属于最常见的需求,开源的 Elasticsearch是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它。

3.Elastic 的底层是开源库 Lucene。但是,你没法直接用Lucene,必须自己写代码去调用它的接口。Elastic是 Lucene 的封装,提供了 REST API的操作接口,开箱即用。

REST API:天然的跨平台。
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html

官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html

社区中文:
http://doc.codingdict.com/elasticsearch/0/

基本概念
1.index (索引)

动词,相当于MySQL中的insert

名词,相当于MySQL中的Database

2.Type (类型)

在Index(索引)中,可以定义一个或多个类型。类似于MySQL中的Table;每一种类型的数据放在一起;

3.Document (文档)

保存在某个索引(Index)下,某种类型(Type)的一个数据(Ducument),文档是JSON格式的,Document就像是MySQL中的某个Table里面的内容;

4.倒排索引机制

分词:将整句拆分为单词

Docker安装ES

1.下载镜像文件
1
2
3
4
# 拉取ES镜像
docker pull elasticsearch:7.4.2
# 拉取可视化镜像
docker pull kibana:7.4.2
2.创建实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 # 创建配置文件夹
mkdir -p /mydata/elasticsearch/config
# 创建数据文件夹
mkdir -p /mydata/elasticsearch/data
# 创建插件文件夹
mkdir -p /mydata/elasticsearch/plugins
# 创建配置文件
echo "http.host:0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
# 让任何用户都能读写
chmod -R 777 /mydata/elasticsearch/
# 启动单机ES
# 9200 http通信端口 9300集群模式下通信端口
# -e "discovery.type=single-node" 单节点运行
# 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 :指定插件路径
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms64m -Xmx520m" \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-d elasticsearch:7.4.2

Docker安装Kibana

1
2
3
4
5
6
7
8
docker run --name kibana \
-e ELASTICSEARCH_URL=http://192.168.72.130:9200 \
-p 5601:5601 \
-d kibana:7.4.2
# 如果-e ELASTICSEARCH_URL=http://192.168.72.130:9200 没有效果,我们必须进入kibana容器中,修改kibana.yml 文件的内容
cd ./config
vi kibana.yml
# elasticsearch.hosts: [ “http://{你的安装ES的ip地址}:9200” ]

入门

_cat (查看ES信息)

1.使用Postman 对ES服务发送查询请求

GET /_cat/nodes: 查看所有节点

GET /_cat/health: 查看ES健康状态

GET /_cat/master: 查看主节点

GET /_cat/indices: 查看ES所有索引 就像MySQL的show databases

put&post新增数据

1.使用postman 向ES服务期发送PUT请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
url:http://192.168.72.130:9200/customer/external/1
method:put
body:
{
"name":"Tom"
}
response:
{
"_index": "customer", // 索引
"_type": "external", // 类型
"_id": "1", // id
"_version": 4, // 版本号
"result": "updated", // 结果 created:新建 ; updated:更新
"_shards": { // 分片,集群参数
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3, // 并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1 //主分片重新分配,如重启,就会变化
}

2.使用postman 向ES服务期发送POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
url:http://192.168.72.130:9200/customer/external/1
method:post
body:
{
"name":"Tom"
}
response:
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 5,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1
}

注:PUT和POST区别

1.POST新增:如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号

2.PUT可以新增也可以修改。PUT必须指定id;由于PUT需要指定id,我们一般都用来做修改操作,不指定id会报错。

get查询数据&乐观锁
get查询数据

查询直接使用get请求 /{索引}/{类型}/{id}

乐观锁

使用_seq_no 来确认版本号

1.乐观锁更新数据

url:http://192.168.72.130:9200/customer/external/1?if_seq_no=3&if_primary_term=1

method:PUT或者POST

如果版本号不定义给定的条件,则出现以下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[1]: version conflict, required seqNo [3], primary term [1]. current document has seqNo [4] and primary term [1]",
"index_uuid": "zb_uJgoTTIS96jNeCk45xQ",
"shard": "0",
"index": "customer"
}
],
"type": "version_conflict_engine_exception",
"reason": "[1]: version conflict, required seqNo [3], primary term [1]. current document has seqNo [4] and primary term [1]",
"index_uuid": "zb_uJgoTTIS96jNeCk45xQ",
"shard": "0",
"index": "customer"
},
"status": 409
}
put&post修改数据

方法1:

1
2
3
4
5
6
POST customer/external/1/_update
{
"doc":{
"name":"Tms1"
}
}

方法2:

1
2
3
4
5
6
POST customer/external/1
{
"name":"Tms1"
}
或者
PUT customer/external/1

1.异同

不同:POST操作会对比源文档数据,如果相同不会有什么操作,文档version不增加,PUT操作总会将数据重新保存并添加version版本;带_update对比元数据如果一样就不进行任何操作

看场景:

对于大并发更新,使用不带update;

对于大并发查询偶尔更新,带update;对比更新,重新计算分配规则。

删除数据&bulk批量操作
删除文档

可以直接删除索引和文档,但不能删除类型

1
DELETE customer/external/1
bulk批量操作

示例1:

1.登入kibana 控制台,点击Dev Tools,在console 输入框里输入以下内容:

1
2
3
4
5
POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"jojo"}
{"index":{"_id":"2"}}
{"name":"jiji"}

2.结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"took" : 8, // 花费多少毫秒
"errors" : false, // 是否有错误
"items" : [ // 结果子项
{
"index" : { // index 表示保存操作
"_index" : "customer",
"_type" : "external",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 6,
"_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" : 7,
"_primary_term" : 1,
"status" : 201
}
}
]
}

示例2:

1.在控制台输入以下内容:

1
2
3
4
5
6
7
8
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 sencond blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123"}}
{"doc":{"title":"My updated blog post"}}

2.结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{
"took" : 148,
"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" : "_SBEJnYB0s8ja78yQIS4",
"_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
}
}
]
}
导入客户银行客户样本测试数据

elasticsearch/accounts.json at master · elastic/elasticsearch (github.com)

1
2
POST /bank/account/_bulk
粘贴样本数据

进阶

两种查询方式
SearchAPI

ES支持两种基本方式检索:

  • 通过使用REST request URL 发送搜索参数(uri+检索参数)

  • 通过使用REST request body 来发送它们(uri+请求体)

uri+检索参数
  1. GET bank/_search

检索bank下所有信息,包括type的和docs

  1. GET bank/_search?q=*&sort=account_number:asc

请求参数方式检索

默认返回10条记录

  1. 结果解析

took:花费时间

time_out:是否超时

_shards:集群情况下的信息

hits:命中的记录

total :总记录数

uri+检索参数(QueryDSL方式)

1.在Kibana控制台输入以下内容:

1
2
3
4
5
6
7
8
9
GET bank/_search
{
"query": {
"match_all": {}
},
"sort":[
{"account_number":"asc"}
]
}

query:查询条件

match_all:匹配所有

sort:排序条件

QueryDSL基本使用&match_all

1.在kibana 根据语法提示直接操作

https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-search.html 官方操作实例

2.使用_source 后接 数组字符串,按需返回字段值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET bank/_search
{
"query":{
"match_all": {}
},
"from":0,
"size":5,
"sort":[
{
"account_number":{
"order":"desc"
}
}
],
"_source": ["banlance","firstname"]
}

3.demo

3.1 demo1

1
2
3
4
5
6
GET bank/_search
{
"query":{
"match_all": {}
}
}

3.2 demo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET bank/_search
{
"query":{
"match_all": {}
},
"from":0,
"size":5,
"sort":[
{
"account_number":{
"order":"desc"
}
}
]
}
match全文检索

1.基本类型(非字符串),精确匹配

1.1 精确匹配demo

1
2
3
4
5
6
7
8
GET bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}

1.2 模糊查询(全文检索)按照评分进行排序,会对检索条件进行分词匹配

1
2
3
4
5
6
7
8
GET bank/_search
{
"query": {
"match": {
"address": "Kings"
}
}
}
match_parase短语匹配

将需要匹配的值当成一个整体单词(不分词)进行检索

1
2
3
4
5
6
7
8
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill road"
}
}
}
multi_match多字段匹配

address 包含 mill 或者 Movico,city 包含 mill 或者 Movico

1
2
3
4
5
6
7
8
9
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill Movico",
"fields": ["address","city"]
}
}
}
bool复合查询

bool 用来做复合查询:

复合语句可以合并任何其他查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

must:必须达到must列举的所有条件

满足should指定的条件,则_scource得分高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "mill"
}},
{"match": {
"gender": "M"
}}
],
"must_not": [
{"match": {
"age": "18"
}}
],
"should": [
{"match": {
"lastname": "Wallace"
}}
]
}
}
}
filter (结果过滤)

并不是所有的查询都需要产生分数,特别是那些仅用于”filtering”(过滤)的文档。为了不计算分数Elasticsearch会自动检查场景并且优化查询的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "mill"
}},
{"match": {
"gender": "M"
}}
],
"must_not": [
{"match": {
"age": "18"
}}
],
"should": [
{"match": {
"lastname": "Wallace"
}}
],
"filter": {"range": {
"age": {
"gte": 18,
"lte": 30
}
}}
}
}
}

GET bank/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gte": 10,
"lte": 20
}
}
}
}
}
}
term 查询

和match一样。匹配某个属性值。全文检索字段用match,其他非text字段匹配用term。

  1. 精确匹配数值
1
2
3
4
5
6
7
8
9
10
11
GET bank/_search
{
"query": {
"term": {
"age": {
"value": 28
}
}
}
}

  1. keyword 精确匹配
1
2
3
4
5
6
7
8
GET bank/_search
{
"query": {
"match": {
"address.keyword": "789 Madison Street"
}
}
}

注:数值类型使用term,文本字段使用match

aggregations 聚合分析

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL的GROUP BY和SQL的聚合函数。在Elasticsearch中,您有执行搜索返回hits(命中结果),并且同时返回聚合结果,把一个响应的所有hits(命中结果)分隔开的能力。这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的API来避免网络往返。

搜索address中包含mill的所有人的年龄分布以及平均年龄,但不显示这些人的详情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
}
,
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg": {
"avg": {
"field": "age"
}
},
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}

按照年龄聚合,并且请求这些年龄端的这些人的平均薪资

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"ageAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}

查出所有年龄分布,并且这些年龄段中M(女士)的平均薪资和F(男士)的平均薪资以及这个年龄段的总体平均薪资

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {
"field": "gender.keyword",
"size": 10
},
"aggs": {
"banlanceAvg": {
"avg": {
"field": "balance"
}
}
}
},
"ageBalanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}

映射

mapping创建
什么是Mapping(映射)?

Mapping是用来定义一个文档,以及它所包含的属性是如何存储和索引的。比如,使用mapping来定义:

  • 哪些字符串属性应该被看做是全文本属性。

  • 哪些属性包含数字,日期或者地理位置。

  • 文档中的所有属性是否都能被索引(_all配置)。

  • 日期的格式。

  • 自定义映射规则来执行动态添加属性。

  1. 查询映射
1
GET /bank/_mapping
  1. 创建映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT /my-index
{
"mappings": {
"properties": {
"age": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"name": {
"type": "text"
}
}
}
}
添加新的字段映射

使用 PUT /my_index/_mapping

1
2
3
4
5
6
7
8
9
PUT /my-index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}
修改映射&数据迁移
更新映射

对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移。

数据迁移
  1. 查询之前的mapping数据,复制properties 中的数据
1
GET /bank/_mapping
  1. 创建新的索引映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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"
}
}
}
}
  1. 迁移数据
1
2
3
4
5
6
7
8
9
10
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newbank"
}
}

分词

分词&安装ik分词
何为分词器?

一个tokenizer(分词器)接收一个字符流,将之分割为独立的tokens(词元,通常是独立的单词),然后输出tokens流。

例如,whitespace tokenizer遇到空白字符时分割文本。它会将文本”Quick brown fox!”分割为 [Quick,brown,fox!]。

该tokenizer(分词器)还负责记录各个term(词条)的顺序或position位置(用于phrase短语和word proximity 词近邻拆查询),以及term(词条)所代表的原始word(单词)的start(起始)和end(结束)的character offsets (字符偏移量)(用于高亮显示搜索的内容)。

Elasticsearch 提供了很多内置的分词器,可以用来构建custom analyzer (自定义分词器)

  1. 使用分词器

1.1 空格分词器

1
2
3
4
5
POST _analyze
{
"analyzer": "whitespace",
"text": "The quick brown fox."
}

1.2 标准分词器

1
2
3
4
5
POST _analyze
{
"analyzer": "standard",
"text": "The quick brown fox."
}

2.安装ik分词器

2.1 使用wget 下载 ik分词器

1
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip

2.2 解压压缩包,放到挂载的plugins/ik文件夹下

2.3 测试分词器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用标准分词器
GET my-index/_analyze
{
"text": ["我是中国人"]
}
# 使用ik_smart分词器
GET my-index/_analyze
{
"analyzer": "ik_smart",
"text": ["我是中国人"]
}
# 使用ik_max_word分词器
GET my-index/_analyze
{
"analyzer": "ik_max_word",
"text": ["我是中国人"]
}
自定义扩展词库
为何需要自定义扩展词库?

有一些词汇,分词器中没有对应的,需要我们自己去添加到词库,然后分词器才能够识别。

自定义词库
  1. 使用nginx做静态文件重定向,需要安装nginx和启动,无需做nginx配置

1.1 在html文件夹下,创建es文件夹,再然后创建fenci.txt文本,内容是你需要进行分词的词句。

1.2 访问http://p/es/fenci.txt,网页能打开看到内容,说明能访问。

1.3 配置ik分词器配置文件,在elasticsearch的挂载目录的plugins下,解压之后的ik文件夹下进入config 文件夹下的IKAnalyzer.cfg.xml配置文件,把http://p/es/fenci.txt配置进去

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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.72.130/es/fenci.txt</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

整合

SpringBoot整合high-level-client
java客户端
  1. spring-data-elasticsearch-transport-api.jar (不兼容7.x,8以后废弃)

通过9300(TCP)端口操作

以下通过9200 Http请求交互

  1. JestClient

非官方,更新慢

  1. RestTemplate

模拟发http请求,ES很多操作需要自己封装,麻烦

  1. httpClient

模拟发http请求,ES很多操作需要自己封装,麻烦

  1. Elasticsearch-Rest-Client

官方RestClient,封装了ES操作,API层次分明,上手简单。

引入Elasticsearch-Rest-Client
  1. 引入maven依赖
1
2
3
4
5
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
  1. 修改SpringBoot 默认版本,由于spring-boot-dependencies 中elasticsearch默认版本为<elasticsearch.version>6.4.3</elasticsearch.version>
1
<elasticsearch.version>7.4.2</elasticsearch.version>
  1. 加入common 模块,配置服务发现,再配置ElasticSearch配置类,加入到Bean容器中。
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ElasticSearchConfig {

@Bean
public RestHighLevelClient esRestClient() {
RestClientBuilder builder = RestClient.builder(new HttpHost("192.168.72.130", 9200,
"http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
  1. 使用测试类,是否已经导入到容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class SuperMallElasticsearchApplicationTests {

@Autowired
RestHighLevelClient esClient;

@Test
public void contextLoads() {
System.out.println(esClient);
}

}
测试保存
  1. 在配置类加上ES公共配置Options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class ElasticSearchConfig {

public static final RequestOptions COMMON_OPTIONS;

private static final String TOKEN = "CENTER_SEPT";

static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer" + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}

@Bean
public RestHighLevelClient esRestClient() {
RestClientBuilder builder = RestClient.builder(new HttpHost("192.168.72.130", 9200,
"http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}

}
  1. 在测试类,加入以下代码,测试保存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@RunWith(SpringRunner.class)
@SpringBootTest
public class SuperMallElasticsearchApplicationTests {

@Autowired
RestHighLevelClient esClient;

/**
* 保存index到es中
*/
@Test
public void addIndex() throws IOException {
IndexRequest indexRequest = new IndexRequest("user");
indexRequest.id("1");
// 这是一种方式
// indexRequest.source("userName","chaoren","age","18","gender","男");
// 第二种方式
User user = new User();
user.setUserName("chaoren");
user.setAge(18);
user.setGender("男");
String string = JSON.toJSONString(user);
indexRequest.source(string, XContentType.JSON);
// 执行保存操作
IndexResponse index = esClient.index(indexRequest, ElasticSearchConfig.COMMON_OPTIONS);
System.out.println(index);
}


@Data
class User {
private String userName;
private String gender;
private Integer age;
}

@Test
public void contextLoads() {
System.out.println(esClient);
}

}
测试复杂检索

测试类加入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
   @Test
public void searchData() throws IOException {
// 创建索引请求
SearchRequest searchRequest = new SearchRequest();
// 指定索引
searchRequest.indices("bank");
// 指定DSL,索引条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构造检索条件
//searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
// 聚合
// 按照年龄的值分布进行聚合
searchSourceBuilder.aggregation(AggregationBuilders.terms("ageAgg").field("age").size(10));
// 计算平均薪资
searchSourceBuilder.aggregation(AggregationBuilders.avg("balanceAvg").field("balance"));
searchRequest.source(searchSourceBuilder);


// 执行检索
SearchResponse search = esClient.search(searchRequest, ElasticSearchConfig.COMMON_OPTIONS);
System.out.println(search);
// 分析结果
// 获取所有命中的记录
SearchHits hits = search.getHits();
SearchHit[] searchHit = hits.getHits();
for (SearchHit documentFields : searchHit) {
String sourceAsString = documentFields.getSourceAsString();
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println(account);
}
// 分析聚合信息
Aggregations aggregations = search.getAggregations();
// for (Aggregation aggregation : aggregations) {
// System.out.println("当前聚合:"+aggregation.getName());
// }
Terms ageAgg = aggregations.get("ageAgg");
List<? extends Terms.Bucket> buckets = ageAgg.getBuckets();
for (Terms.Bucket bucket : buckets) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:" + keyAsString);
}
Avg balanceAvg = aggregations.get("balanceAvg");
double value = balanceAvg.getValue();
System.out.println("平均薪资:" + value);
}

@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;
}

商城业务

商品上架

上架的商品才可以在网站上展示

上架的商品需要可以被检索

sku在es中存储模型分析

  1. 商城mapping
分析:商品上架在es中是存sku还是spu ?
  1. 检索的时候输入名字,是需要按照sku的title进行全文检索的。
  2. 检索使用商品规格,规格是spu的公共属性,每个spu是一样的
  3. 按照分类id进去的都是直接列出spu的,还可以切换
  4. 我们如果将sku的全量信息保存到es中(包括spu属性)就太多量字段
  5. 我们如果将spu以及他包含的sku信息保存到es中,也可以方便检索。但是sku属于spu的级联对象,在es中需要nested模型,这种性能差点。
  6. 但是存储与检索我们必须性能折中
  7. 如果我们拆分存储,spu和attr一个索引,sku单独一个索引可能涉及的问题

检索商品的名字,如“手机”,对应的spu有很多,我们要分析出这些spu的所有关联属性,在做一次查询,就必须将所有spu_id都发出去。假设有1万个数据,数据传输就一次就10000*4=4MB;并发情况下假设1000检索请求,那就是4GB的数据,传输阻塞时间会很长,业务无法继续。

所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能考虑数据库范式。

添加product索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "text"
},
"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"
}
}
}
}
}
}

nested数据类型场景

Es-数组的扁平化处理

image-20201218143304391

为了不让数组扁平化的发生,我们可以使用nested。

  1. 测试数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PUT my_index/_doc/1
{
"group": "fans",
"user": [
{
"first": "John",
"last": "Smith"
},
{
"first": "Alice",
"last": "White"
}
]
}
GET my_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user.first": "Alice"
}
},
{
"match": {
"user.last": "Smith"
}
}
]
}
}
}
GET my_index/_mapping
  1. 修改为嵌入式
1
2
3
4
5
6
7
8
9
10
11
12
13
DELETE my_index

PUT my_index
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
# 再执行上面的插入操作,使用数据进行对比

构造基本数据

  1. 添加上架业务

SpuInfoController.java

1
2
3
4
5
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}

SpuInfoServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 商品上架
*
* @param spuId
*/
@Override
public void up(Long spuId) {
// 组装需要数据
// 查出当前spuId对应的所有sku信息,品牌的名称
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
// todo attrs 查询当前sku的所有可以被用来检索的规格属性
// 封装每个sku的信息
skus.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
// skuPrice,skuImg
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
// hasStock,hotScore
// todo 1.发送远程调用,库存系统查询是否有库存
// todo 2.热度评分
// 3.查询品牌信息
// brandName,brandImg
BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
// catalogName
// 4.查询分类信息
CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
return skuEsModel;
}).collect(Collectors.toList());
// todo 将数据发送给es进行保存;
}

Common模块添加 SkuEsModel.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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;
private Long brandId;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;

@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}

SkuInfoServiceImpl.java

1
2
3
4
5
@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
List<SkuInfoEntity> list = this.list(new QueryWrapper<SkuInfoEntity>().eq("spu_id",spuId));
return list;
}

构造sku检索数据

  1. 查询当前sku的所有可以被用来检索的规格属性

1.1 SpuInfoServiceImpl 修改 up 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
        public void up(Long spuId) {
// 组装需要数据
// 查出当前spuId对应的所有sku信息,品牌的名称
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
// attrs 查询当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
// 过滤出不存在的规格属性,构建属性集合
List<SkuEsModel.Attrs> attrs = new ArrayList<>();
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs1);
return attrs1;
}).collect(Collectors.toList());
// 封装每个sku的信息
skus.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
// skuPrice,skuImg
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
// hasStock,hotScore
// todo 1.发送远程调用,库存系统查询是否有库存
// todo 2.热度评分,暂时定为零
skuEsModel.setHotScore(0L);
// 3.查询品牌信息
// brandName,brandImg
BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
// catalogName
// 4.查询分类信息
CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
// 设置检索属性
skuEsModel.setAttrs(attrsList);
return skuEsModel;
}).collect(Collectors.toList());
// todo 将数据发送给es进行保存;
}

1.2 添加根据attrId集合查询检索属性集合

AttrDao.java

1
List<Long> selectSearchAttrs(@Param("attrIds") List<Long> attrIds);

AttrDao.xml

1
2
3
4
5
6
7
<select id="selectSearchAttrs"  resultType="java.lang.Long">
select attr_id from pms_attr where attr_id in
<foreach collection="attrIds" item="id" separator="," open="(" close=")">
#{id}
</foreach>
and search_type = 1
</select>

AttrService.java

1
2
3
4
5
6
/**
* 在指定的所有属性集合里面,挑出检索属性
* @param attrIds
* @return
*/
List<Long> selectSearchAttrs(List<Long> attrIds);

AttrServiceImpl.java

1
2
3
4
public List<Long> selectSearchAttrs(List<Long> attrIds) {
List<Long> longs = this.baseMapper.selectSearchAttrs(attrIds);
return longs;
}

远程查询库存&泛型结果封装

  1. 远程查询库存

1.1 修改上架方法

SpuInfoServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Autowired
WareFeignService wareFeignService;

public void up(Long spuId) {
// 组装需要数据
// 查出当前spuId对应的所有sku信息,品牌的名称
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIds = skus.stream().map(item -> {
return item.getSkuId();
}).collect(Collectors.toList());
// attrs 查询当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
// 过滤出不存在的规格属性,构建属性集合
List<SkuEsModel.Attrs> attrs = new ArrayList<>();
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs1);
return attrs1;
}).collect(Collectors.toList());

// todo 1.发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try {
R<List<SkuHasStockVo>> skusHasStock = wareFeignService.getSkusHasStock(skuIds);
stockMap = skusHasStock.getData().stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId
, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}",e);
}

// 封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
skus.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
// skuPrice,skuImg
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
// hasStock,hotScore
// 判断是否有库存,然后设置库存信息
if (finalStockMap ==null) {
// 如果库存服务异常,则默认为有库存 todo
skuEsModel.setHasStock(true);
}else{
skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
// todo 2.热度评分,暂时定为零
skuEsModel.setHotScore(0L);
// 3.查询品牌信息
// brandName,brandImg
BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
// catalogName
// 4.查询分类信息
CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
// 设置检索属性
skuEsModel.setAttrs(attrsList);
return skuEsModel;
}).collect(Collectors.toList());
// todo 将数据发送给es进行保存;
}

1.2 Ware 模块检索业务

SkuHasStockVo.java Product和Ware模块同时建立这个VO

1
2
3
4
5
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}

WareSkuController.java

1
2
3
4
5
6
7
@PostMapping("/hasstock")
public R<List<SkuHasStockVo>> getSkusHasStock(@RequestBody List<Long> skuIds) {
List<SkuHasStockVo> skuHasStockVos = wareSkuService.getSkusHasStock(skuIds);
R<List<SkuHasStockVo>> ok = R.ok();
ok.setData(skuHasStockVos);
return ok;
}

WareSkuServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
List<SkuHasStockVo> result = skuIds.stream().map(item -> {
SkuHasStockVo vo = new SkuHasStockVo();
// 查询当前sku的总库存量
// select sum(stock-stock_locked) from `wms_ware_sku` where sku_id = 1
long count = baseMapper.getSkuStock(item);
vo.setSkuId(item);
vo.setHasStock(count > 0);
return vo;
}).collect(Collectors.toList());
return result;
}

WareSkuDao.java

1
long getSkuStock(Long item);

WareSkuDao.xml

1
2
3
<select id="getSkuStock" resultType="java.lang.Long" >
select sum(stock-stock_locked) from wms_ware_sku where sku_id = #{skuId}
</select>

1.3 Product 模块构建Ware服务的feign

WareFeignService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FeignClient("supermall-ware")
public interface WareFeignService {

/**
* 1.R设计的时候可以加上泛型
* 2.直接返回对应的类型
* 3.自己封装解析结果
*
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")
R<List<SkuHasStockVo>> getSkusHasStock(@RequestBody List<Long> skuIds);
}
  1. 泛型结果封装

增加R.java 中的泛型成员变量data;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class R<T> extends HashMap<String, Object> {

private static final long serialVersionUID = 1L;

private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public R() {
put("code", 0);
put("msg", "success");
}

public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}

public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}

public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}

public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}

public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}

public static R ok() {
return new R();
}

public R put(String key, Object value) {
super.put(key, value);
return this;
}

public Integer getCode() {
Integer code = (Integer) this.get("code");
return code;
}
}

远程上架接口

  1. 在elasticsearch模块新建商品上架信息保存到ES的业务逻辑

ElasticSearchSaveController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSearchSaveController {

@Autowired
ProductSaveService productSaveService;

@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
} catch (IOException e) {
log.error("ElasticSearchSaveController商品上架错误:{}", e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
// 成功
if (!b) {
return R.ok();
} else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}

ProductSaveService

1
2
3
public interface ProductSaveService {
boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException;
}

ProductSaveServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {

@Autowired
RestHighLevelClient restHighLevelClient;


@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
// 1.给es中建立一个索引。product.
// 建立好映射关系product-mapping.txt
// 2.保存到es中
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
// 赋值索引
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
// 赋值id
indexRequest.id(skuEsModel.getSkuId().toString());
// 设置数据
String data = JSON.toJSONString(skuEsModel);
indexRequest.source(data, XContentType.JSON);
// 添加到批量操作中
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ElasticSearchConfig.COMMON_OPTIONS);
// 处理响应结果
// todo 如果批量错误
boolean b = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.error("商品上架完成:{}", collect);
return b;
}
}

EsConstant 常量

1
2
3
4
public class EsConstant {
// sku数据在ES中的索引
public static final String PRODUCT_INDEX = "product";
}

增加Product 上架异常错误编码

BizCodeEnume

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000, "系统未知异常"),
VAILD_EXCEPTION(10001, "参数格式校验失败"),
PRODUCT_UP_EXCEPTION(11000, "商品上架异常");

private int code;
private String msg;

BizCodeEnume(int code, String msg) {
this.code = code;
this.msg = msg;
}

public int getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

在resource 下新建 product-mapping.txt,保存新建索引内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "text"
},
"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"
}
}
}
}
}
}
  1. 回到product模块,新建远程调用类

SearchFeignService

1
2
3
4
5
6
@FeignClient("supermall-elasticsearch")
public interface SearchFeignService {

@PostMapping("/search/save/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
  1. 修改上架逻辑

SpuInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Autowired
SearchFeignService searchFeignService;

public void up(Long spuId) {
// 组装需要数据
// 查出当前spuId对应的所有sku信息,品牌的名称
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIds = skus.stream().map(item -> {
return item.getSkuId();
}).collect(Collectors.toList());
// attrs 查询当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
// 过滤出不存在的规格属性,构建属性集合
List<SkuEsModel.Attrs> attrs = new ArrayList<>();
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs1);
return attrs1;
}).collect(Collectors.toList());

// 1.发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try {
R<List<SkuHasStockVo>> skusHasStock = wareFeignService.getSkusHasStock(skuIds);
stockMap = skusHasStock.getData().stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId
, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}",e);
}

// 封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> skuEsModels = skus.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
// skuPrice,skuImg
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
// hasStock,hotScore
// 判断是否有库存,然后设置库存信息
if (finalStockMap == null) {
// 如果库存服务异常,则默认为有库存 todo
skuEsModel.setHasStock(true);
} else {
skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
// todo 2.热度评分,暂时定为零
skuEsModel.setHotScore(0L);
// 3.查询品牌信息
// brandName,brandImg
BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
// catalogName
// 4.查询分类信息
CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
// 设置检索属性
skuEsModel.setAttrs(attrsList);
return skuEsModel;
}).collect(Collectors.toList());
// todo 将数据发送给es进行保存;
R r = searchFeignService.productStatusUp(skuEsModels);
if (r.getCode()==0) {
// 远程调用成功
// 修改当前Spu的上架状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else{
// 调用失败
// todo 重复调用,接口的幂等
}
}

SpuInfoDao

1
void updateSpuStatus(@Param("spuId") Long spuId, @Param("code") int code);

SpuInfoDao.xml

1
2
3
<update id="updateSpuStatus">
UPDATE pms_spu_info set public_status=#{code},update_time = NOW() where id = #{spuId}
</update>

上架接口调试&feign源码

  1. WareSkuServiceImpl 修改 getSkusHasStock 方法
1
2
3
4
5
6
7
8
9
10
11
12
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
List<SkuHasStockVo> result = skuIds.stream().map(item -> {
SkuHasStockVo vo = new SkuHasStockVo();
// 查询当前sku的总库存量
// select sum(stock-stock_locked) from `wms_ware_sku` where sku_id = 1
Long count = baseMapper.getSkuStock(item);
vo.setSkuId(item);
vo.setHasStock(count == null ? false : count > 0);
return vo;
}).collect(Collectors.toList());
return result;
}

WareSkuDao

1
Long getSkuStock(Long item);

WareFeignService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FeignClient("supermall-ware")
public interface WareFeignService {

/**
* 1.R设计的时候可以加上泛型
* 2.直接返回对应的类型
* 3.自己封装解析结果
*
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")
List<SkuHasStockVo> getSkusHasStock(@RequestBody List<Long> skuIds);
}

WareSkuController

1
2
3
4
5
@PostMapping("/hasstock")
public List<SkuHasStockVo> getSkusHasStock(@RequestBody List<Long> skuIds) {
List<SkuHasStockVo> skuHasStockVos = wareSkuService.getSkusHasStock(skuIds);
return skuHasStockVos;
}

抽取响应结果&上架测试完成

  1. R继承了hashMap 在里面定义泛型成员变量,没有效果?

R.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class R extends HashMap<String, Object> {

private static final long serialVersionUID = 1L;

public R setData(Object data) {
put("data", data);
return this;
}

public <T> T getData(TypeReference<T> tTypeReference) {
return this.getData("data", tTypeReference);
}

public <T> T getData(String key, TypeReference<T> tTypeReference) {
Object data = this.get(key);
String toJSONString = JSON.toJSONString(data);
T t = JSON.parseObject(toJSONString, tTypeReference);
return t;
}

public R() {
put("code", 0);
put("msg", "success");
}

public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}

public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}

public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}

public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}

public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}

public static R ok() {
return new R();
}

public R put(String key, Object value) {
super.put(key, value);
return this;
}

public Integer getCode() {
Integer code = (Integer) this.get("code");
return code;
}
}

使用alibaba.json 快速转换,SpuInfoServiceImpl中的up

1
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>(){};       skuHasStocks.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));

首页

整合thymeleaf渲染首页

动静分离架构,网关更好的做鉴权

动静分离:

静:图片,js,css等静态资源(以实际文件存在的方式)

动:服务器需要处理的请求

image-20201222150442692

  1. 在Product导入thymeleaf 模板引擎
1
2
3
4
5
<!--   thymeleaf 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. 导入商城静态资源到resouce/static下,页面拷贝到template文件夹下;要看默认配置,查看WebMvcAutoConfiguration.java类。
  2. 在配置文件加入以下配置,暂时不是用缓存,让页面实时更新。
1
2
3
spring:
thymeleaf:
cache: false
  1. 创建一个web包,要来处理页面请求

整合dev-tools渲染一级分类数据

  1. 创建首页请求相应类IndexController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class IndexController {

@Autowired
CategoryService categoryService;

@GetMapping({"/", "index.html"})
public String indexPage(Model model) {
//获取一级分类
List<CategoryEntity> list = categoryService.getLevelFirstCategorys();
model.addAttribute("categorys", list);
// 视图解析器自动拼接前后缀
// classpath:templates/ + index + .html
return "index";
}
}
  1. 新建查询一级分类方法
1
2
3
4
public List<CategoryEntity> getLevelFirstCategorys() {
List<CategoryEntity> list = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return list;
}
  1. 声明 thymeleaf 语法到html中
1
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  1. 使用thymeleaf语法渲染一级分类
1
2
3
4
5
6
7
<div class="header_main_left">
<ul>
<li th:each=" category : ${categorys}">
<a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}"></b></a>
</li>
</ul>
</div>
  1. 修改页面,无需重启服务器实时更新页面

5.1 需要引入dev-tool 依赖,product pom 中加入以下依赖

1
2
3
4
5
6
7
<!--   dev-tool-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- true 为开启 -->
<optional>true</optional>
</dependency>

5.2 页面修改完,build项目,就相当于发布完成。

渲染二级和三级分类数据

  1. 由于菜单渲染是根据catalogLoader.js这个文件请求 index/json/catalog.json来获取resouce下的静态文件catalog.json中的数据的。所以我们需要读取数据库中配置的动态数据作为渲染数据。
  2. 在后端做二三级渲染

Catelog2Vo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
private String catalog1Id;
private List<Object> catalog3List;
private String id;
private String name;


@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Catelog3Vo{
private String catalog2Id;
private String id;
private String name;
}
}

CategoryServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 查出所有一级分类
List<CategoryEntity> levelFirstCategorys = getLevelFirstCategorys();
Map<String, List<Catelog2Vo>> collect = levelFirstCategorys.stream().collect(Collectors.toMap(k -> {
return k.getCatId().toString();
}, lv1 -> {
// 每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> list2 = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",
lv1.getCatId()));
List<Catelog2Vo> collect2 = null;
if (list2 != null) {
collect2 = list2.stream().map(lv2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(lv1.getCatId().toString(), null, lv2.getCatId().toString()
, lv2.getName());
List<CategoryEntity> list3 = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq(
"parent_cid",
lv2.getCatId()));
if (list3 != null) {
List<Catelog2Vo.Catelog3Vo> collect3 = list3.stream().map(lv3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(lv2.getCatId().toString(),
lv3.getCatId().toString(), lv3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect3);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return collect2;
}));
return collect;
}

IndexController

1
2
3
4
5
6
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatalogJson() {
Map<String, List<Catelog2Vo>> result = categoryService.getCatalogJson();
return result;
}

修改catalogLoader.js 请求路径

1
$.getJSON("index/catalog.json",function (data) 

nginx

搭建域名访问环境一(反向代理配置)

正向代理与反向代理

image-20201223095121108

  1. 使用SwitchHosts直接可以修改hosts配置文件,并提供添加修改方法策略。
1
192.168.72.130 supermall.com
  1. 启动虚拟机nginx

2.1 在挂载的conf.d文件夹下创建一个配置conf,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#http服务,一个server可以配置多个location
server {
listen 80; #服务监听端口
server_name supermall.com; #主机名、域名

#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;

location / {
proxy_pass http://{这里为你的主机的网关地址加Product模块端口号};
}

#error_page 404 /404.html;

# 将500 502 503 504的错误页面重定向到 /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html { #匹配error_page指定的页面路径
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;
#}
}

2.2 在你的主机启动项目,访问在hosts中配置好的映射域名进行访问,我的是supermall.com

搭建域名访问环境二(负载均衡到网关)

  1. 新增负载均衡 upstream

1.1 在nginx.conf 中加入以下负载均衡配置:

1
2
3
upstream supermall {
server 192.168.72.1:88;
}

1.2 在supermall.conf 中替换内容:

注:因为nginx会对请求做丢弃处理,所以为了让请求头中host属性不会被丢弃而造成请求达到网关服务时候匹配不到服务,所以我们需要加上proxy_set header Host $host

1
2
3
4
location / {
proxy_set header Host $host;
proxy_pass http://supermall;
}
  1. 配置网关服务,加入页面请求断言
1
2
3
4
- id: super_mall_host_route
uri: lb://supermall-product
predicates:
- Host=**.supermall.com,supermall.com

性能压测

压力测试

基本介绍

压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。

使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是:

内存泄漏,并发与同步

有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。

  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 Response Time):指用户发出请求或者指令到系统做出反应(响应)的最少时间。

90%响应时间(90% Response Time):是指所有用户的响应时间进行排序,第90%的响应时间。

从外部看,性能测试主要关注如下三个指标:

​ 吞吐量:每秒钟系统能够处理的请求数,任务数。

​ 响应时间:服务处理一个请求或一个任务的耗时。

​ 错误率:一批请求中结果出错的请求所占比例。

Apache JMeter安装使用

  1. JMeter 安装

https://jmeter.apache.org/download_jmeter.cgi

下载对应的压缩包,解压运行jmeter.bat 即可

  1. JMeter的使用(多用用)

  2. 压测调优

3.1 加大JVM内存

3.2 加大tomcat的最大线程数

1
server.tomcat.max-threads=200

JMeter在windows下地址占用bug解决

JMeter Address Already in user 错误解决

windows本身提供的端口访问机制的问题。

Windows 提供给TCP/IP链接的端口为1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。

解决方案:

  1. cmd中,用regedit命令打开注册表
  2. 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Service\Tcpip\Parameters 下,
    1. 右击parameters,添加一个新的DWORD,名字为MaxUserPort
    2. 然后点击MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作)
  3. 修改配置完毕后重启机器
  4. 按照以上步骤再入TCPTimedWaitDelay:30

性能监控

堆内存与垃圾回收

影响性能考虑点包括:

​ 数据库,应用程序,中间件(tomcat,nginx),网络和操作系统等方面

首先考虑自己的应用属于 CPU密集型 还是 IO密集型

jconsole与jvisualvm

​ Jdk的两个小工具jconsole,jvisualvm(升级版jconsole);通过命令行启动,可监控本地和远程应用。远程应用需要配置。

在windows下 cmd 下,执行命令 jconsole 或者 jvisualvm 就可以打开。

jvisualvm能干什么?

监控内存泄露,跟踪垃圾回收,执行时内存,cpu分析,线程分析

运行:正在运行

休眠:sleep

等待:wait

驻留:线程池里面的空闲线程

监视:阻塞的线程,正在等待锁

安装插件方便查看GC
  1. 启动jvisualvm,工具->插件
  2. 如果503错误解决:

2.1 打开网址https://visualvm.github.io/pluginscenters.html

2.2 cmd 查看自己的的jdk版本,找到对应的版本,然后复制网页上的Catalog URL

2.3 然后粘贴到更新插件中心的URL中,最后可以使用插件中心了,下载Vusual GC 插件可以查看动态的GC。

优化

中间件对性能的影响

  1. 使用JMeter,50线程不断请求nginx,然后使用docker stats 命令查看nginx容器的状态。

  2. 压测网关,使用JvisualVM查看cpu和GC状态

  3. 使用一个普通的业务模块压测,使用JvisualVM查看cpu和GC状态。

  4. 网关加简单业务服务,使用JvisualVM查看cpu和GC状态。

压测内容 压测线程数 吞吐量/s 90%响应时间 99%响应时间
Nginx 50 5152 11 16
Gateway 50 4986 1 4
简单服务 50 5247 1 3
首页一级菜单渲染(thymeleaf缓存) 50 290
首页一级菜单渲染 50 515 51 79
首页一级菜单渲染(thymeleaf缓存+db索引+取消日志) 50 270 267 365
三级分类数据获取 50 15 3690 4007
首页全量数据获取 50 7 (静态资源)
Nginx+Gateway 50
Gateway+简单服务 50 2675 5 12
全链路 50 1225 34 45

结论:中间件越多,性能损耗越大,大多都损失在网络交互。

优化:

SQL耗时越小越好,一般情况下微秒级别。

命中率越高越好,一般情况下不能低于95%

锁等待次数越低越好,等待时间越短越好。

简单优化吞吐量测试

  1. 压测首页一级菜单

  2. 直接压测三级菜单

  3. 压测首页全量数据(图片,js等静态数据)

JMeter http取样器中选择高级,勾选”从HTML文件中获取所有的资源”

  1. 首页一级菜单渲染(thymeleaf缓存+db索引+取消日志)
  2. 首页一级菜单渲染(thymeleaf缓存)
  3. 优化之后的全量数据

nginx动静分离

  1. 将所有项目的静态资源都nginx里面
  2. 指定一个规则:/static/***下的所有请求都有nginx直接返回
  3. 在nginx的html下创建static文件夹专门存放静态资源
  4. 然后修改index.html中的所有静态资源路径,在路径前加上static或/static

href=” 替换href=”/static/