全文检索 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: method:put body: { "name" : "Tom" } response: { "_index" : "customer" , "_type" : "external" , "_id" : "1" , "_version" : 4 , "result" : "updated" , "_shards" : { "total" : 2 , "successful" : 1 , "failed" : 0 } , "_seq_no" : 3 , "_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: 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" : "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支持两种基本方式检索:
uri+检索参数
GET bank/_search
检索bank下所有信息,包括type的和docs
GET bank/_search?q=*&sort=account_number:asc
请求参数方式检索
默认返回10条记录
结果解析
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 2 3 4 5 6 7 8 9 10 11 GET bank/_search { "query": { "term": { "age": { "value": 28 } } } }
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 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 } } }
修改映射&数据迁移 更新映射 对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移。
数据迁移
查询之前的mapping数据,复制properties 中的数据
创建新的索引映射
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 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 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": ["我是中国人"] }
自定义扩展词库 为何需要自定义扩展词库? 有一些词汇,分词器中没有对应的,需要我们自己去添加到词库,然后分词器才能够识别。
自定义词库
使用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客户端
spring-data-elasticsearch-transport-api.jar (不兼容7.x,8以后废弃)
通过9300(TCP)端口操作
以下通过9200 Http请求交互
JestClient
非官方,更新慢
RestTemplate
模拟发http请求,ES很多操作需要自己封装,麻烦
httpClient
模拟发http请求,ES很多操作需要自己封装,麻烦
Elasticsearch-Rest-Client
官方RestClient,封装了ES操作,API层次分明,上手简单。
引入Elasticsearch-Rest-Client
引入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 >
修改SpringBoot 默认版本,由于spring-boot-dependencies 中elasticsearch默认版本为<elasticsearch.version>6.4.3</elasticsearch.version>
1 <elasticsearch.version>7.4.2</elasticsearch.version>
加入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 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); } }
测试保存
在配置类加上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(); 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 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; @Test public void addIndex () throws IOException { IndexRequest indexRequest = new IndexRequest ("user" ); indexRequest.id("1" ); 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" ); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder (); 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(); 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中存储模型分析
商城mapping
分析:商品上架在es中是存sku还是spu ?
检索的时候输入名字,是需要按照sku的title进行全文检索的。
检索使用商品规格,规格是spu的公共属性,每个spu是一样的
按照分类id进去的都是直接列出spu的,还可以切换
我们如果将sku的全量信息保存到es中(包括spu属性)就太多量字段
我们如果将spu以及他包含的sku信息保存到es中,也可以方便检索。但是sku属于spu的级联对象,在es中需要nested模型,这种性能差点。
但是存储与检索我们必须性能折中
如果我们拆分存储,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-数组的扁平化处理
为了不让数组扁平化的发生,我们可以使用nested。
测试数据
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 2 3 4 5 6 7 8 9 10 11 12 13 DELETE my_index PUT my_index { "mappings": { "properties": { "user": { "type": "nested" } } } } # 再执行上面的插入操作,使用数据进行对比
构造基本数据
添加上架业务
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检索数据
查询当前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) { List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId); 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()); skus.stream().map(sku -> { SkuEsModel skuEsModel = new SkuEsModel (); BeanUtils.copyProperties(sku, skuEsModel); skuEsModel.setSkuPrice(sku.getPrice()); skuEsModel.setSkuImg(sku.getSkuDefaultImg()); skuEsModel.setHotScore(0L ); BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId()); skuEsModel.setBrandName(brandEntity.getName()); skuEsModel.setBrandImg(brandEntity.getLogo()); CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId()); skuEsModel.setCatalogName(categoryEntity.getName()); skuEsModel.setAttrs(attrsList); return skuEsModel; }).collect(Collectors.toList()); }
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 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 修改上架方法
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) { List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId); List<Long> skuIds = skus.stream().map(item -> { return item.getSkuId(); }).collect(Collectors.toList()); 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()); 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); } Map<Long, Boolean> finalStockMap = stockMap; skus.stream().map(sku -> { SkuEsModel skuEsModel = new SkuEsModel (); BeanUtils.copyProperties(sku, skuEsModel); skuEsModel.setSkuPrice(sku.getPrice()); skuEsModel.setSkuImg(sku.getSkuDefaultImg()); if (finalStockMap ==null ) { skuEsModel.setHasStock(true ); }else { skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId())); } skuEsModel.setHotScore(0L ); BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId()); skuEsModel.setBrandName(brandEntity.getName()); skuEsModel.setBrandImg(brandEntity.getLogo()); CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId()); skuEsModel.setCatalogName(categoryEntity.getName()); skuEsModel.setAttrs(attrsList); return skuEsModel; }).collect(Collectors.toList()); }
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 (); 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 { @PostMapping("/ware/waresku/hasstock") R<List<SkuHasStockVo>> getSkusHasStock (@RequestBody List<Long> skuIds) ; }
泛型结果封装
增加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; } }
远程上架接口
在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 { BulkRequest bulkRequest = new BulkRequest (); for (SkuEsModel skuEsModel : skuEsModels) { IndexRequest indexRequest = new IndexRequest (EsConstant.PRODUCT_INDEX); 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); 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 { 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" } } } } } }
回到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) ; }
修改上架逻辑
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) { List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId); List<Long> skuIds = skus.stream().map(item -> { return item.getSkuId(); }).collect(Collectors.toList()); 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()); 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); } Map<Long, Boolean> finalStockMap = stockMap; List<SkuEsModel> skuEsModels = skus.stream().map(sku -> { SkuEsModel skuEsModel = new SkuEsModel (); BeanUtils.copyProperties(sku, skuEsModel); skuEsModel.setSkuPrice(sku.getPrice()); skuEsModel.setSkuImg(sku.getSkuDefaultImg()); if (finalStockMap == null ) { skuEsModel.setHasStock(true ); } else { skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId())); } skuEsModel.setHotScore(0L ); BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId()); skuEsModel.setBrandName(brandEntity.getName()); skuEsModel.setBrandImg(brandEntity.getLogo()); CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId()); skuEsModel.setCatalogName(categoryEntity.getName()); skuEsModel.setAttrs(attrsList); return skuEsModel; }).collect(Collectors.toList()); R r = searchFeignService.productStatusUp(skuEsModels); if (r.getCode()==0 ) { baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode()); }else { } }
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源码
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 (); 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 { @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; }
抽取响应结果&上架测试完成
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等静态资源(以实际文件存在的方式)
动:服务器需要处理的请求
在Product导入thymeleaf 模板引擎
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
导入商城静态资源到resouce/static下,页面拷贝到template文件夹下;要看默认配置,查看WebMvcAutoConfiguration.java类。
在配置文件加入以下配置,暂时不是用缓存,让页面实时更新。
1 2 3 spring: thymeleaf: cache: false
创建一个web包,要来处理页面请求
创建首页请求相应类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); return "index" ; } }
新建查询一级分类方法
1 2 3 4 public List<CategoryEntity> getLevelFirstCategorys () { List<CategoryEntity> list = baseMapper.selectList(new QueryWrapper <CategoryEntity>().eq("parent_cid" , 0 )); return list; }
声明 thymeleaf 语法到html中
1 <html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
使用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 >
修改页面,无需重启服务器实时更新页面
5.1 需要引入dev-tool 依赖,product pom 中加入以下依赖
1 2 3 4 5 6 7 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <optional > true</optional > </dependency >
5.2 页面修改完,build项目,就相当于发布完成。
渲染二级和三级分类数据
由于菜单渲染是根据catalogLoader.js这个文件请求 index/json/catalog.json来获取resouce下的静态文件catalog.json中的数据的。所以我们需要读取数据库中配置的动态数据作为渲染数据。
在后端做二三级渲染
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 搭建域名访问环境一(反向代理配置) 正向代理与反向代理
使用SwitchHosts直接可以修改hosts配置文件,并提供添加修改方法策略。
1 192.168.72.130 supermall.com
启动虚拟机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
搭建域名访问环境二(负载均衡到网关)
新增负载均衡 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 2 3 4 - id: super_mall_host_route uri: lb://supermall-product predicates: - Host=**.supermall.com,supermall.com
性能压测 压力测试 基本介绍 压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是:
内存泄漏,并发与同步
有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。
性能指标
响应时间(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安装使用
JMeter 安装
https://jmeter.apache.org/download_jmeter.cgi
下载对应的压缩包,解压运行jmeter.bat 即可
JMeter的使用(多用用)
压测调优
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,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。
解决方案:
cmd中,用regedit命令打开注册表
在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Service\Tcpip\Parameters 下,
右击parameters,添加一个新的DWORD,名字为MaxUserPort
然后点击MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作)
修改配置完毕后重启机器
按照以上步骤再入TCPTimedWaitDelay:30
性能监控 堆内存与垃圾回收 影响性能考虑点包括:
数据库,应用程序,中间件(tomcat,nginx),网络和操作系统等方面
首先考虑自己的应用属于 CPU密集型 还是 IO密集型
jconsole与jvisualvm Jdk的两个小工具jconsole,jvisualvm(升级版jconsole);通过命令行启动,可监控本地和远程应用。远程应用需要配置。
在windows下 cmd 下,执行命令 jconsole 或者 jvisualvm 就可以打开。
jvisualvm能干什么? 监控内存泄露,跟踪垃圾回收,执行时内存,cpu分析,线程分析
运行:正在运行
休眠:sleep
等待:wait
驻留:线程池里面的空闲线程
监视:阻塞的线程,正在等待锁
安装插件方便查看GC
启动jvisualvm,工具->插件
如果503错误解决:
2.1 打开网址https://visualvm.github.io/pluginscenters.html
2.2 cmd 查看自己的的jdk版本,找到对应的版本,然后复制网页上的Catalog URL
2.3 然后粘贴到更新插件中心的URL中,最后可以使用插件中心了,下载Vusual GC 插件可以查看动态的GC。
优化 中间件对性能的影响
使用JMeter,50线程不断请求nginx,然后使用docker stats 命令查看nginx容器的状态。
压测网关,使用JvisualVM查看cpu和GC状态
使用一个普通的业务模块压测,使用JvisualVM查看cpu和GC状态。
网关加简单业务服务,使用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%
锁等待次数越低越好,等待时间越短越好。
简单优化吞吐量测试
压测首页一级菜单
直接压测三级菜单
压测首页全量数据(图片,js等静态数据)
JMeter http取样器中选择高级,勾选”从HTML文件中获取所有的资源”
首页一级菜单渲染(thymeleaf缓存+db索引+取消日志)
首页一级菜单渲染(thymeleaf缓存)
优化之后的全量数据
nginx动静分离
将所有项目的静态资源都nginx里面
指定一个规则:/static/***下的所有请求都有nginx直接返回
在nginx的html下创建static文件夹专门存放静态资源
然后修改index.html中的所有静态资源路径,在路径前加上static或/static
href=” 替换href=”/static/