admin管理员组

文章数量:1655520

一、 项目整合redis

1 思路:

虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能需要优化。

一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。

一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。

提高数据库本身的性能首先是优化sql,包括:使用索引,减少不必要的大表关联次数,控制查询字段的行数和列数。另外当数据量巨大是可以考虑分库分表,以减轻单点压力。

这部分知识在mysql高级已有讲解,这里大家可以以详情页中的sql作为练习,尝试进行优化,这里不做赘述。

重点要讲的是另外一个层面:尽量避免直接查询数据库。

解决办法就是:缓存

缓存可以理解是数据库的一道保护伞,任何请求只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。

咱们就用Redis作为缓存系统进行优化。

结构图:

多级缓存应用:

安装Redis :

  1. 安装依赖
  2. 导入jar包
  3. 创建安装目录
  4. Make
  5. Make install 
  6. 修改配置文件

2 整合redis到大工程中。

由于redis作为缓存数据库,要被多个项目使用,所以要制作一个通用的工具类,方便工程中的各个模块使用。

而主要使用redis的模块,都是后台服务的模块,xxx-service工程。所以咱们把redis的工具类放到service-util模块中,这样所有的后台服务模块都可以使用redis。

首先引入依赖包

<!-- https://mvnrepository/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 {

    private JedisPool jedisPool;

    public  void  initJedisPool(String host,int port,int database){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 总数
        jedisPoolConfig.setMaxTotal(200);
        // 获取连接时等待的最大毫秒
        jedisPoolConfig.setMaxWaitMillis(10*1000);
        // 最少剩余数
        jedisPoolConfig.setMinIdle(10);
        // 如果到最大数,设置等待
        jedisPoolConfig.setBlockWhenExhausted(true);
        // 在获取连接时,检查是否有效
        jedisPoolConfig.setTestOnBorrow(true);

// 创建连接池
jedisPool = new  JedisPool(jedisPoolConfig,host,port,20*1000);


    }
    public Jedis getJedis(){
        Jedis jedis = jedisPool.getResource();
        return jedis;
    }
}

RedisConfig

@Configuration 相当于spring3.0版本的xml 

@Configuration
public class RedisConfig {

    //读取配置文件中的redisip地址
    @Value("${spring.redis.host:disabled}")
    private String host;

    @Value("${spring.redis.port:0}")
    private int port;

    @Value("${spring.redis.database:0}")
    private int database;

    @Bean
    public RedisUtil getRedisUtil(){
        if(host.equals("disabled")){
            return null;
        }
        RedisUtil redisUtil=new RedisUtil();
        redisUtil.initPool(host,port,database);
        return redisUtil;
    }

}

同时,任何模块想要调用redis都必须在application.properties配置,否则不会进行注入。

spring.redis.host=192.168.67.204
spring.redis.port=6379
spring.redis.database=0

现在可以在manage-service中的getSkuInfo()方法测试一下

try {
    Jedis jedis = redisUtil.getJedis();
    jedis.set("test","text_value" );
}catch (JedisConnectionException e){
    e.printStackTrace();
}

在启动类上加上@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;

public class ManageConst {
    public static final String SKUKEY_PREFIX="sku:";

    public static final String SKUKEY_SUFFIX=":info";

    public static final int SKUKEY_TIMEOUT=24*60*60;

}

@Override
public SkuInfo getSkuInfo(String skuId) {
    // 缓存测试-
    Jedis jedis = redisUtil.getJedis();
    // Ctrl+Alt+M 提取方法
    SkuInfo skuInfo=null;
    String skuInfoKey = ManageConst.SKUKEY_PREFIX+skuId+ManageConst.SKUKEY_SUFFIX;
    if (jedis.exists(skuInfoKey)){
        // 取出数据
        String skuInfoJson = jedis.get(skuInfoKey);
        if (skuInfoJson!=null && skuInfoJson.length()!=0){
            skuInfo = JSON.parseObject(skuInfoJson, SkuInfo.class);
        }
        // 将数据转换成对象
    }else{
        // 从数据库中取得数据
        skuInfo = getSkuInfoDB(skuId);
        // 将最新的数据放入到缓存中
        String jsonString = JSON.toJSONString(skuInfo);
        jedis.setex(skuInfoKey,ManageConst.SKUKEY_TIMEOUT,jsonString);
    }
    jedis.close();
    return skuInfo;
}
public SkuInfo getSkuInfoDB(String skuId){
    // 单纯的信息
    SkuInfo skuInfo = skuInfoMapper.selectByPrimaryKey(skuId);
    // 查询图片
    SkuImage skuImage = new SkuImage();
    skuImage.setSkuId(skuId);
    List<SkuImage> imageList = skuImageMapper.select(skuImage);

// 将查询出来所有图片赋予对象
    skuInfo.setSkuImageList(imageList);
    // 查询属性值
    SkuSaleAttrValue skuSaleAttrValue = new SkuSaleAttrValue();
    skuSaleAttrValue.setSkuId(skuId);
    List<SkuSaleAttrValue> skuSaleAttrValueList = skuSaleAttrValueMapper.select(skuSaleAttrValue);
    
    // 将查询出来所有商品属性值赋给对象
    skuInfo.setSkuSaleAttrValueList(skuSaleAttrValueList);
    return skuInfo;
}

