一、 项目整合redis
1 思路:
虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能需要优化。
一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。
一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。
提高数据库本身的性能首先是优化sql,包括:使用索引,减少不必要的大表关联次数,控制查询字段的行数和列数。另外当数据量巨大是可以考虑分库分表,以减轻单点压力。
这部分知识在mysql高级已有讲解,这里大家可以以详情页中的sql作为练习,尝试进行优化,这里不做赘述。
重点要讲的是另外一个层面:尽量避免直接查询数据库。
解决办法就是:缓存
缓存可以理解是数据库的一道保护伞,任何请求只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。
咱们就用Redis作为缓存系统进行优化。
结构图:
多级缓存应用:
安装Redis :
- 安装依赖
- 导入jar包
- 创建安装目录
- Make
- Make install
- 修改配置文件
2 整合redis到大工程中。
由于redis作为缓存数据库,要被多个项目使用,所以要制作一个通用的工具类,方便工程中的各个模块使用。
而主要使用redis的模块,都是后台服务的模块,xxx-service工程。所以咱们把redis的工具类放到service-util模块中,这样所有的后台服务模块都可以使用redis。
首先引入依赖包
<!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> |
分别按照之前的方式放到parent模块和service-util的pom文件中。
然后在service-util中创建两个类RedisConfig和RedisUtil
RedisConfig负责在spring容器启动时自动注入,而RedisUtil就是被注入的工具类以供其他模块调用。
Spring boot 项目 推荐使用注解方式来完成配置。
RedisUtil
package com.test.gmall.config; public class RedisUtil { // 创建连接池
|
RedisConfig
@Configuration 相当于spring3.0版本的xml
@Configuration |
同时,任何模块想要调用redis都必须在application.properties配置,否则不会进行注入。
spring.redis.host=192.168.67.204 |
现在可以在manage-service中的getSkuInfo()方法测试一下
try { |
在启动类上加上@ComonentScan注解 |
3 使用redis进行业务开发
开始开发先说明redis key的命名规范,由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。
企业中最常用的方式就是:object:id:field
比如:sku:1314:info
user:1092:info
:表示根据windows的 /一个意思
重构getSkuInfo方法
在gmall-manage-service项目中定义常量
package com.test.gmall.manage.constant; |
@Override // 将查询出来所有图片赋予对象 |
以上基本实现使用缓存的方案。
如果说:redis服务没有启动,宕机!如何解决?
Try-catch 获取mysql数据返回
如果说:访问redis的时候,可能会有产生高并发,如何解决这种高并发访问?
使用分布式锁!
set test ok px 10000 nx
4 解决缓存击穿问题:
Redis:命令
# set sku:1:info “OK” NX PX 10000
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
public static final int SKULOCK_EXPIRE_PX=10000; |
@Override |
5 redisson 解决分布式锁
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.1</version> </dependency> |
private SkuInfo getSkuInfoRedisson(String skuId) { |
- 搜索
什么是搜索, 计算机根据用户输入的关键词进行匹配,从已有的数据库中摘录出相关的记录反馈给用户。
常见的全文搜索引擎,像百度、谷歌这样的。但是除此以外,搜索技术在垂直领域也有广泛的使用,比如淘宝、京东搜索商品,万芳、知网搜索期刊,csdn中搜索问题贴。也都是基于海量数据的搜索。
Elasticsearch:全文检索
Solr:本质web项目 xxx.war
SolrCloud简介 – solr集群。
二、如何处理搜索
1.1 用传统关系性数据库
弊端:
1、 对于传统的关系性数据库对于关键词的查询,只能逐字逐行的匹配,性能非常差。
2、匹配方式不合理,比如搜索“小密手机” ,如果用like进行匹配, 根本匹配不到。但是考虑使用者的用户体验的话,除了完全匹配的记录,还应该显示一部分近似匹配的记录,至少应该匹配到“手机”。
1.2 专业全文索引是怎么处理的
全文搜索引擎目前主流的索引技术就是倒排索引的方式。
传统的保存数据的方式都是
记录→单词
而倒排索引的保存数据的方式是
单词→记录
例如
搜索“红海行动”
但是数据库中保存的数据如图:
那么搜索引擎是如何能将两者匹配上的呢?
基于分词技术构建倒排索引:
首先每个记录保存数据时,都不会直接存入数据库。系统先会对数据进行分词,然后以倒排索引结构保存。如下:
然后等到用户搜索的时候,会把搜索的关键词也进行分词,会把“红海行动”分词分成:红海和行动两个词。
这样的话,先用红海进行匹配,得到id=1和id=2的记录编号,再用行动匹配可以迅速定位id为1,3的记录。
那么全文索引通常,还会根据匹配程度进行打分,显然1号记录能匹配的次数更多。所以显示的时候以评分进行排序的话,1号记录会排到最前面。而2、3号记录也可以匹配到。
1.3 首页搭建
在nginx 目录下新建文件夹front
然后将 首页资源统统放入进去,然后nginx.conf 配置添加配置
server { listen 80; server_name www.gmall.com; location / { root front; index index.htm; } } =========================item.gmall.com===================================== upstream item.gmall.com{ server 192.168.67.1:8084; } server { listen 80; server_name item.gmall.com; location / { proxy_pass http://item.gmall.com; } } |
三、 全文检索工具elasticsearch
如果es打不开:卸掉,把一些不相关tar.gz 删掉!
1 lucene与elasticsearch
咱们之前讲的处理分词,构建倒排索引,等等,都是这个叫lucene的做的。那么能不能说这个lucene就是搜索引擎呢?
还不能。lucene只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来的应用。
好比lucene是类似于jdk,而搜索引擎软件就是tomcat 的。
目前市面上流行的搜索引擎软件,主流的就两款,elasticsearch和solr,这两款都是基于lucene的搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作,修改、添加、保存、查询等等都十分类似。就好像都是支持sql语言的两种数据库软件。只要学会其中一个另一个很容易上手。
从实际企业使用情况来看,elasticSearch的市场份额逐步在取代solr,国内百度、京东、新浪都是基于elasticSearch实现的搜索功能。国外就更多了 像维基百科、GitHub、Stack Overflow等等也都是基于ES的
2 elasticSearch的使用场景
- 为用户提供按关键字查询的全文搜索功能。
- 著名的ELK框架(ElasticSearch,Logstash,Kibana),实现企业海量日志的处理分析的解决方案。大数据领域的重要一份子。
3 elasticSearch的安装
详见《elasticSearch的安装手册》
4 elasticsearch的基本概念
cluster | 整个elasticsearch 默认就是集群状态,整个集群是一份完整、互备的数据。 |
node | 集群中的一个节点,一般只一个进程就是一个node |
Shard | 分片,即使是一个节点中的数据也会通过hash算法,分成多个片存放,默认是5片。 |
Index | 相当于rdbms的database, 对于用户来说是一个逻辑数据库,虽然物理上会被分多个shard存放,也可能存放在多个node中。 |
Type | 类似于rdbms的table,但是与其说像table,其实更像面向对象中的class , 同一Json的格式的数据集合。 |
Document | 类似于rdbms的 row、面向对象里的object |
Field | 相当于字段、属性 |
5 利用kibana学习 elasticsearch restful api (DSL)
5.1 es中保存的数据结构
public class Movie { String id; String name; Double doubanScore; List<Actor> actorList; } public class Actor{ String id; String name; } |
这两个对象如果放在关系型数据库保存,会被拆成2张表,但是elasticsearch是用一个json来表示一个document。
所以它保存到es中应该是:
{ “id”:”1”, “name”:”operation red sea”, “doubanScore”:”8.5”, “actorList”:[ {“id”:”1”,”name”:”zhangyi”}, {“id”:”2”,”name”:”haiqing”}, {“id”:”3”,”name”:”zhanghanyu”} ] } |
5.2 对数据的操作
5.2.1 查看es中有哪些索引
GET /_cat/indices?v |
es 中会默认存在一个名为.kibana的索引
表头的含义
health | green(集群完整) yellow(单点正常、集群不完整) red(单点不正常) |
status | 是否能使用 |
index | 索引名 |
uuid | 索引统一编号 |
pri | 主节点几个 |
rep | 从节点几个 |
docs.count | 文档数 |
docs.deleted | 文档被删了多少 |
store.size | 整体占空间大小 |
pri.store.size | 主节点占 |
5.2.2 增加一个索引
PUT /movie_index |
5.2.3 删除一个索引
ES 是不删除也不修改任何数据 ,伪删除更新当前index的版本。
DELETE /movie_index |
5.2.4 新增文档
- 格式 PUT /index/type/id
PUT /movie_index/movie/1 { "id":1, "name":"operation red sea", "doubanScore":8.5, "actorList":[ {"id":1,"name":"zhang yi"}, {"id":2,"name":"hai qing"}, {"id":3,"name":"zhang han yu"} ] } PUT /movie_index/movie/2 { "id":2, "name":"operation meigong river", "doubanScore":8.0, "actorList":[ {"id":3,"name":"zhang han yu"} ] } PUT /movie_index/movie/3 { "id":3, "name":"incident red sea", "doubanScore":5.0, "actorList":[ {"id":4,"name":"liu de hua"} ] } |
如果之前没建过index或者type,es 会自动创建。
5.2.5 直接用id查找
GET movie_index/movie/1 |
5.2.6 修改—整体替换
和新增没有区别
PUT /movie_index/movie/3 { "id":"3", "name":"incident red sea", "doubanScore":"5.0", "actorList":[ {"id":"1","name":"zhang guo li"} ] } |
5.2.7 修改—某个字段 更新es商品中的排名
POST movie_index/movie/3/_update { "doc": { "doubanScore":"7.0" } } |
5.2.8 删除一个document
DELETE movie_index/movie/3 |
5.2.9 搜索type全部数据
GET movie_index/movie/_search |
结果
{ "took": 2, //耗费时间 毫秒 "timed_out": false, //是否超时 "_shards": { "total": 5, //发送给全部5个分片 "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 3, //命中3条数据 "max_score": 1, //最大评分 "hits": [ // 结果 { "_index": "movie_index", "_type": "movie", "_id": 2, "_score": 1, "_source": { "id": "2", "name": "operation meigong river", "doubanScore": 8.0, "actorList": [ { "id": "1", "name": "zhang han yu" } ] } 。。。。。。。。 。。。。。。。。 } |
5.2.10 按条件查询(全部)
GET movie_index/movie/_search { "query":{ "match_all": {} } } |
5.2.11 按分词查询
GET movie_index/movie/_search { "query":{ "match": {"name":"red"} } } |
注意结果的评分
5.2.12 按分词子属性查询
GET movie_index/movie/_search { "query":{ "match": {"actorList.name":"zhang"} } } |
5.2.13 match phrase 按词组查询
GET movie_index/movie/_search { "query":{ "match_phrase": {"name":"operation red"} } } |
按短语查询,不再利用分词技术,直接用短语在原始数据中匹配
5.2.14 fuzzy查询
GET movie_index/movie/_search { "query":{ "fuzzy": {"name":"rad"} } } |
校正匹配分词,当一个单词都无法准确匹配,es通过一种算法对非常接近的单词也给与一定的评分,能够查询出来,但是消耗更多的性能。
5.2.15 过滤--查询后过滤
GET movie_index/movie/_search { "query":{ "match": {"name":"red"} }, "post_filter":{ "term": { "actorList.id": 3 } } } |
5.2.16 过滤--查询前过滤(推荐)
其实准确来说,ES中的查询操作分为2种:查询(query)和过滤(filter)。查询即是之前提到的query查询,它(查询)默认会计算每个返回文档的得分,然后根据得分排序。而过滤(filter)只会筛选出符合的文档,并不计算得分,且它可以缓存文档。所以,单从性能考虑,过滤比查询更快。 换句话说,过滤适合在大范围筛选数据,而查询则适合精确匹配数据。一般应用时,应先使用过滤操作过滤数据,然后使用查询匹配数据。 |
GET movie_index/movie/_search { "query":{ "bool":{ "filter":[ {"term": { "actorList.id": "1" }}, {"term": { "actorList.id": "3" }} ], "must":{"match":{"name":"red"}} } } } |
term、terms过滤 term、terms的含义与查询时一致。term用于精确匹配、terms用于多词条匹配。不过既然过滤器适用于大氛围过滤,term、terms在过滤中使用意义不大。在项目中建议使用term。 Term: where id = ? Terms: where id in () # select * from skuInfo where id=? Select * from skuInfo where id in () |
5.2.17 过滤--按范围过滤
GET movie_index/movie/_search { "query": { "bool": { "filter": { "range": { "doubanScore": {"gte": 8} } } } } } |
关于范围操作符:跟html标签中的转义字符一样!
gt | 大于 |
lt | 小于 |
gte | 大于等于 |
lte | 小于等于 |
5.2.18 排序
GET movie_index/movie/_search { "query":{ "match": {"name":"red sea"} } , "sort": [ { "doubanScore": { "order": "desc" } } ] } |
面试题:
Mysql 默认升序
Oracle 默认是升序
Sqlserver 默认是升序
端口号不一样,分页语句不一样!
3306 1521 1433
Limit rownum top
5.2.19 分页查询
GET movie_index/movie/_search { "query": { "match_all": {} }, // 第几条开始查询! "from": 1, "size": 1 } |
5.2.20 指定查询的字段
GET movie_index/movie/_search { "query": { "match_all": {} }, "_source": ["name", "doubanScore"] } |
5.2.21 高亮
GET movie_index/movie/_search { "query":{ "match": {"name":"red sea"} }, "highlight": { "fields": {"name":{} } }
} |
修改自定义高亮标签 GET movie_index/movie/_search { "query":{ "match": {"name":"red sea"} }, "highlight": { "post_tags": ["</span>"], "pre_tags": ["<span>"], "fields": {"name":{} } } } |
5.2.22 聚合
取出每个演员共参演了多少部电影 – sql : group by !
GET movie_index/movie/_search { "aggs": { "groupby_actor": { "terms": { "field": "actorList.name.keyword" } } } } |
每个演员参演电影的平均分是多少,并按评分排序
GET movie_index/movie/_search { "aggs": { "groupby_actor_id": { "terms": { "field": "actorList.name.keyword" , "order": { "avg_score": "desc" } }, "aggs": { "avg_score":{ "avg": { "field": "doubanScore" } } } } } } |
5.3 关于mapping
之前说type可以理解为table,那每个字段的数据类型是如何定义的呢
查看看mapping
GET movie_index/_mapping/movie |
实际上每个type中的字段是什么数据类型,由mapping定义。
但是如果没有设定mapping系统会自动,根据一条数据的格式来推断出应该的数据格式。
- true/false → boolean
- 1020 → long
- 20.1 → double,float
- “2018-02-01” → date
- “hello world” → text +keyword
默认只有text会进行分词,keyword是不会分词的字符串。
mapping除了自动定义,还可以手动定义,但是只能对新加的、没有数据的字段进行定义。一旦有了数据就无法再做修改了。
注意:虽然每个Field的数据放在不同的type下,但是同一个名字的Field在一个index下只能有一种mapping定义。
5.4 中文分词
elasticsearch本身自带的中文分词,就是单纯把中文一个字一个字的分开,根本没有词汇的概念。但是实际应用中,用户都是以词汇为条件,进行查询匹配的,如果能够把文章以词汇为单位切分开,那么与用户的查询条件能够更贴切的匹配上,查询速度也更加快速。
5.4.1 安装
下载好的zip包,请解压后放到 /usr/share/elasticsearch/plugins/
[root@localhost plugins]# unzip elasticsearch-analysis-ik-5.6.4.zip
将压缩包文件删除!否则启动失败!
然后重启es
[root@localhost plugins]# service elasticsearch restart
5.4.2 测试使用
使用默认
GET movie_index/_analyze { "text": "我是中国人" } |
请观察结果
使用分词器
GET movie_index/_analyze { "analyzer": "ik_smart", "text": "我是中国人" } |
请观察结果
另外一个分词器
ik_max_word
GET movie_index/_analyze { "analyzer": "ik_max_word", "text": "我是中国人" } |
请观察结果
能够看出不同的分词器,分词有明显的区别,所以以后定义一个type不能再使用默认的mapping了,要手工建立mapping, 因为要选择分词器。
5.4.3 基于中文分词搭建索引
1、建立mapping
PUT movie_chn { "mappings": { "movie":{ "properties": { "id":{ "type": "long" }, "name":{ "type": "text" , "analyzer": "ik_smart" }, "doubanScore":{ "type": "double" }, "actorList":{ "properties": { "id":{ "type":"long" }, "name":{ "type":"keyword" } } } } } } } |
插入数据
PUT /movie_chn/movie/1 { "id":1, "name":"红海行动", "doubanScore":8.5, "actorList":[ {"id":1,"name":"张译"}, {"id":2,"name":"海清"}, {"id":3,"name":"张涵予"} ] } PUT /movie_chn/movie/2 { "id":2, "name":"湄公河行动", "doubanScore":8.0, "actorList":[ {"id":3,"name":"张涵予"} ] } PUT /movie_chn/movie/3 { "id":3, "name":"红海事件", "doubanScore":5.0, "actorList":[ {"id":4,"name":"张国立"} ] } |
查询测试
GET /movie_chn/movie/_search { "query": { "match": { "name": "红海战役" } } } GET /movie_chn/movie/_search { "query": { "term": { "actorList.name": "张译" } } } |
5.4.4 自定义词库
什么使用?
当词库满足不了你的需要,可以使用自定义词库!
修改/usr/share/elasticsearch/plugins/ik/config/中的IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里配置自己的扩展字典 --> <entry key="ext_dict"></entry> <!--用户可以在这里配置自己的扩展停止词字典--> <entry key="ext_stopwords"></entry> <!--用户可以在这里配置远程扩展字典 --> <entry key="remote_ext_dict">http://192.168.67.163/fenci/myword.txt</entry> <!--用户可以在这里配置远程扩展停止词字典--> <!-- <entry key="remote_ext_stopwords">words_location</entry> --> </properties> |
按照标红的路径利用nginx发布静态资源
在nginx.conf中配置
server { listen 80; server_name 192.168.67.163; location /fenci/ { root es; } } |
并且在/usr/local/nginx/下建/es/fenci/目录,目录下加myword.txt
myword.txt中编写关键词,每一行代表一个词。
然后重启es服务器,重启nginx。
更新完成后,es只会对新增的数据用新词分词。历史数据是不会重新分词的。如果想要历史数据重新分词。需要执行:
POST movies_index_chn/_update_by_query?conflicts=proceed |
四、 Java程序中的应用
1 、搭建模块
pom.xml
<?xml version="1.0" encoding="UTF-8"?> |
2、 关于es 的java 客户端的选择
目前市面上有两类客户端
一类是TransportClient 为代表的ES原生客户端,不能执行原生dsl语句必须使用它的Java api方法。
另外一种是以Rest Api为主的missing client,最典型的就是jest。 这种客户端可以直接使用dsl语句拼成的字符串,直接传给服务端,然后返回json字符串再解析。
两种方式各有优劣,但是最近elasticsearch官网,宣布计划在7.0以后的版本中废除TransportClient。以RestClient为主。
所以在官方的RestClient 基础上,进行了简单包装的Jest客户端,就成了首选,而且该客户端也与springboot完美集成。
3 、在gmall-list-service项目中导入Jest依赖
<dependency> |
其中jest和jna请将版本号,部分纳入gmall-parent中管理。spring-boot-starter-data-elasticsearch不用管理版本号,其版本跟随springboot的1.5.10大版本号。
4 、在测试类中测试ES
application.properties中加入
server.port=8085 |
spring.elasticsearch.jest.uris=http://192.168.67.163:9200 |
在springBoot 测试类 中添加
@Autowired |
打印结果:
以上技术方面的准备就做好了。下面回到咱们电商的业务
\
五、利用elasticSearch开发电商的搜索列表功能
1、功能简介
入口: 两个
首页的分类
搜索栏
列表展示页面
2 根据业务搭建数据结构
建立mapping!
这时我们要思考三个问题:
- 哪些字段需要分词
- 例如:商品名称[不desc是skuName]
- 我们用哪些字段进行过滤
- 平台属性[三级分类Id]【真正的过滤应该是通过平台属性值进行过滤】
- 哪些字段我们需要通过搜索查询出来。
- 商品名称,价格等。
需要分词的字段 | 名称 | 分词 |
需要用于过滤的字段 | 三级分类、平台属性值 | 不分词 |
需要查询的字段 | Sku_id,价格,名称(关键词高亮),图片地址 | 显示的内容 |
以上分析的所有显示,以及分词,过滤的字段都应该在es中出现。Es中如何保存这些数据呢?
“根据上述的字段描述,应该建立一个mappings对应的存上上述字段描述的信息!”
根据以上制定出如下结构:mappings
Index:gmall
type:SkuInfo
document: properties - rows
field: id,price,skuName…
Es中index默认是true。
SkuInfo = Type
PUT gmall { "mappings": { "SkuInfo":{ "properties": { "id":{ "type": "keyword" , "index": false }, "price":{ "type": "double" }, "skuName":{ "type": "text", "analyzer": "ik_max_word" }, "catalog3Id":{ "type": "keyword" }, "skuDefaultImg":{ "type": "keyword", "index": false }, "skuAttrValueList":{ "properties": { "valueId":{ "type":"keyword" } } } } } } } |
注意:ik_max_word 中文词库必须有!
skuAttrValueList:平台属性值的集合,主要用于平台属性值过滤。
3 sku数据保存到ES
思路:
回顾一下,es数据保存的dsl javaBean == json格式的数据。
PUT /movie_index/movie/1 { "id":1, "name":"operation red sea", "doubanScore":8.5, "actorList":[ {"id":1,"name":"zhang yi"}, {"id":2,"name":"hai qing"}, {"id":3,"name":"zhang han yu"} ] } |
es存储数据是以json格式保存的,那么如果一个javabean的结构刚好跟要求的json格式吻合,我们就可以直接把javaBean序列化为json保持到es中,所以我们要制作一个与es中json格式一致的javabean.
3.1 JavaBean
把es中所有的字段封装到skuLsInfo中。
public class SkuLsInfo implements Serializable { } |
SkuLsAttrValue
public class SkuLsAttrValue implements Serializable { } |
添加get,set方法
3.2 保存sku数据的业务实现类
自行添加gmall-interface中增加接口方法
package com.test.gmall.service; |
在gmall-list-service模块中增加业务实现类listServiceImpl
@Service |
电商业务:分为前台,后台管理
前台:显示,以及购买流程
后台:商品的管理
管理:spu,sku,商品的上架,下架。
下架:实际就是从es中删除。
上架:将最新的产品新增到es上。
苹果3,4,4s,5,5s,5c,se,6,6s…
3.3 在后台管理的sku保存中,调用该方法
AttManageController 中 @RequestMapping(value = "onSale",method = RequestMethod.GET) |
测试: |
查询 GET gmall/SkuInfo/3 |
4 查询数据的后台方法
4.1 分析
首先先观察功能页面,咱们一共要用什么查询条件,查询出什么数据?
查询条件:
- 关键字
- 可以通过分类ID进入列表页面
- 平台属性值:过滤
- 分页页码
查询结果:
1 sku的列表(关键字高亮显示)
- 这些sku涉及了哪些属性和属性值
- 命中个数[total],用于分页
基于以上
4.2 编写DSL语句 :
主要目的:是从es中取得数据!
GET gmall/SkuInfo/_search { "query": { "bool": { "filter": [ {"term":{ "skuAttrValueList.valueId": "13"}}, {"term":{ "skuAttrValueList.valueId": "80"}}, {"term":{"catalog3Id":"61"}} ], "must": { "match": { "skuName": "小米" } } } } , "highlight": { "fields": {"skuName":{}} }, "from": 0, "size": 2, "sort":{"hotScore":{"order":"desc"}}, "aggs": { "groupby_attr": { "terms": { "field": "skuAttrValueList.valueId" } } } } |
Es 匹配根据中文分词解析库进行匹配
4.3 制作传入参数的类
在gmall-bean 项目中添加如下实体类 传入的参数是根据dsl语句得到
public class SkuLsParams implements Serializable { } |
4.4 返回结果的类
public class SkuLsResult implements Serializable { } |
4.5 基于这个DSL查询编写Java代码
接口 ListService public SkuLsResult search(SkuLsParams skuLsParams); |
实现类 public SkuLsResult search(SkuLsParams skuLsParams){ |
4.5.1 构造查询DSL
查询的过程很简单,但是要构造查询的query这个字符串有点麻烦,主要是这个Json串中的数据都是动态的。要拼接这个字符串,需要各种循环判断,处理标点符号等等。操作麻烦,可读性差。
但是JestClient这个客户端包,提供了一组builder工具。这个工具可以比较方便的帮程序员组合复杂的查询Json。
public String makeQueryStringForSearch(SkuLsParams skuLsParams){ |
4.5.2 处理返回值
思路:所有的返回值其实都在这个searchResult中
searchResult = jestClient.execute(search); |
它的结构其实可以在kibana 中观察一下:
命中的结果
高亮显示
分组统计结果:
针对这三个部分来解析searchResult
private SkuLsResult makeResultForSearch(SkuLsParams skuLsParams,SearchResult searchResult){ |
测试后台程序…
4.5.3 创建gmall-list-web模块
5.1.1 pom.xml
<parent> |
Controller
|
# application.properties 配置文件
server.port=8086 |
域名更改:
1.修改nginx.conf 配置
# vim /usr/local/nginx/conf/nginx.conf
2 具体修改
upstream manage.gmall.com { server 192.168.67.1:8082; } server { listen 80; server_name manage.gmall.com; location /{ proxy_pass http://manage.gmall.com; } } upstream list.gmall.com{ server 192.168.67.1:8086; } server { listen 80; server_name list.gmall.com; location /{ proxy_pass http://list.gmall.com; } } |
在hosts 配置文件中添加
3 重启nginx
./nginx -s quit :重启
./nginx
quit 退出重启
./nginx -s reload :重启
启动状态下 重新读取nginx.conf reload
# hosts 配置文件
六、利用elasticSearch开发电商的搜索列表功能
1 检索的页面
检索功能
1.1 为gmall-list-web模块添加静态页面
1.1.1 静态网页及资源文件
拷贝静态文件到resources目录下,手工建立static和templates目录
1.2 sku列表功能
首先是根据关键字、属性值、分类Id、页码查询sku列表。
1.2.1 ListController
@RequestMapping("list.html") return "list"; } |
1.2.2 页面html渲染
<div style="width:215px" th:each="skuLsInfo:${skuLsInfoList}" > |
要注意的是其中skuName中因为关键字标签所以必须要用utext否则标签会被转义。
1.2.3 搜索栏相关html
<!--搜索导航--> |
1.2.4 js代码
function searchList(){ function item(skuid) { |
这时可以看到列表效果了。
1.3 页面功能—提供可供选择的属性列表
1.3.1 思路:
这个列表有两种情况
- 如果是通过首页的3级分类点击进入的,要按照分类Id查询对应的属性和属性值列表。
- 如果是直接用搜索栏输入文字进入的,要根据sku的查询结果涉及的属性值(好在我们已经通过es的聚合取出来了),再去查询数据库把文字列表显示出来。
1.3.2 ListController
数据来源于:base_attr_info ,base_attr_value
//根据查询的结果返回属性和属性值列表 |
其中manageService.getAttrList(String catalog3Id)这个方法我们已经有现查的了。
但是manageService.getAttrList(attrValueIdList) 这个方法我们要新添加。
1.3.3 在ManageServiceImpl中增加方法
@Override |
1.3.4 BaseAttrInfoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
|
注意这里面没有用#{}是因为attrValueIds 是两个数字用逗号分开的,所以不能整体套上单引,所以使用${}。
1.3.5 BaseAttrInfoMapper.class
public interface BaseAttrInfoMapper extends Mapper<BaseAttrInfo> { |
此处必须要用@Param注解否则${ }无法识别。
1.3.6 点击属性值的链接
getAttrList(List<String> attrValueIdList)方法实现后,还有一个问题就是,点击属性时,要把上次查询的内容也带上,即带上历史参数。
在controller中添加方法拼接条件方法 public String makeUrlParam(SkuLsParams skuLsParam){ |
@RequestMapping("list.html") |
1.3.7 生成属性列表的html部分
<div class="GM_selector"> |
完成后
1.4 页面功能--面包屑
面包屑导航是为了能够让用户清楚的知道当前页面的所在位置和筛选条件的功能。但是这个小的人性化功能却有点麻烦。
功能点:
1、点击某个属性值的时候对应的那行属性要消失掉不能再次选择。
2、列在上面的属性面包屑,要可以取消掉,恢复到没选择之前。
1.4.1 思路:
- 把本应显示的列表与用户已选择的属性值列表用循环交叉判断,如果匹配把本应显示的那个属性去掉。
- 已选择的属性值列表,要携带点击跳转的路径,这个路径参数就是咱们上边讲的那个“历史参数”,但是要把自己本身的属性值去掉。
- 重构BaseAttrValue实体类
@Transient private String urlParam; |
1.4.2 重构makeUrlParam方法
将多余的条件去除!
public String makeUrlParam(SkuLsParams skuLsParam,String... excludeValueIds){ // 不能写break;如果写了break;其他条件则无法拼接! } |
1.4.3 controller中的getList方法增加
代码如下用红色标识部分!@RequestMapping("list.html")
|
这块代码看似多层循环嵌套性能隐患,其实因为单次循环基本不会超过五次,循环中没有网络或者io访问,完全在虚拟机中运行,所以即使多层循环嵌套压力也不会太大。
1.4.4 页面代码
<div class="GM_ipone"> |
1.5 分页:在ListController中添加如下代码
// 设置每页显示的条数
|
2 排序
页面结构完成了,考虑一下如何排序,es查询的dsl语句中我们是用了hotScore来进行排序的。
但是hotScore从何而来,根据业务去定义,也可以扩展更多类型的评分,让用户去选择如何排序。
这里的hotScore我们假定以点击量来决定热度。
那么我们每次用户点击,将这个评分+1,不就可以了么。
2.1 问题:
1、 es大量的写操作会影响es 性能,因为es需要更新索引,而且es不是内存数据库,会做相应的io操作。
2、而且修改某一个值,在高并发情况下会有冲突,造成更新丢失,需要加锁,而es的乐观锁会恶化性能问题。
从业务角度出发,其实我们为商品进行排序所需要的热度评分,并不需要非常精确,大致能比出个高下就可以了。
利用这个特点我们可以稀释掉大量写操作。
2.2 解决思路:
用redis做精确计数器,redis是内存数据库读写性能都非常快,利用redis的原子性的自增可以解决并发写操作。
redis每计10次数(可以被10整除)我们就更新一次es ,这样写操作就被稀释了1倍,这个倍数可以根据业务情况灵活设定。
代码 在listServiceImpl中增加更新操作
- 在gmall-list-service更新redis计数器
gmall-list-service在配置文件中添加配置
spring.redis.host=192.168.67.202 |
注意:在启动类上添加扫描注解:
@ComponentScan(basePackages = "com.test.gmall1128") |
接口: public void incrHotScore(String skuId); 在ListServiceImpl实现类中添加方法
|
2.4 代码:更新es
private void updateHotScore(String skuId,Long hotScore){ |
2.5 详情页调用
这个incrHotScore 方法可以在进入详情页面的时候调用。
@Controller …… listService.incrHotScore(skuId); //最终应该由异步方式调用 } |