以上基本实现使用缓存的方案。

如果说: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;
public static final String SKULOCK_SUFFIX=":lock";

@Override
public SkuInfo getSkuInfo(String skuId) {
    SkuInfo skuInfo = null;
try{
    Jedis jedis = redisUtil.getJedis();
    // 定义key
    String skuInfoKey = ManageConst.SKUKEY_PREFIX+skuId+ManageConst.SKUKEY_SUFFIX; //key= sku:skuId:info

    String skuJson = jedis.get(skuInfoKey);

    if (skuJson==null || skuJson.length()==0){
        // 没有数据 ,需要加锁!取出完数据,还要放入缓存中,下次直接从缓存中取得即可!
        System.out.println("没有命中缓存");
        // 定义key user:userId:lock
        String skuLockKey=ManageConst.SKUKEY_PREFIX+skuId+ManageConst.SKULOCK_SUFFIX;
        // 生成锁
        String lockKey  = jedis.set(skuLockKey, "OK", "NX", "PX", ManageConst.SKULOCK_EXPIRE_PX);
        if ("OK".equals(lockKey)){
            System.out.println("获取锁!");
            // 从数据库中取得数据
            skuInfo = getSkuInfoDB(skuId);
            // 将是数据放入缓存
            // 将对象转换成字符串
            String skuRedisStr = JSON.toJSONString(skuInfo);
            jedis.setex(skuInfoKey,ManageConst.SKUKEY_TIMEOUT,skuRedisStr);
            jedis.close();
            return skuInfo;
        }else {
            System.out.println("等待!");
            // 等待
            Thread.sleep(1000);
            // 自旋
           return getSkuInfo(skuId);
        }
    }else{
        // 有数据
        skuInfo = JSON.parseObject(skuJson, SkuInfo.class);
        jedis.close();
        return skuInfo;
    }
}catch (Exception e){
    e.printStackTrace();
}
   // 从数据库返回数据
    return getSkuInfoDB(skuId);
}

5 redisson 解决分布式锁

  1. 导入依赖 service-util

<dependency>

   <groupId>org.redisson</groupId>

   <artifactId>redisson</artifactId>

   <version>3.11.1</version>

</dependency>  

  1. 修改实现类

private SkuInfo getSkuInfoRedisson(String skuId) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.67.219:6379");

        RedissonClient redissonClient = Redisson.create(config);

        // 使用redisson 调用getLock
        RLock lock = redissonClient.getLock("yourLock");


        // 加锁
        lock.lock(10, TimeUnit.SECONDS);

//        try {
//            boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
//
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        // 放入业务逻辑代码
        SkuInfo skuInfo =null;
        Jedis jedis = null;
        // ctrl+alt+t
        try {
            jedis = redisUtil.getJedis();
            // 定义key: 见名之意: sku:skuId:info
            String skuKey = ManageConst.SKUKEY_PREFIX+skuId+ManageConst.SKUKEY_SUFFIX;
            // 判断缓存中是否有数据,如果有,从缓存中获取,没有从db获取并将数据放入缓存!
            // 判断redis 中是否有key
            if (jedis.exists(skuKey)){
                // 取得key 中的value
                String skuJson = jedis.get(skuKey);
                // 将字符串转换为对象
                skuInfo = JSON.parseObject(skuJson, SkuInfo.class);
//                jedis.close();
                return skuInfo;
            }else {
                skuInfo = getSkuInfoDB(skuId);
                // 放redis 并设置过期时间
                jedis.setex(skuKey,ManageConst.SKUKEY_TIMEOUT,JSON.toJSONString(skuInfo));
                return skuInfo;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis!=null){
                jedis.close();
            }
            lock.unlock();
        }
        return getSkuInfoDB(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;

          location / {

                root front;

                index index.htm;

                }

        }

=========================item.gmall=====================================

        upstream item.gmall{

         server 192.168.67.1:8084;

        }

      server {

          listen       80;

          server_name  item.gmall;

          location / {

                proxy_pass http://item.gmall;

                }

        }

三、 全文检索工具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的使用场景

  1. 为用户提供按关键字查询的全文搜索功能。
  2. 著名的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 新增文档 
  1. 格式 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系统会自动,根据一条数据的格式来推断出应该的数据格式。

  1. true/false → boolean
  2. 1020  →  long
  3. 20.1 → double,float
  4. “2018-02-01” → date
  5. “hello world” → text +keyword

默认只有text会进行分词,keyword是不会分词的字符串。

mapping除了自动定义,还可以手动定义,但是只能对新加的、没有数据的字段进行定义。一旦有了数据就无法再做修改了。

注意:虽然每个Field的数据放在不同的type下,但是同一个名字的Field在一个index下只能有一种mapping定义。

5.4 中文分词

elasticsearch本身自带的中文分词,就是单纯把中文一个字一个字的分开,根本没有词汇的概念。但是实际应用中,用户都是以词汇为条件,进行查询匹配的,如果能够把文章以词汇为单位切分开,那么与用户的查询条件能够更贴切的匹配上,查询速度也更加快速。

分词器下载网址:GitHub - medcl/elasticsearch-analysis-ik: The IK Analysis plugin integrates Lucene IK analyzer into elasticsearch, support customized dictionary.

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/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"?>
<project xmlns="http://maven.apache/POM/4.0.0" xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.test.gmall</groupId>
   <artifactId>gmall-list-service</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>gmall-list-service</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>com.test.gmall</groupId>
      <artifactId>gmall-parent</artifactId>
      <version>1.0-SNAPSHOT</version>
   </parent>
   <dependencies>

      <dependency>
         <groupId>com.test.gmall</groupId>
         <artifactId>gmall-interface</artifactId>
         <version>1.0-SNAPSHOT</version>
      </dependency>

      <dependency>
         <groupId>com.test.gmall</groupId>
         <artifactId>gmall-service-util</artifactId>
         <version>1.0-SNAPSHOT</version>
      </dependency>

   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>

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>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

<!-- https://mvnrepository/artifact/io.searchbox/jest -->
<dependency>
   <groupId>io.searchbox</groupId>
   <artifactId>jest</artifactId>
</dependency>

<!-- https://mvnrepository/artifact/net.java.dev.jna/jna -->
<dependency>
   <groupId>net.java.dev.jna</groupId>
   <artifactId>jna</artifactId>
 </dependency>

其中jest和jna请将版本号,部分纳入gmall-parent中管理。spring-boot-starter-data-elasticsearch不用管理版本号,其版本跟随springboot的1.5.10大版本号。

4 、在测试类中测试ES

application.properties中加入

server.port=8085
logging.level.root=error
spring.dubbo.application.name=list-service
spring.dubbo.registry.protocol=zookeeper
spring.dubbo.registry.address=192.168.67.203:2181
spring.dubbo.base-package=com.test.gmall0319
spring.dubbo.protocol.name=dubbo
spring.datasource.url=jdbc:mysql://localhost:3306/gmall?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mybatis
mybatis.mapper-locations=classpath:mapper/*Mapper.xml
mybatis.configuration.mapUnderscoreToCamelCase=true

# redis
spring.redis.host=192.168.67.203
spring.redis.port=6379
spring.redis.database=0

spring.elasticsearch.jest.uris=http://192.168.67.163:9200

在springBoot 测试类 中添加

@Autowired
JestClient jestClient;

@Test
public void testEs() throws IOException {
   String query="{\n" +
         "  \"query\": {\n" +
         "    \"match\": {\n" +
         "      \"actorList.name\": \"张译\"\n" +
         "    }\n" +
         "  }\n" +
         "}";
   Search search = new Search.Builder(query).addIndex("movie_chn").addType("movie").build();

   SearchResult result = jestClient.execute(search);

   List<SearchResult.Hit<HashMap, Void>> hits = result.getHits(HashMap.class);

   for (SearchResult.Hit<HashMap, Void> hit : hits) {
      HashMap source = hit.source;
      System.err.println("source = " + source);
   }

}

打印结果:

    以上技术方面的准备就做好了。下面回到咱们电商的业务

\

五、利用elasticSearch开发电商的搜索列表功能

1、功能简介

入口: 两个

首页的分类

搜索栏

列表展示页面

2 根据业务搭建数据结构

建立mapping!

这时我们要思考三个问题:

  1. 哪些字段需要分词
    1. 例如:商品名称[不desc是skuName]
  2. 我们用哪些字段进行过滤
    1. 平台属性[三级分类Id]【真正的过滤应该是通过平台属性值进行过滤】
  3. 哪些字段我们需要通过搜索查询出来。
    1. 商品名称,价格等。

     

需要分词的字段

名称

分词

需要用于过滤的字段

三级分类、平台属性值

不分词

需要查询的字段

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 {

    String id;

    BigDecimal price;

    String skuName;

    String catalog3Id;

    String skuDefaultImg;

    Long hotScore=0L;

    List<SkuLsAttrValue> skuAttrValueList;

}

SkuLsAttrValue

public class SkuLsAttrValue implements Serializable {

    String valueId;

}

添加get,set方法

3.2 保存sku数据的业务实现类

自行添加gmall-interface中增加接口方法

package com.test.gmall.service;

import com.test.gmall.bean.SkuLsInfo;

public interface ListService {
    
    public void saveSkuInfo(SkuLsInfo skuLsInfo);
}

 在gmall-list-service模块中增加业务实现类listServiceImpl 

@Service
public class ListServiceImpl implements ListService {

    @Autowired
    JestClient jestClient;

    public static final String ES_INDEX="gmall";

    public static final String ES_TYPE="SkuInfo";

    @Override
    public void saveSkuInfo(SkuLsInfo skuLsInfo) {
        // 保存数据
        Index index = new Index.Builder(skuLsInfo).index(ES_INDEX).type(ES_TYPE).id(skuLsInfo.getId()).build();
        try {
            DocumentResult documentResult = jestClient.execute(index);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

电商业务:分为前台,后台管理

前台:显示,以及购买流程

后台:商品的管理

管理:spu,sku,商品的上架,下架。

下架:实际就是从es中删除。

上架:将最新的产品新增到es上。

苹果3,4,4s,5,5s,5c,se,6,6s…

3.3 在后台管理的sku保存中,调用该方法

AttManageController 

@RequestMapping(value = "onSale",method = RequestMethod.GET)
@ResponseBody
public void onSale(String skuId){
    SkuInfo skuInfo = manageService.getSkuInfo(skuId);
    SkuLsInfo skuLsInfo = new SkuLsInfo();
    // 属性拷贝
    try {
        BeanUtils.copyProperties(skuLsInfo,skuInfo);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
    listService.saveSkuInfo(skuLsInfo);
}

测试:

http://localhost:8082/onSale?skuId=3

查询

GET gmall/SkuInfo/3

4 查询数据的后台方法

4.1 分析

首先先观察功能页面,咱们一共要用什么查询条件,查询出什么数据?

查询条件:

  1. 关键字
  2. 可以通过分类ID进入列表页面
  3. 平台属性值:过滤
  4. 分页页码

查询结果:

1  sku的列表(关键字高亮显示)

  1. 这些sku涉及了哪些属性和属性值
  2. 命中个数[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 {

    String  keyword;

    String catalog3Id;

    String[] valueId;

    int pageNo=1;

    int pageSize=20;

}

4.4 返回结果的类

public class SkuLsResult implements Serializable {

    List<SkuLsInfo> skuLsInfoList;

    long total;

    long totalPages;

    List<String> attrValueIdList;

}

4.5 基于这个DSL查询编写Java代码

接口 ListService

public SkuLsResult search(SkuLsParams skuLsParams);

实现类

public SkuLsResult search(SkuLsParams skuLsParams){

    String query=makeQueryStringForSearch(skuLsParams);

    Search search= new Search.Builder(query).addIndex(ES_INDEX).addType(ES_TYPE).build();
    SearchResult searchResult=null;
 try {
       searchResult = jestClient.execute(search);
    } catch (IOException e) {
       e.printStackTrace();
    }

    SkuLsResult skuLsResult = makeResultForSearch(skuLsParams, searchResult);

    return skuLsResult;

}

4.5.1 构造查询DSL

查询的过程很简单,但是要构造查询的query这个字符串有点麻烦,主要是这个Json串中的数据都是动态的。要拼接这个字符串,需要各种循环判断,处理标点符号等等。操作麻烦,可读性差。

 但是JestClient这个客户端包,提供了一组builder工具。这个工具可以比较方便的帮程序员组合复杂的查询Json。

public  String makeQueryStringForSearch(SkuLsParams skuLsParams){
    // 创建查询bulid
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

    if (skuLsParams.getKeyword()!=null){
        MatchQueryBuilder ma = new MatchQueryBuilder("skuName", skuLsParams.getKeyword());
        boolQueryBuilder.must(ma);
        // 设置高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        // 设置高亮字段
        highlightBuilder.field("skuName");
        highlightBuilder.preTags("<span style='color:red'>");
        highlightBuilder.postTags("</span>");
        // 将高亮结果放入查询器中
        searchSourceBuilder.highlight(highlightBuilder);

    }
    // 设置三级分类
    if (skuLsParams.getCatalog3Id()!=null){
        TermQueryBuilder termQueryBuilder = new TermQueryBuilder("catalog3Id", skuLsParams.getCatalog3Id());
        boolQueryBuilder.filter(termQueryBuilder);
    }
    // 设置属性值
    if (skuLsParams.getValueId()!=null && skuLsParams.getValueId().length>0){
        for (int i=0;i<skuLsParams.getValueId().length;i++){
            String valueId = skuLsParams.getValueId()[i];
            TermQueryBuilder termsQueryBuilder = new TermQueryBuilder("skuAttrValueList.valueId", valueId);
            boolQueryBuilder.filter(termsQueryBuilder);
        }
    }
    searchSourceBuilder.query(boolQueryBuilder);
    // 设置分页
    int form = (skuLsParams.getPageNo()-1)*skuLsParams.getPageSize();
    searchSourceBuilder.from(form);
    searchSourceBuilder.size(skuLsParams.getPageSize());
    // 设置按照热度
    searchSourceBuilder.sort("hotScore", SortOrder.DESC);

    // 设置聚合
    TermsBuilder groupby_attr = AggregationBuilders.terms("groupby_attr").field("skuAttrValueList.valueId");
    searchSourceBuilder.aggregation(groupby_attr);

    String query = searchSourceBuilder.toString();
    System.out.println("query="+query);
    return  query;
}

4.5.2 处理返回值

思路:所有的返回值其实都在这个searchResult中

searchResult = jestClient.execute(search);

它的结构其实可以在kibana 中观察一下:

命中的结果

高亮显示

 

分组统计结果:

针对这三个部分来解析searchResult

private SkuLsResult makeResultForSearch(SkuLsParams skuLsParams,SearchResult searchResult){
    SkuLsResult skuLsResult=new SkuLsResult();
    List<SkuLsInfo> skuLsInfoList=new ArrayList<>(skuLsParams.getPageSize());

    //获取sku列表
    List<SearchResult.Hit<SkuLsInfo, Void>> hits = searchResult.getHits(SkuLsInfo.class);
    for (SearchResult.Hit<SkuLsInfo, Void> hit : hits) {
        SkuLsInfo skuLsInfo = hit.source;
        if(hit.highlight!=null&&hit.highlight.size()>0){
            List<String> list = hit.highlight.get("skuName");
            //把带有高亮标签的字符串替换skuName
            String skuNameHl = list.get(0);
            skuLsInfo.setSkuName(skuNameHl);
        }
        skuLsInfoList.add(skuLsInfo);
    }
    skuLsResult.setSkuLsInfoList(skuLsInfoList);
    skuLsResult.setTotal(searchResult.getTotal());

    //取记录个数并计算出总页数
    long totalPage= (searchResult.getTotal() + skuLsParams.getPageSize() -1) / skuLsParams.getPageSize();
    skuLsResult.setTotalPages(totalPage);

    //取出涉及的属性值id
    List<String> attrValueIdList=new ArrayList<>();
    MetricAggregation aggregations = searchResult.getAggregations();
    TermsAggregation groupby_attr = aggregations.getTermsAggregation("groupby_attr");
    if(groupby_attr!=null){
        List<TermsAggregation.Entry> buckets = groupby_attr.getBuckets();
        for (TermsAggregation.Entry bucket : buckets) {
            attrValueIdList.add( bucket.getKey()) ;
        }
        skuLsResult.setAttrValueIdList(attrValueIdList);
    }
    return skuLsResult;
}

测试后台程序…

4.5.3 创建gmall-list-web模块

5.1.1 pom.xml

<parent>
   <groupId>com.test.gmall</groupId>
   <artifactId>gmall-parent</artifactId>
   <version>1.0-SNAPSHOT</version>
</parent>


<dependencies>

<dependency>
   <groupId>com.test.gmall</groupId>
   <artifactId>gmall-interface</artifactId>
   <version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
   <groupId>com.test.gmall</groupId>
   <artifactId>gmall-web-util</artifactId>
   <version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

Controller


@Controller
public class ListController {

    @Reference
    private ListService listService;
    @RequestMapping("list.html")
    @ResponseBody
    public String getList(SkuLsParams skuLsParams){
        SkuLsResult search = listService.search(skuLsParams);
        return JSON.toJSONString(search);
    }
}

# application.properties 配置文件

server.port=8086
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5
spring.dubbo.application.name=list-web
spring.dubbo.registry.protocol=zookeeper
spring.dubbo.registry.address=192.168.67.202:2181
spring.dubbo.base-package=com.test.gmall
spring.dubbo.protocol.name=dubbo
spring.dubbo.consumer.timeout=10000
spring.dubbo.consumer.check=false

域名更改:

1.修改nginx.conf 配置

# vim /usr/local/nginx/conf/nginx.conf

2 具体修改

upstream manage.gmall {

                server 192.168.67.1:8082;

        }

    server {

                listen       80;

                server_name  manage.gmall;

                location /{

                        proxy_pass http://manage.gmall;

                }

        }

upstream list.gmall{

                server 192.168.67.1:8086;

        }

        server {

                listen       80;

                server_name  list.gmall;

                location /{

                        proxy_pass http://list.gmall;

                }

        }

在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")
public String getList(  SkuLsParams skuLsParams, Model model){

    //根据参数返回sku列表
    SkuLsResult skuLsResult = listService.search(skuLsParams);
    model.addAttribute("skuLsInfoList",skuLsResult.getSkuLsInfoList());

    return "list";

}

1.2.2 页面html渲染

<div style="width:215px" th:each="skuLsInfo:${skuLsInfoList}" >
      <p class="da">
          <a href="#" th:οnclick="'javascript:item('+${skuLsInfo.id}+')'">
              <img th:src="${skuLsInfo.skuDefaultImg}"   src="img/57d0d400Nfd249af4.jpg" class="dim">
          </a>
      </p>

      <p class="tab_R">
          <span th:text="''+${#numbers.formatDecimal(skuLsInfo.price,1,2)}">¥5199.00</span>
      </p>

          <a href="#" title=""   th:οnclick="'javascript:item('+${skuLsInfo.id}+')'" class="tab_JE" th:utext="${skuLsInfo.skuName}" >
              Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
          </a>
  </div>

要注意的是其中skuName中因为关键字标签所以必须要用utext否则标签会被转义。

1.2.3 搜索栏相关html

<!--搜索导航-->
<div class="header_sous">
    <div class="logo">
        <a href="#"><img src="/image/jdlogo-201708-@1x.png" alt=""></a>
    </div>
    <div class="header_form">
        <input id="keyword" name="keyword" type="text" placeholder="手机" />
        <a href="#" οnclick="searchList()">搜索</a>
    </div>
    <div class="header_ico">
        <div class="header_gw">
            <span><a href="#">我的购物车</a></span>
            <img src="/image/settleup-@1x.png" />
            <span>0</span>
        </div>
        <div class="header_ko">
            <p>购物车中还没有商品,赶紧选购吧!</p>
        </div>
    </div>

1.2.4 js代码

function searchList(){
    var keyword = $("#keyword").val();
    window.location.href="/list.html?keyword="+keyword;
}

function item(skuid) {
    window.location.href="http://localhost:8084/"+skuid+".html";
}

这时可以看到列表效果了。

1.3 页面功能提供可供选择的属性列表

1.3.1 思路:

这个列表有两种情况

  1. 如果是通过首页的3级分类点击进入的,要按照分类Id查询对应的属性和属性值列表。
  2. 如果是直接用搜索栏输入文字进入的,要根据sku的查询结果涉及的属性值(好在我们已经通过es的聚合取出来了),再去查询数据库把文字列表显示出来。
1.3.2  ListController

数据来源于:base_attr_info ,base_attr_value

//根据查询的结果返回属性和属性值列表
@Reference
ListService listService;
@Reference
private ManageService manageService;

@RequestMapping("list.html")
public  String getList(SkuLsParams skuLsParams, Model model){
    SkuLsResult skuLsResult = listService.search(skuLsParams);

    // 获取sku属性值列表
    List<SkuLsInfo> skuLsInfoList = skuLsResult.getSkuLsInfoList();
    model.addAttribute("skuLsInfoList",skuLsInfoList);
    // 从结果中取出平台属性值列表
    List<String> attrValueIdList = skuLsResult.getAttrValueIdList();
    List<BaseAttrInfo> attrList = manageService.getAttrList(attrValueIdList);
    model.addAttribute("attrList",attrList);
    //return JSON.toJSONString(search);
    return "list";
}

其中manageService.getAttrList(String catalog3Id)这个方法我们已经有现查的了。

但是manageService.getAttrList(attrValueIdList) 这个方法我们要新添加。

1.3.3 在ManageServiceImpl中增加方法

@Override
public List<BaseAttrInfo> getAttrList(List<String> attrValueIdList) {
    String attrValueIds = StringUtils.join(attrValueIdList.toArray(), ",");
    List<BaseAttrInfo> baseAttrInfoList = baseAttrInfoMapper.selectAttrInfoListByIds(attrValueIds);
    return baseAttrInfoList;

}

1.3.4 BaseAttrInfoMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper SYSTEM "http://mybatis/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.test.gmall.manage.mapper.BaseAttrInfoMapper">

<select id="selectAttrInfoListByIds" resultMap="baseAttrInfoMap">
    SELECT ai.id,ai.attr_name,ai.catalog3_id, av.id attr_value_id ,av.attr_id ,av.value_name
    FROM base_attr_info ai INNER JOIN base_attr_value av ON ai.id=av.attr_id
    WHERE av.id IN (${valueIds})
</select>


</mapper>

   

   注意这里面没有用#{}是因为attrValueIds 是两个数字用逗号分开的,所以不能整体套上单引,所以使用${}。

1.3.5 BaseAttrInfoMapper.class

public interface BaseAttrInfoMapper extends Mapper<BaseAttrInfo> {

List<BaseAttrInfo> selectAttrInfoListByIds(@Param("valueIds") String valueIds);
}

此处必须要用@Param注解否则${ }无法识别。

1.3.6  点击属性值的链接

 getAttrList(List<String> attrValueIdList)方法实现后,还有一个问题就是,点击属性时,要把上次查询的内容也带上,即带上历史参数。

在controller中添加方法拼接条件方法

public String makeUrlParam(SkuLsParams skuLsParam){
    String urlParam="";
    if(skuLsParam.getKeyword()!=null){
        urlParam+="keyword="+skuLsParam.getKeyword();
    }
    if (skuLsParam.getCatalog3Id()!=null){
        if (urlParam.length()>0){
            urlParam+="&";
        }
        urlParam+="catalog3Id="+skuLsParam.getCatalog3Id();
    }
    // 构造属性参数
    if (skuLsParam.getValueId()!=null && skuLsParam.getValueId().length>0){
        for (int i=0;i<skuLsParam.getValueId().length;i++){
            String valueId = skuLsParam.getValueId()[i];
            if (urlParam.length()>0){
                urlParam+="&";
            }
            urlParam+="valueId="+valueId;
        }
    }
    return  urlParam;
}

@RequestMapping("list.html")
public  String getList(SkuLsParams skuLsParams, Model model){
    SkuLsResult skuLsResult = listService.search(skuLsParams);
    // 从结果中取出平台属性值列表
    List<String> attrValueIdList = skuLsResult.getAttrValueIdList();
    List<BaseAttrInfo> attrList = manageService.getAttrList(attrValueIdList);

    // 已选的属性值列表\
    String urlParam = makeUrlParam(skuLsParams);
    // itco
    for (Iterator<BaseAttrInfo> iterator = attrList.iterator(); iterator.hasNext(); ) {
        BaseAttrInfo baseAttrInfo =  iterator.next();
        List<BaseAttrValue> attrValueList = baseAttrInfo.getAttrValueList();
        for (BaseAttrValue baseAttrValue : attrValueList) {
            if(skuLsParams.getValueId()!=null&&skuLsParams.getValueId().length>0){
                for (String valueId : skuLsParams.getValueId()) {
                    //选中的属性值 和 查询结果的属性值
                    if(valueId.equals(baseAttrValue.getId())){
                        iterator.remove();
                    }
                }
            }
        }
    }
    model.addAttribute("urlParam",urlParam);
    model.addAttribute("attrList",attrList);
    // 获取sku属性值列表
    List<SkuLsInfo> skuLsInfoList = skuLsResult.getSkuLsInfoList();
    model.addAttribute("skuLsInfoList",skuLsInfoList);
    //return JSON.toJSONString(search);
    return "list";
}

1.3.7 生成属性列表的html部分

<div class="GM_selector">
    <!--手机商品筛选-->
    <div class="title">
        <h3><em>商品筛选</em></h3>

    </div>
    <div class="GM_nav_logo">

        <div class="GM_pre"  th:each="attrInfo:${attrList}">
            <div class="sl_key">
                <span th:text="${attrInfo.attrName}+':'">属性:</span>
            </div>
            <div class="sl_value">
                <ul>
                    <li  th:each="attrValue:${attrInfo.attrValueList}"><a th:href="'/list.html?'+${urlParam}+'&valueId='+${attrValue.id}"  th:text="${attrValue.valueName}">属性值</a></li>
                </ul>
            </div>
        </div>
    </div>
</div>

完成后

1.4  页面功能--面包屑

面包屑导航是为了能够让用户清楚的知道当前页面的所在位置和筛选条件的功能。但是这个小的人性化功能却有点麻烦。

功能点:

1、点击某个属性值的时候对应的那行属性要消失掉不能再次选择。

2、列在上面的属性面包屑,要可以取消掉,恢复到没选择之前。

1.4.1 思路:
  1. 把本应显示的列表与用户已选择的属性值列表用循环交叉判断,如果匹配把本应显示的那个属性去掉。
  2. 已选择的属性值列表,要携带点击跳转的路径,这个路径参数就是咱们上边讲的那个“历史参数”,但是要把自己本身的属性值去掉。
  3. 重构BaseAttrValue实体类

@Transient

private String urlParam;

1.4.2 重构makeUrlParam方法

将多余的条件去除!

public String makeUrlParam(SkuLsParams skuLsParam,String... excludeValueIds){
    String urlParam="";
    List<String> paramList = new ArrayList<>();
    if(skuLsParam.getKeyword()!=null){
        urlParam+="keyword="+skuLsParam.getKeyword();
    }
    if (skuLsParam.getCatalog3Id()!=null){
        if (urlParam.length()>0){
            urlParam+="&";
        }
        urlParam+="catalog3Id="+skuLsParam.getCatalog3Id();
    }
    // 构造属性参数
    if (skuLsParam.getValueId()!=null && skuLsParam.getValueId().length>0){
        for (int i=0;i<skuLsParam.getValueId().length;i++){
            String valueId = skuLsParam.getValueId()[i];
            if (excludeValueIds!=null && excludeValueIds.length>0){
               String excludeValueId = excludeValueIds[0];
                if (excludeValueId.equals(valueId)){
                    // 跳出代码,后面的参数则不会继续追加【后续代码不会执行】

// 不能写break;如果写了break;其他条件则无法拼接!
                    continue;
                }
            }
            if (urlParam.length()>0){
                urlParam+="&";
            }
            urlParam+="valueId="+valueId;
        }
    }
    return  urlParam;
}

 }

1.4.3 controller中的getList方法增加

代码如下用红色标识部分!@RequestMapping("list.html")
public  String getList(SkuLsParams skuLsParams, Model model){
    SkuLsResult skuLsResult = listService.search(skuLsParams);
    // 从结果中取出平台属性值列表
    List<String> attrValueIdList = skuLsResult.getAttrValueIdList();
    List<BaseAttrInfo> attrList = manageService.getAttrList(attrValueIdList);
    // 已选的属性值列表
    List<BaseAttrValue> baseAttrValuesList = new ArrayList<>();
    String urlParam = makeUrlParam(skuLsParams);
    // itco
    for (Iterator<BaseAttrInfo> iterator = attrList.iterator(); iterator.hasNext(); ) {
        BaseAttrInfo baseAttrInfo =  iterator.next();
        List<BaseAttrValue> attrValueList = baseAttrInfo.getAttrValueList();
        for (BaseAttrValue baseAttrValue : attrValueList) {
         baseAttrValue.setUrlParam(urlParam);
            if(skuLsParams.getValueId()!=null&&skuLsParams.getValueId().length>0){
                for (String valueId : skuLsParams.getValueId()) {
                    //选中的属性值 和 查询结果的属性值
                    if(valueId.equals(baseAttrValue.getId())){
                        iterator.remove();
                        // 构造面包屑列表
                        BaseAttrValue baseAttrValueSelected = new BaseAttrValue();
                        baseAttrValueSelected.setValueName(baseAttrInfo.getAttrName()+":"+baseAttrValue.getValueName());
                        // 去除重复数据
                        String makeUrlParam = makeUrlParam(skuLsParams, valueId);
                        baseAttrValueSelected.setUrlParam(makeUrlParam);
                        baseAttrValuesList.add(baseAttrValueSelected);
                    }
                }
            }
        }
    }


    // 保存面包屑清单
    model.addAttribute("baseAttrValuesList",baseAttrValuesList);
    model.addAttribute("keyword",   skuLsParams.getKeyword());
    model.addAttribute("urlParam",urlParam);
    model.addAttribute("attrList",attrList);
    // 获取sku属性值列表
    List<SkuLsInfo> skuLsInfoList = skuLsResult.getSkuLsInfoList();
    model.addAttribute("skuLsInfoList",skuLsInfoList);
    //return JSON.toJSONString(search);
    return "list";
}

这块代码看似多层循环嵌套性能隐患,其实因为单次循环基本不会超过五次,循环中没有网络或者io访问,完全在虚拟机中运行,所以即使多层循环嵌套压力也不会太大。

1.4.4 页面代码

<div class="GM_ipone">
    <div class="GM_ipone_bar">
        <div class="GM_ipone_one a">
            筛选条件
        </div>
      <i><img src="/image/right-@1x.png" alt=""></i>
        <span th:if="${keyword}!=null" th:text="'"'+${keyword}+'"'">"小米"</span>
      <a class="select-attr"  th:each="baseAttrValue: ${baseAttrValuesList}"  th:utext="${baseAttrValue.valueName}+'<b>  </b>'"  th:href="'/list.html?'+${baseAttrValue.urlParam}" href="#"     > 2G<b>  </b>
      </a>
       <!--<a class="select-attr"   href="#"      > 屏幕尺寸:5.1-5.5英寸<b>  </b>-->
      <!--</a>-->
    </div>
</div>

1.5 分页:在ListController中添加如下代码

// 设置每页显示的条数
skuLsParams.setPageSize(2);


model.addAttribute("totalPages", skuLsResult.getTotalPages());
model.addAttribute("pageNo",skuLsParams.getPageNo());

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中增加更新操作

  1. 在gmall-list-service更新redis计数器

gmall-list-service在配置文件中添加配置

spring.redis.host=192.168.67.202
spring.redis.port=6379
spring.redis.database=0

注意:在启动类上添加扫描注解:

@ComponentScan(basePackages = "com.test.gmall1128")

接口:

public void incrHotScore(String skuId);

ListServiceImpl实现类中添加方法


//更新热度评分
@Override
public void incrHotScore(String skuId){
    Jedis jedis = redisUtil.getJedis();
    int timesToEs=10;
    Double hotScore = jedis.zincrby("hotScore", 1, "skuId:" + skuId);
    if(hotScore%timesToEs==0){
        updateHotScore(skuId,  Math.round(hotScore));
    }

}

2.4 代码:更新es

private void updateHotScore(String skuId,Long hotScore){
     String updateJson="{\n" +
             "   \"doc\":{\n" +
             "     \"hotScore\":"+hotScore+"\n" +
             "   }\n" +
             "}";

    Update update = new Update.Builder(updateJson).index("gmall").type("SkuInfo").id(skuId).build();
    try {
        jestClient.execute(update);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.5 详情页调用

这个incrHotScore 方法可以在进入详情页面的时候调用。

@Controller
public class ItemController {

    @Reference
    ListService listService;

    @Reference
    ManageService manageService;

    @RequestMapping("/{skuId}.html")
    public String getSkuInfo(@PathVariable("skuId") String skuId, Model model){

     ……

listService.incrHotScore(skuId);  //最终应该由异步方式调用

}

本文标签: 技术一文要点方案全文