谷粒商城分布式高级篇(上)

谷粒商城分布式基础篇
谷粒商城分布式高级篇(上)
谷粒商城分布式高级篇(中)
谷粒商城分布式高级篇(下)

文章目录

  • 全文检索-ElasticSearch
    • 简介
    • Docker安装ES
    • Docker安装Kibana
    • 入门
      • _cat
      • put&post新增数据
      • get查询数据&乐观锁字段
      • put&post修改数据
      • 删除数据&bulk批量操作导入样本测试数据
    • 进阶
      • 两种查询方式
      • QueryDSL基本使用&match_all
      • match全文检索 匹配查询
      • match_phrase短语匹配
      • multi_match多字段匹配
      • bool复合查询
      • filter过滤
      • term查询
        • .keyword 精确匹配
      • aggregations聚合分析
    • 映射
      • mapping创建
        • *Mapping Type 过时*
      • 添加新的字段映射
      • 修改映射&数据迁移
    • 分词
      • 分词&安装ik分词
        • vagrant 密码登录
        • 补充-修改linux网络设置&开启root密码访问
      • 自定义扩展词库
    • 整合
      • SpringBoot整合high-level-client
      • 测试保存
      • 测试复杂检索
  • 商城业务
    • 商品上架
      • sku在es中存储模型分析
      • nested数据类型场景
      • 构造基本数据
      • 构造sku检索属性
      • 远程查询库存&泛型结果封装
      • 远程上架接口
      • 上架接口调试&feign源码
      • 抽取响应结果&上架测试完成
    • 首页
      • 整合thymeleaf渲染首页
      • 整合dev-tools渲染一级分类数据
      • 渲染二级三级分类数据
    • nginx
      • 搭建域名访问环境一(反向代理配置)
      • 搭建域名访问环境二(负载均衡到网关)
  • 性能压测
    • 压力测试
      • 基本介绍
      • Apache JMeter安装使用
      • JMeter在windows下地址占用bug解决
    • 性能监控
      • 堆内存与垃圾回收
      • jvisualvm使用
    • 优化
      • 中间件对性能的影响
      • 简单优化吞吐量测试
      • nginx动静分离
      • 模拟线上应用内存崩溃宕机情况
      • 优化三级分类数据获取
  • 缓存
    • 缓存使用
      • 本地缓存与分布式缓存
      • 整合redis测试
      • 改造三级分类业务
      • 压力测试出的内存泄露及解决
      • 缓存击穿、穿透、雪崩
      • 加锁解决缓存击穿问题
      • 本地锁在分布式下的问题
    • 分布式锁
      • 分布式锁原理与使用
      • Redisson简介&整合
      • Redisson-lock锁测试
      • Redisson-lock看门狗原理-redisson如何解决死锁
      • Redisson-读写锁测试
      • 读写锁补充
      • 闭锁测试
      • 信号量测试
      • 缓存一致性解决
    • SpringCache
      • 简介
      • 整合&体验@Cacheable
      • @Cacheable细节设置
      • 自定义缓存配置
      • @CacheEvict
      • 原理与不足
  • 商城业务
    • 检索服务
      • 搭建页面环境
      • 调整页面跳转
      • 检索查询参数模型分析抽取
      • 检索返回结果模型分析抽取
      • 检索DSL测试-查询部分
      • 检索DSL测试-聚合部分
      • SearchRequest构建-检索
      • SearchRequest构建-聚合
      • SearchResponse分析&封装
      • 验证结果封装正确性
      • 页面基本数据渲染
      • 页面筛选条件渲染
      • 页面分页数据渲染
      • 页面排序功能
      • 页面排序字段回显
      • 页面价格区间搜索
      • 面包屑导航
      • 条件删除与URL编码问题
      • 条件筛选联动


全文检索-ElasticSearch

简介

谷粒商城分布式高级篇(上)_第1张图片
类比到MySQL里

ElasticSearch MySQL
Index (索引) 数据库(DataBase)
Type (类型) 数据表
Document (文档) 数据
属性 列名

谷粒商城分布式高级篇(上)_第2张图片

谷粒商城分布式高级篇(上)_第3张图片

Docker安装ES

谷粒商城分布式高级篇(上)_第4张图片

mkdir -p /mydata/elasticsearch/config    
mkdir -p /mydata/elasticsearch/data   
echo "http.host: 0.0.0.0" >>  /mydata/elasticsearch/config/elasticsearch.yml 

docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

在虚拟机创建了 elasticsearch 的两个 docker 外部 挂载用文件夹
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data

写入了一个配置并创建了yml配置文件, 代表可以被远程的任意 机器访问
echo "http.host:0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

为容器起了一个名字elasticsearch 暴露两个端口 9200端口 向elasticsearch的restApi发送http请求的端口 9300是es在分布式集群状态下 节点之间的通讯端口
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \

以单节点模式运行
-e "discovery.type=single-node" \

指定内存占用
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \

目录的挂载
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:usr/share/elasticsearch/plugins \

指定刚下载的镜像
-d elasticsearch:7.4.2

查看日志,发现权限不够
docker logs elasticsearch
在这里插入图片描述
赋予权限
在这里插入图片描述
重新启动容器
谷粒商城分布式高级篇(上)_第5张图片

Docker安装Kibana

postman 发送查询
谷粒商城分布式高级篇(上)_第6张图片

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601 \
-d kibana:7.4.2

http://192.168.56.10:9200 为自己的虚拟机地址

入门

_cat

谷粒商城分布式高级篇(上)_第7张图片

put&post新增数据

谷粒商城分布式高级篇(上)_第8张图片

谷粒商城分布式高级篇(上)_第9张图片

get查询数据&乐观锁字段

谷粒商城分布式高级篇(上)_第10张图片
乐观锁做并发修改
谷粒商城分布式高级篇(上)_第11张图片
每次修改 _seq_no 都会改变 修改时带上这个值才能成功
谷粒商城分布式高级篇(上)_第12张图片

put&post修改数据

谷粒商城分布式高级篇(上)_第13张图片

post 带_update 更新会对比源数据,如果没做改变,那么什么都不变
post 不带 _update 不会检查元数据

put 同上

删除数据&bulk批量操作导入样本测试数据

谷粒商城分布式高级篇(上)_第14张图片
没有删除类型

谷粒商城分布式高级篇(上)_第15张图片
谷粒商城分布式高级篇(上)_第16张图片
谷粒商城分布式高级篇(上)_第17张图片
导入测试数据,测试数据上传到了网盘

谷粒商城分布式高级篇(上)_第18张图片
链接:https://pan.baidu.com/s/1BJ6_6EAhjmTNdSgXjB4TcQ
提取码:hnfd

进阶

两种查询方式

docker 容器自启动
谷粒商城分布式高级篇(上)_第19张图片
文档
谷粒商城分布式高级篇(上)_第20张图片
谷粒商城分布式高级篇(上)_第21张图片
谷粒商城分布式高级篇(上)_第22张图片
将查询条件写为json的方式成为 Query DSL(查询领域对象语言)

QueryDSL基本使用&match_all

谷粒商城分布式高级篇(上)_第23张图片
谷粒商城分布式高级篇(上)_第24张图片
请求体中的各个参数就像sql中的查询条件
谷粒商城分布式高级篇(上)_第25张图片
match_all = select *

match全文检索 匹配查询

相当于 查询 字段 account_number 为 20 的值

GET bank/_search
{
     
  "query": {
     
    "match": {
     
      "account_number": "20"
    }
  }
}

又可以模糊查询

GET bank/_search
{
     
  "query": {
     
    "match": {
     
      "address": "Kings"
    }
  }
}

match_phrase短语匹配

不分词查询,包含完整的短语

谷粒商城分布式高级篇(上)_第26张图片

multi_match多字段匹配

谷粒商城分布式高级篇(上)_第27张图片
也做了分词
谷粒商城分布式高级篇(上)_第28张图片

bool复合查询

可以加入多种条件查询

GET bank/_search
{
     
  "query": {
     
    "bool": {
     
      "must": [
        {
     
          "match": {
     
            "gender": "M"
          }
        },
        {
     
          "match": {
     
            "address": "mill"
          }
        }
      ],
      "must_not": [
        {
     
          "match": {
     
            "age": "30"
          }
        }
      ],
      "should": [
        {
     
          "match": {
     
            "lastname": "Wallace"
          }
        }
      ]
    }
  }
}

条件关键词

  1. must 必须满足
  2. must_not 必须不满足
  3. should 应该满足,也可以不满足

filter过滤

filter没有相关性得分
range区间
谷粒商城分布式高级篇(上)_第29张图片

GET bank/_search
{
     
  "query": {
     
    "range": {
     
      "age": {
     
        "gte": 10,
        "lte": 30
      }
    }
  }
}

用filter过滤区间就不会如上查询获得相关性得分
谷粒商城分布式高级篇(上)_第30张图片

term查询

term和match一样是查询
但是文本字段避免使用term查询,文本字段的全文检索推荐 match。精确数组字段使用 term

规定全文检索字段用match,其他非text字段匹配用term

.keyword 精确匹配

每一个文本字段都可以 .keyword 代表匹配文本字段的整个精确值(不分词匹配)
和 match_phrase 短语匹配的区别
谷粒商城分布式高级篇(上)_第31张图片
match_phrase 短语匹配的区别
.keyword 匹配的值中 只能全等于 这个值
match_phrase 匹配的值中 包含 这个值 (此短语)
谷粒商城分布式高级篇(上)_第32张图片

aggregations聚合分析

谷粒商城分布式高级篇(上)_第33张图片
聚合语法
谷粒商城分布式高级篇(上)_第34张图片
为query的查询结果做聚合,有多少种不同的 age字段的值 size 前10个可能
terms聚合,用来查看值有多少种可能

GET bank/_search
{
     
  "query": {
     
    "match": {
     
      "address": "mill"
    }
  },
  "aggs": {
     
    "ageAgg": {
     
      "terms": {
     
        "field": "age",
        "size": 10
      }
    }
  }
}

谷粒商城分布式高级篇(上)_第35张图片
指定显示hits的条数size
谷粒商城分布式高级篇(上)_第36张图片
聚合中再子聚合 先聚合出年龄段 再聚合年龄段的平均薪资
谷粒商城分布式高级篇(上)_第37张图片

多次聚合
在这里插入图片描述

GET bank/_search
{
     
  "query": {
     "match_all": {
     }},
  "aggs": {
     
    "ageAgg": {
     
      "terms": {
     
        "field": "age",
        "size": 100
      },
      "aggs": {
     
        "genderAvg": {
     
          "terms": {
     
            "field": "gender.keyword",
            "size": 10
          },"aggs": {
     
            "genderBanlance": {
     
              "avg": {
     
                "field": "balance"
              }
            }
          }
        },
        "allBalanceAvg":{
     
          "avg": {
     
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

映射

mapping创建

相当于 MySQL创建表时 定义每一列的类型 (如 String int date )
谷粒商城分布式高级篇(上)_第38张图片
在这里插入图片描述
_mapping可以查看当前所有的所有属性的类型
谷粒商城分布式高级篇(上)_第39张图片
第一次存数据的时候,ES就会猜测 属性的类型

可以在第一次保存数据前可以给索引指定映射,创建索引时指定映射
谷粒商城分布式高级篇(上)_第40张图片

Mapping Type 过时

在6.0版本移除了映射类型
因为ES底层是Lucene开发的
谷粒商城分布式高级篇(上)_第41张图片

谷粒商城分布式高级篇(上)_第42张图片

添加新的字段映射

新增一个属性的映射
index:false为此映射不需要被索引
不写的话 都是默认 为 index:true
是用来控制这个属性是不是来参与检索的
相当于做了一个冗余字段
谷粒商城分布式高级篇(上)_第43张图片

修改映射&数据迁移

要修改映射类型,只能创建新索引指定好映射类型后,再数据迁移

创建新索引并指定好映射

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"
        }
      }
  }
}

数据迁移
老数据有Type的,需要指定type 没有的就不用指定
谷粒商城分布式高级篇(上)_第44张图片

分词

分词&安装ik分词

测试标准分词器的分词

POST _analyze
{
     
  "analyzer": "standard",
  "text": "which you can then accept by hitting Enter/Tab."
}

标准分词器会将中文分词一个一个字,不好用
谷粒商城分布式高级篇(上)_第45张图片
测试ik分词器的效果
谷粒商城分布式高级篇(上)_第46张图片

vagrant 密码登录

谷粒商城分布式高级篇(上)_第47张图片

补充-修改linux网络设置&开启root密码访问

这两个文件就是网卡文件
谷粒商城分布式高级篇(上)_第48张图片
修改eth1 文件
谷粒商城分布式高级篇(上)_第49张图片
重启网卡 service network restart
下载新 的 yum
设置国内的 yum 源
安装 wget 和 unzip

自定义扩展词库

free -m 查看虚拟机内存
谷粒商城分布式高级篇(上)_第50张图片
为虚拟机重新分配内存

之前创建ES 内存分配的有点小,现在移除掉 ES容器 重新创建ES容器

  1. 停止容器 docker stop 容器id
  2. 移除容器 docker rm 容器id
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

安装 nginx
先创建nginx的挂载目录

在这里插入图片描述
要进入到 /mydata/ 目录下再复制到当前目录
谷粒商城分布式高级篇(上)_第51张图片

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

在nginx 的html 目录中创建 词库
谷粒商城分布式高级篇(上)_第52张图片
词库中输入单词,回车分隔

进入 ES的plugins 中修改 ik分词的配置
谷粒商城分布式高级篇(上)_第53张图片
将刚创建的词库的地址填入
谷粒商城分布式高级篇(上)_第54张图片

整合

SpringBoot整合high-level-client

创建ES模块

只导入 WEB 依赖
谷粒商城分布式高级篇(上)_第55张图片

在pom中导入 ES依赖

<dependency>
    <groupId>org.elasticsearch.clientgroupId>
    <artifactId>elasticsearch-rest-high-level-clientartifactId>
    <version>7.4.2version>
dependency>

发现包管理中的 ES版本不对,因为spring-boot 会对ES的版本做管理
谷粒商城分布式高级篇(上)_第56张图片
所以在pom中单独指定版本
谷粒商城分布式高级篇(上)_第57张图片
做好nacos配置
参考官网的ES配置
官网文档
谷粒商城分布式高级篇(上)_第58张图片
编写配置类
谷粒商城分布式高级篇(上)_第59张图片
测试
谷粒商城分布式高级篇(上)_第60张图片

测试保存

在对ES做所有操作前要做 RequestOptions(请求的设置项)
带上安全头等设置信息
谷粒商城分布式高级篇(上)_第61张图片
测试保存 详细在 IndexAPI文档
谷粒商城分布式高级篇(上)_第62张图片

测试复杂检索

详细在文档 Search APIs

谷粒商城分布式高级篇(上)_第63张图片

商城业务

商品上架

sku在es中存储模型分析

谷粒商城分布式高级篇(上)_第64张图片
sku在es中的存储模型设计有两种
第一种、冗余设计,每个sku基本信息带上检索属性attrs的冗余
谷粒商城分布式高级篇(上)_第65张图片
第二种、避免冗余,分开索引,将attrs 新创建索引
但是在聚合规格做分析时,会动态计算所有sku的 attrs 就会造成沉重的分布查询
这种会造成超大的查询压力
所以采用 第一种设计的冗余存储查询,用空间换时间的思想
谷粒商城分布式高级篇(上)_第66张图片

商品的数据模型

PUT product
{
     
  "mappings": {
     
    "properties": {
     
      "skuId": {
     
        "type": "long"
      },
      "spuId": {
     
        "type": "long"
      },
      "skuTitle": {
     
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
     
        "type": "keyword"
      },
      "skuImg": {
     
        "type": "keyword",
        "index": false,
        "doc_values": false
      },
      "saleCount": {
     
        "type": "long"
      },
      "hasStock": {
     
        "type": "boolean"
      },
      "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"
          }
        }
      }
    }
  }
}

type:keyword 不可拆分的精确检索
index:false 不可用来被检索
doc_value:false 默认true默认可以用来聚合

nested数据类型场景

谷粒商城分布式高级篇(上)_第67张图片
nested 嵌入式的数据类型 官方解释
因为数组默认被扁平化处理了会出现错误的查询结果

构造基本数据

谷粒商城分布式高级篇(上)_第68张图片

构造sku检索属性

谷粒商城分布式高级篇(上)_第69张图片
谷粒商城分布式高级篇(上)_第70张图片

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

谷粒商城分布式高级篇(上)_第71张图片
谷粒商城分布式高级篇(上)_第72张图片
重新设计 R 工具类,加上泛型,可以解决远程调用后还要强转一次返回值
谷粒商城分布式高级篇(上)_第73张图片

谷粒商城分布式高级篇(上)_第74张图片
谷粒商城分布式高级篇(上)_第75张图片

远程上架接口

上架接口调试&feign源码

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

首页

整合thymeleaf渲染首页

  1. 导入thymeleaf依赖
  2. 关闭缓存
    在这里插入图片描述

谷粒商城分布式高级篇(上)_第76张图片

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

  1. 引入依赖并设置为true
    谷粒商城分布式高级篇(上)_第77张图片

渲染二级三级分类数据

nginx

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

谷粒商城分布式高级篇(上)_第78张图片
谷粒商城分布式高级篇(上)_第79张图片

  1. 修改host文件 增加 192.168.56.10 guliamll.com
  2. 查看nginx配置文件的组成,发现 总配置文件中集成多个 sever块,每个server块相当于一个站点
    谷粒商城分布式高级篇(上)_第80张图片
    每一个站点都可以在 conf.d 文件夹中配置一个文件
  3. 复制conf.d 文件夹 中默认的配置文件为新的配置
  4. 查看本机ip,更改nginx站点配置文件

谷粒商城分布式高级篇(上)_第81张图片
谷粒商城分布式高级篇(上)_第82张图片

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

在总配置文件中的http块中配置 upstream (上游服务器) 命名为 gulimall
设置为路由到88端口,路由到网关模块,再由网关分配
谷粒商城分布式高级篇(上)_第83张图片
站点配置文件中 配置上
谷粒商城分布式高级篇(上)_第84张图片
重启nginx
配置网关断言规则,网关接受nginx负载均衡来的请求 -Host断言 判断请求头中 的Host 的域名并 路由到指定 服务
谷粒商城分布式高级篇(上)_第85张图片

谷粒商城分布式高级篇(上)_第86张图片

配置完成后发现无法访问
因为nginx代理给网关的时候,会丢失请求的host信息,导致网关无法判断,其实nginx会丢掉很多信息
谷粒商城分布式高级篇(上)_第87张图片
必须在站点负载均衡时手动设置上请求头需要携带的信息

性能压测

压力测试

基本介绍

谷粒商城分布式高级篇(上)_第88张图片
谷粒商城分布式高级篇(上)_第89张图片
谷粒商城分布式高级篇(上)_第90张图片

Apache JMeter安装使用

  1. 安装JMeter

  2. 添加线程租,设置线程数
    在这里插入图片描述
    谷粒商城分布式高级篇(上)_第91张图片

  3. 添加取样器和监听器,在取样器中设置测试地址

JMeter在windows下地址占用bug解决

谷粒商城分布式高级篇(上)_第92张图片

性能监控

堆内存与垃圾回收


谷粒商城分布式高级篇(上)_第93张图片

jvisualvm使用

  1. cmd 输入 jvisualvm启动
  2. 输入正确插件地址后还是链接不上
  3. 手动安装,点击下载后
    谷粒商城分布式高级篇(上)_第94张图片
  4. 添加已下载的插件
    谷粒商城分布式高级篇(上)_第95张图片

优化

中间件对性能的影响

压测网关加简单请求
谷粒商城分布式高级篇(上)_第96张图片

在这里插入图片描述

压测内容 压测线程数 吞吐量/s 90%响应时间 99%响应时间 压测地址
Nginx 50 8459 7 67 192.168.60.10:80/
GateWay 50 30430 3 7 localhost:88/
简单服务 50 34552 2 6 localhost:10000/hello
首页一级菜单渲染 50 903(db,thymeleaf) 64 207 localhost:10000/
首页渲染(开缓存) 50 1107 54 89 localhost:10000/ (开缓存)
首页渲染(开缓存、优化Db、关日志) 50 1977 34 54 localhost:10000/ (开缓存、优化Db、关日志)
三级分类数据获取 50 8 (db) 6908 7109 loaclhost:10000/index/catalog.json
三级分类数据获取(业务优化) 50 207 263 689 loaclhost:10000/index/catalog.json
首页全量数据获取 50 47 (静态资源) 1170 1370 localhost:10000/ (高级设置)
首页全量数据获取(动静分离后,内存分配后) 50 268 966 6192 localhost:10000/ (高级设置)
Nginx+Gateway 50
Gateway+简单服务 50 8,445 9 16 localhost:88/hello
全链路 50 2539 30 46 gulimall.com:80/hello
  • 中间件越多,性能损失越大,大多损失在网络交互了
  • 业务
    • Db (MySQL 优化 )
    • 模板的渲染速度
    • 静态资源

简单优化吞吐量测试

首页渲染(开缓存、优化Db)

  1. 开thymeleaf缓存
  2. 关日志
  3. 为这个字段加上索引
    谷粒商城分布式高级篇(上)_第97张图片
    谷粒商城分布式高级篇(上)_第98张图片

nginx动静分离

谷粒商城分布式高级篇(上)_第99张图片

  1. 将静态资源上传至nginx html 静态资源目录下,删除本地的 静态资源
    谷粒商城分布式高级篇(上)_第100张图片
  2. 替换资源路径 index/img/xxx.xxx ----> staitc/index/img/xxx.xxx
  3. 增加nginx站点配置
    谷粒商城分布式高级篇(上)_第101张图片

配置意为 路径为/static/的地址 资源在 root /usr/share/nginx/html; 下寻找

模拟线上应用内存崩溃宕机情况

发现设置动静分离后提升不大,而老年代几乎爆满,慢在老年代的GC

谷粒商城分布式高级篇(上)_第102张图片
设置服务的最大内存占用重新分配内存
谷粒商城分布式高级篇(上)_第103张图片
配置意思分别为 最大内存 最小内存 新生代内存(伊甸园区+幸存者区)

优化三级分类数据获取

优化业务,只做一次查询

缓存

缓存使用

本地缓存与分布式缓存

谷粒商城分布式高级篇(上)_第104张图片
分布式部署时-本地缓存模式的问题

  1. 请求可能会到各个服务器,造成缓存不一致
  2. 更改缓存可能只更改到一台服务器,造成缓存不一致
    谷粒商城分布式高级篇(上)_第105张图片

解决方法 - 引入缓存中间件redis
谷粒商城分布式高级篇(上)_第106张图片

整合redis测试

  1. 引入redis springboot 的启动器
    谷粒商城分布式高级篇(上)_第107张图片

  2. 配置文件配置redis地址
    在这里插入图片描述

  3. 使用 springboot 自动配置好的 StringRedisTemplate 来操作 redis

  4. 测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallProductApplicationTests {
     

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void contextLoads() {
     
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

        ops.set("hello","world_"+ UUID.randomUUID().toString());

        String hello = ops.get("hello");

        System.out.println(hello);
    }

}

改造三级分类业务

@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
     
    //1、加入缓存逻辑,缓存中存的数据是json字符串
    //JSON 跨语言跨平台
    String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
    if (StringUtils.isEmpty(catalogJson)) {
     
        //缓存中没查到,查询数据库
        Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
        //查询到的数据先转为JSON再放入缓存
        String s = JSON.toJSONString(catelogJsonFromDb);
        stringRedisTemplate.opsForValue().set("catalogJson", s);
        return catelogJsonFromDb;
    }
    //缓存中查到,将JSON转为对象再返回
    Map<String, List<Catelog2Vo>> stringListMap =
            JSON.parseObject(catalogJson, 
            new TypeReference<Map<String, List<Catelog2Vo>>>() {
     });
    return stringListMap;

}

压力测试出的内存泄露及解决

压测 http://localhost:10000/index/catalog.json 出现 异常
在这里插入图片描述
堆外内存溢出
在这里插入图片描述

压测内容 压测线程数 吞吐量/s 90%响应时间 99%响应时间 压测地址
三级分类数据获取 50 8 (db) 6908 7109 loaclhost:10000/index/catalog.json
三级分类数据获取(业务优化) 50 207 263 689 loaclhost:10000/index/catalog.json
三级分类数据获取(业务优化、redis缓存) 50 915 70 99 loaclhost:10000/index/catalog.json

缓存击穿、穿透、雪崩

谷粒商城分布式高级篇(上)_第108张图片
谷粒商城分布式高级篇(上)_第109张图片

谷粒商城分布式高级篇(上)_第110张图片
谷粒商城分布式高级篇(上)_第111张图片

加锁解决缓存击穿问题

谷粒商城分布式高级篇(上)_第112张图片
测试本地锁


@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
     
     //1、加入缓存逻辑,缓存中存的数据是json字符串
     //JSON 跨语言跨平台
     String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
     if (StringUtils.isEmpty(catalogJson)) {
     
         //缓存中没查到,查询数据库
         System.out.println("缓存不命中,查询数据库");
         Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
         //查询到的数据先转为JSON再放入缓存
         String s = JSON.toJSONString(catelogJsonFromDb);
         stringRedisTemplate.opsForValue().set("catalogJson", s);
         return catelogJsonFromDb;
     }
     System.out.println("缓存命中,直接返回");
     //缓存中查到,将JSON转为对象再返回
     Map<String, List<Catelog2Vo>> stringListMap =
             JSON.parseObject(catalogJson, 
             new TypeReference<Map<String, List<Catelog2Vo>>>() {
      });
     return stringListMap;
}
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {
     

    synchronized (this){
     
        //得到锁后在缓存确认依次,如果没有再查询
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");

        if (!StringUtils.isEmpty(catalogJson)){
     
            //缓存不为空,直接返回
            return stringListMap;
        }
        System.out.println("查询了数据库");

        //1、查出一级分类
        List<CategoryEntity> level1Categorys = getParentCid(selectList, 0L);

        //2、封装分类
        业务逻辑


        return map;
    }

}


要保证查询和放入是一个原子操作,否则会出现在释放锁后放入缓存的间隙其他线程拿到锁再查询
谷粒商城分布式高级篇(上)_第113张图片

本地锁在分布式下的问题

idea启动多个商品服务,测试分布式下的本地锁功能
谷粒商城分布式高级篇(上)_第114张图片
更改名字和端口就能启动多个服务
谷粒商城分布式高级篇(上)_第115张图片
压测地址为 http gulimall.com 80 由网关分配给各个服务
测试结果如预期每一个服务都单独查询了数据 库

分布式锁

分布式锁原理与使用

谷粒商城分布式高级篇(上)_第116张图片
用 xshell 测试 redis 占坑
谷粒商城分布式高级篇(上)_第117张图片
撰写栏中发送给所有会话
谷粒商城分布式高级篇(上)_第118张图片
改造
谷粒商城分布式高级篇(上)_第119张图片
这种方法也会出现死锁问题,因为在 执行业务时可能会发生异常导致没有及时删除锁,这就造成死锁
解决办法,给锁设置自动过期时间
谷粒商城分布式高级篇(上)_第120张图片
这种也会出现问题,可能在拿到锁后发生意外,程序没有执行到设置过期时间而造成了死锁
解决办法,拿锁和设置时间一条命令完成
redis 命令为
谷粒商城分布式高级篇(上)_第121张图片
代码方法为
谷粒商城分布式高级篇(上)_第122张图片
这种也会产生一个问题,删锁问题

  1. 业务超时,锁已经过期了,这时候别的线程进来了,再删锁就删掉了别的线程的锁
  2. 解决办法,给自己的锁设置唯一id,且删锁时获取锁的id和删除锁作为原子操作
    谷粒商城分布式高级篇(上)_第123张图片
  3. 锁的自动续期,避免业务还没执行完,而锁却过期了,给锁的过期时间设置长点如300秒,因为不可能有业务执行时间超过300秒,并给整体业务加入 try final ,无论业务结果如何,最终都会释放锁
    谷粒商城分布式高级篇(上)_第124张图片

谷粒商城分布式高级篇(上)_第125张图片
主要注意点

  1. 获取锁和设置锁的过期时间为原子操作
  2. 找到锁和删除锁为原子操作

Redisson简介&整合

  1. 引入依赖
  2. 写入配置整合 官方文档

Redisson-lock锁测试

可重入锁:嵌套调用可以重复使用的锁,所有的锁都应该设置为可重入锁,避免死锁问题

简单解释: 方法A中调用方法B ,方法A有lock1 方法B也能使用 lock1 B执行完后 A释放锁程序结束,如果不可重入锁,B无法拿到方法持有的lock1 锁,这就形成了死锁,所以所有的锁都应该设计为可重入锁

测试

  1. 开启两个线程,同时发请求,一个线程拿到锁后,还没释放锁就关闭线程
  2. 另一个线程还是能拿到锁并访问成功
  3. 因为redisson的锁是一个阻塞式等待 lock.lock();方法 如果拿不到锁就会一直停留在这儿等待锁的释放,默认加锁时间是30s
  4. redisson看门狗,能对锁进行自动续期,如果业务超长,运行期间会自动给锁续上新的30s,不用担心业务时间长,锁自动过期被删掉
  5. 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后删除
@ResponseBody
@GetMapping("/hello")
public String hello(){
     
    //1、获取一把锁,只要锁的名字一样,就是同一把锁
    RLock lock = redisson.getLock("my-lock");

    //2、加锁
    lock.lock();//阻塞式等待

    try {
     
        System.out.println("加锁成功:"+Thread.currentThread().getId());
        Thread.sleep(10000);
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    } finally {
     
        System.out.println("释放锁");
        lock.unlock();
    }

    return "hello";
}

Redisson-lock看门狗原理-redisson如何解决死锁

可以给锁指定过期时间
lock.lock(10, TimeUnit.SECONDS);但是这个方法无法给锁自动续期,到时锁自动删除
在这里插入图片描述
谷粒商城分布式高级篇(上)_第126张图片
推荐手动设置超时时间

Redisson-读写锁测试

读写锁文档
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

实现一个效果,写锁在写入数据时,读锁必须等待写锁的释放,
在这里插入图片描述

    //写锁
    @ResponseBody
    @GetMapping("/write")
    public String writeLock() {
     
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.writeLock();
        rLock.lock();
        try {
     
            System.out.println("写锁加锁成功。。。" + Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set("writeValue", s);
            Thread.sleep(10000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            rLock.unlock();
            System.out.println("写锁释放。。。" + Thread.currentThread().getId());
        }
        return s;
    }


    @ResponseBody
    @GetMapping("/read")
    public String readLock() {
     
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.readLock();
        rLock.lock();
        try {
     
            System.out.println("读锁加锁成功。。。" + Thread.currentThread().getId());

            s = redisTemplate.opsForValue().get("writeValue");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            rLock.unlock();
            System.out.println("读锁释放。。。" + Thread.currentThread().getId());
        }
        return s;
    }

读写锁补充

读锁还没释放时,写锁也需要等待
谷粒商城分布式高级篇(上)_第127张图片

闭锁测试

设置等待次数,达到次数才能释放锁

@ResponseBody
@GetMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
     
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();
    return "全部释放完成";
}

@ResponseBody
@GetMapping("/gogo")
public String gogo() throws InterruptedException {
     
    RCountDownLatch door = redisson.getCountDownLatch("door");
    //计数-1,锁的数-1 (i--)
    door.countDown();
    long count = door.getCount();
    return "走了....剩余"+count;
}

信号量测试

信号量可以用来限量
acquiretryAcquire的区别
acquire 为阻塞式等待,会一直等到释放锁在执行操作
tryAcquire 非阻塞式等待,没有锁就直接返回false

//信号量
@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
     
    RSemaphore park = redisson.getSemaphore("park");
    //park.acquire();//是阻塞式获取无返回值、一定要获取一个占位才能继续执行
    boolean b = park.tryAcquire();//非阻塞、有量就返回true,无量返回false,继续向下执行
    if (b){
     
        return "有位置了";
    }else {
     
        return "没位置";
    }
}

@ResponseBody
@GetMapping("/gocar")
public String goCar(){
     
    RSemaphore park = redisson.getSemaphore("park");
    park.release();//释放一个信号 释放一个值 释放一个车位
    return "ok";
}

缓存一致性解决

redisson 改造 之前手写的 redis 锁

如何保持缓存一致性

  1. 设置缓存过期时间能解决大部分业务的缓存需求,一段时间过后,缓存必然会更新,做到最终一致性
  2. 读写锁,在写的时候不能读,写完后再都就是一致性了
    谷粒商城分布式高级篇(上)_第128张图片

SpringCache

简介

谷粒商城分布式高级篇(上)_第129张图片

谷粒商城分布式高级篇(上)_第130张图片

整合&体验@Cacheable

1. 引入依赖
 spring-boot-starter-cache、spring-Boot-starter-data-redis
2. 写配置
	application.properties文件中 写入缓存类型 spring.cache.type=redis
3. 配置类开启缓存
@EnableConfigurationProperties(CacheProperties.class)
//开启缓存功能
@EnableCaching
@Configuration
public class MyCacheConfig {
     

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
     

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config = config.entryTtl();
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        //将配置文件中的所有配置都生效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
     
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
     
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
     
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
     
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

@Cacheable细节设置

1. 可以给缓存的key设置名字
2. 可以用ttl表达式获取参数的名字作为key值

在这里插入图片描述
谷粒商城分布式高级篇(上)_第131张图片

自定义缓存配置

  1. 自定义配置类中配置存入redis key和value的数据为json
  2. 生效配置文件
@EnableConfigurationProperties(CacheProperties.class)
//开启缓存功能
@EnableCaching
@Configuration
public class MyCacheConfig {
     

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
     

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config = config.entryTtl();
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        //将配置文件中的所有配置都生效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
     
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
     
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
     
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
     
            config = config.disableKeyPrefix();
        }
        return config;
    }
}
#以下是整合Spring Cache 的相关配置
#配置缓存的类型 (最简化的配置)
spring.cache.type=redis

#指定缓存的名字
#spring.cache.cache-names=qq,

#指定缓存的存活时间 单位:ms
spring.cache.redis.time-to-live=3600000

#为了区分redis其他的东西
#如果指定了前缀,就用我们指定的前缀,如果没有就默认使用缓存的名字(分区名-value)作为前缀
#优先级高
#spring.cache.redis.key-prefix=CACHE_
#默认是使用前缀的
spring.cache.redis.use-key-prefix=true

#是否缓存空值 防止缓存穿透
spring.cache.redis.cache-null-values=true

@CacheEvict

@CacheEvict 删除 触发将数据从缓存删除的操作

调用方法将删除redis缓存

谷粒商城分布式高级篇(上)_第132张图片
更改业务代码,加入缓存注解,取消手动缓存的操作

@Cacheable(value = "category",key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson(){
     
    System.out.println("查数据库");
    List<CategoryEntity> selectList = baseMapper.selectList(null);


    //1、查出一级分类
    //2、封装分类
   	//业务逻辑
   	
    return map;
}

@Caching 注解 多个操作,同时删除多个缓存
谷粒商城分布式高级篇(上)_第133张图片
指定删除分区下的数据
@CacheEvict(value = "category", allEntries = true)
谷粒商城分布式高级篇(上)_第134张图片

原理与不足

谷粒商城分布式高级篇(上)_第135张图片

商城业务

检索服务

搭建页面环境

  1. search 服务加入thymeleaf

  2. template目录放入html页面

  3. 静态资源导入nginx 静态资源目录下

  4. 配置本地转发到虚拟机
    在这里插入图片描述

  5. 修改nginx站点配置文件 统一转至88网关模块端口,发现 *. 不起作用
    谷粒商城分布式高级篇(上)_第136张图片
    换成一一列出
    谷粒商城分布式高级篇(上)_第137张图片

  6. 网关模块转发至 search 服务

        - id: gulimall_search_route
          uri: lb://gulimall-search
          predicates:
            - Host=search.gulimall.com
  1. 引入devtools和关闭thymeleaf缓存

调整页面跳转

  1. 侧边栏的点击跳转
    在这里插入图片描述

  2. 搜索框的点击跳转

检索查询参数模型分析抽取

所有的检索条件抽取为一个对象传输
谷粒商城分布式高级篇(上)_第138张图片
抽象一个处理检索的service
谷粒商城分布式高级篇(上)_第139张图片

检索返回结果模型分析抽取

分析要返回页面的数据,和能进行检索的属性设计返回模型

@Data
public class SearchParam {
     
    private String keyword;//页面传递过来的检索参数 相当于全文匹配关键字
    private Long catalog3Id;//三级分类id

    /**
     * 排序条件
     *  sort=saleCount_asc/desc 倒序
     *  sort=skuPrice_asc/desc 根据价格
     *  sort=hotScore_asc/desc
     */
    private String sort;

    /**
     * hasStock(是否有货) skuPrice区间 brandId catalog3Id attrs
     * hasStock 0/1
     * skuPrice=1_500 500_ _500
     * brandId = 1
     * attrs1_5寸_6寸
     * // 0 无库存 1有库存
     */
    private Integer hasStock;

    /**
     * 价格区查询
     */
    private String skuPrice;

    /**
     * 多个品牌id
     */
    private List<Long> brandId;

    /**
     * 按照属性进行筛选
     */
    private List<String> attrs;

    /**
     * 页码
     */
    private Integer pageNum = 1;
}

检索DSL测试-查询部分

DSL依次包含有

  1. 模糊匹配
  2. 过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
GET newproduct/_search
{
     
  "query": {
     
    "bool": {
     
      "must": [
        {
     
          "match": {
     
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
     
          "term": {
     
            "catalogId": "225"
          }
        },
        {
     
          "terms": {
     
            "brandId": [
              "1",
              "2",
              "3"
            ]
          }
        },
        {
     
          "nested": {
     
            "path": "attrs",
            "query": {
     
              "bool": {
     
                "must": [
                  {
     
                    "term": {
     
                      "attrs.attrId": {
     
                        "value": "11"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        {
     
          "term": {
     
            "hasStock": false
          }
        },
        {
     
          "range": {
     
            "skuPrice": {
     
              "gte": 0,
              "lte": 5000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
     
      "skuPrice": {
     
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 1,
  "highlight": {
     
    "fields": {
     "skuTitle": {
     }}, 
    "pre_tags": "",
    "post_tags": ""
  }
}

检索DSL测试-聚合部分

按品牌id聚合就会列出所含有的brandId
谷粒商城分布式高级篇(上)_第140张图片
嵌套聚合,用上一层聚合出来的brandId 聚合出 brandName
谷粒商城分布式高级篇(上)_第141张图片
发现报错,错误为,type为Keyword的属性不能用来聚合
所以重新put一个索引,更改属性

PUT gulimall_product
{
     
  "mappings": {
     
    "properties": {
     
      "skuId": {
     
        "type": "long"
      },
      "spuId": {
     
        "type": "long"
      },
      "skuTitle": {
     
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
     
        "type": "keyword"
      },
      "skuImg": {
     
        "type": "keyword"
      },
      "saleCount": {
     
        "type": "long"
      },
      "hasStock": {
     
        "type": "boolean"
      },
      "brandId": {
     
        "type": "long"
      },
      "catalogId": {
     
        "type": "long"
      },
      "brandName": {
     
        "type": "keyword"
      },
      "brandImg": {
     
        "type": "keyword"
      },
      "catalogName": {
     
        "type": "keyword"
      },
      "attrs": {
     
        "type": "nested",
        "properties": {
     
          "attrId": {
     
            "type": "long"
          },
          "attrName": {
     
            "type": "keyword"
          },
          "attrValue": {
     
            "type": "keyword"
          }
        }
      }
    }
  }
}

迁移数据

POST _reindex
{
     
  "source": {
     
    "index": "product"
    
  }
  ,"dest": {
     
    "index": "gulimall_product"
  }
}

更改常量中的索引
在这里插入图片描述
单独的聚合语句,注意属性为嵌入式的聚合有些许不同

GET gulimall_product/_search
{
     
  "query": {
     
    "match_all": {
     }
  },
  "aggs": {
     
    "brand_agg": {
     
      "terms": {
     
        "field": "brandId",
        "size": 10
      },
      "aggs": {
     
        "brand_name_agg": {
     
          "terms": {
     
            "field": "brandName",
            "size": 1
          }
        },
        "brand_img_agg":{
     
          "terms": {
     
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalog_agg": {
     
      "terms": {
     
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
     
        "catalog_name_agg": {
     
          "terms": {
     
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg":{
     
      "nested": {
     
        "path": "attrs"
      },
      "aggs": {
     
        "attr_id_agg": {
     
          "terms": {
     
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
     
            "attr_name_agg": {
     
              "terms": {
     
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attr_value_agg": {
     
              "terms": {
     
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

SearchRequest构建-检索

分为三大部分

  1. 准备检索请求。动态构建出查询需要的DSL语句(将上一节分析列出的DSL用 api 构造出)
  2. 执行检索请求 (传入用api构造的DSL )
  3. 分析响应数据封装成我们需要的格式(操作上一步api返回的数据,封装成需要的格式)
    谷粒商城分布式高级篇(上)_第142张图片
    分步编写
    一、通过api构建DSL查询语句的方法,大致步骤
/**
 * 准备检索请求
 * 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存)、排序、分页、高亮、聚合分析
 *
 * @return SearchResult
 */
private SearchRequest buildSearchRequest(SearchParam param) {
     
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句

    /**
     * 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存)
     */
    //1、构建bool - query
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    //1.1、must - 模糊匹配
    if (!StringUtils.isEmpty(param.getKeyword())) {
     
        boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
    }
    //1.2、filter - catalogId
    if (!StringUtils.isEmpty(param.getCatalog3Id())) {
     
        boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
    }
    //1.3、filter - brandId
    if (!StringUtils.isEmpty(param.getBrandId())) {
     
        boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
    }
    //1.4、filter - nested
    if (param.getAttrs() != null && param.getAttrs().size() > 0) {
     
        //attrs=1_5寸:8寸&attrs=2_16g:8g
        for (String attrStr : param.getAttrs()) {
     
            BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
            //attr = 1_5寸:8寸
            String[] s = attrStr.split("_");
            String attrId = s[0];//检索属性的id
            String[] attrValues = s[1].split(":");//检索属性的值
            nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
            nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
            //每一个都单独生成一个nested查询
            NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
            boolQuery.filter(nestedQuery);
        }
    }

    //1.5、filter - hasStock
    boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));

    //1.6、filter - range 区间
    if (!StringUtils.isEmpty(param.getSkuPrice())) {
     
        //  1_500  or  _500  or  500_
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
        String[] s = param.getSkuPrice().split("_");
        if (s.length == 2) {
     
            //区间
            rangeQuery.gte(s[0]).lte(s[1]);
        } else if (s.length == 1) {
     
            if (param.getSkuPrice().startsWith("_")) {
     
                rangeQuery.lte(s[0]);
            }
            if (param.getSkuPrice().endsWith("_")) {
     
                rangeQuery.gte(s[0]);
            }

        }
        boolQuery.filter(rangeQuery);
    }


    //把所有条件都拿来封装,query部分结束
    sourceBuilder.query(boolQuery);

    /**
     * 排序、分页、高亮
     */
    //2.1 sort
    if (!StringUtils.isEmpty(param.getSort())) {
     
        String sort = param.getSort();
        //sort=hotScore_asc
        String[] s = sort.split("_");
        SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
        sourceBuilder.sort(s[0], order);
    }
    //2.2 分页
    sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
    sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
    //2.3 高亮
    if (!StringUtils.isEmpty(param.getKeyword())) {
     
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("skuTitle");
        highlightBuilder.preTags("");
        highlightBuilder.postTags("");
        sourceBuilder.highlighter(highlightBuilder);
    }


    /**
     * 聚合分析
     */

    System.out.println("构建的DSL语句" + sourceBuilder.toString());

    SearchRequest searchRequest = new SearchRequest(new String[]{
     EsConstant.PRODUCT_INDEX}, sourceBuilder);
    return searchRequest;
}

谷粒商城分布式高级篇(上)_第143张图片

SearchRequest构建-聚合

根据DSL语句依次聚合,难点在 nested 的聚合

//TODO 聚合分析

//1、品牌聚合 brand_agg
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId").size(50);

//1.1 品牌聚合的子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
sourceBuilder.aggregation(brand_agg);

//2、分类聚合 catalog_agg
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
catalog_agg.field("catalogId").size(50);
//2.1 分类聚合的子聚合
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
sourceBuilder.aggregation(catalog_agg);

//3、属性聚合 (nested) attr_agg
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//3.1 属性聚合的子聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
//3.1 属性聚合的子聚合的子聚合
//聚合分析出当前attr_id对应的名字
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
//聚合分析出当前attr_id对应的所有可能的属性值atrValue
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
//依次放入
attr_agg.subAggregation(attr_id_agg);
sourceBuilder.aggregation(attr_agg);

SearchResponse分析&封装

构造页面需要的类的结果大致步骤

/**
 * 构造结果数据
 *
 * @return SearchResult
 */
private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {
     
    SearchResult result = new SearchResult();
    //1、返回所有查询到的商品
    result.setProducts();
    //2、当前所有商品涉及到的所有属性信息
    result.setAttrs();
    //3、当前有所商品涉及到的所有分类信息
    result.setBrands();
    //4、当前所有商品涉及到的所有分类信息
    result.setCatalogs();
    //5、分页信息-页码
    result.setPageNum();
    //5.1、分页信息总记录数
    result.setTotal();
    //5.2、分页信息-总页码
    result.setTotalPages();
    return result;
}

debug 断点在此方法,分析传入的 SearchResponse

在这里插入图片描述
就可以根据断点分析所需要封装的数据,和数据的类型
谷粒商城分布式高级篇(上)_第144张图片

验证结果封装正确性

debug

页面基本数据渲染

将返回的数据通过thymeleaf渲染到页面

页面筛选条件渲染

编写一个统一函数,点击属性值统一调用跳转
报错无法处理特殊字符
在这里插入图片描述
改为双引号的转义字符 "
在这里插入图片描述

function searchProducts(name, value) {
     
    //原来页面的值
    var href = location.href + ""
    if (href.indexOf("?") != -1){
     
        location.href = location.href + "&" + name + "=" + value;
    }else {
     
        location.href = location.href + "?" + name + "=" + value;
    }
}

页面分页数据渲染

为搜索按钮加入筛选

function searchByKeyword(){
     
    searchProducts("keyword",$("#keyword_input").val());
}

还是如上一步方法,在链接加入参数
为返回类型加入记录页数以便 thymeleaf 循环

谷粒商城分布式高级篇(上)_第145张图片
谷粒商城分布式高级篇(上)_第146张图片
页面部分,上一页就是当前页码减一,并自定义一个属性值pn。循环出封装的页数,
谷粒商城分布式高级篇(上)_第147张图片
页码class的点击部分,点击后将当前页码拼接或替换到链接

//分页被点击后
$(".page_a").click(function () {
     
    //拿到当前属性中自定义属性pn 用户记录当前记录数 默认是1
    var pn = $(this).attr("pn");
    //拿到当前连接
    var href = location.href;
    console.log(href)
    //连接存在pagenum字段
    console.log(href.indexOf("pageNum"))
    //没找到
    if (href.indexOf("pageNum") != -1) {
     
        //替换pageNum的值
        location.href = replaceAndAddParamVal(href, "pageNum", pn);
    } else {
     
        let c = replaceAndAddParamVal(location.href, "pageNum", pn, true);
        location.href = c;
        //否则
        // location.href = location.href + "&pageNum=" + pn;
    }
    return false
})

页面排序功能

为排序按钮绑定单击事件
在这里插入图片描述
js代码部分就不记录了

页面排序字段回显

页面价格区间搜索

面包屑导航

  1. 返回模型SearchResult中加入面包屑的集合属性

谷粒商城分布式高级篇(上)_第148张图片
2. buildSearchResult方法内继续封装面包屑,重新包装远程接口返回的AttrRespVoAttrResponseVo

//6、构建面包屑导航功能
List<SearchResult.NavVo> navVos = param.getAttrs().stream().map(attr -> {
     
    SearchResult.NavVo navVo = new SearchResult.NavVo();
    //attrs=2_5寸:6寸 封装地址参数中存在的 属性值
    String[] s = attr.split("_");
    navVo.setNavValue(s[0]);
    R r = productFeignService.attrInfo(Long.parseLong(s[0]));
    if (r.getCode() == 0){
     
        AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
     
        });
        navVo.setNavName(data.getAttrName());
    }else {
     
        navVo.setNavName(s[0]);
    }
    //取消了这个面包屑后要跳转的地方
    return navVo;
}).collect(Collectors.toList());

result.setNavs(navVos);

条件删除与URL编码问题

点击取消了这个面包屑后要跳转的地方,将请求地址的url里面的当前置空

拿到所有的查询条件,去掉当前
在这里插入图片描述
在controller中 调用原生的 severlet 可获取到参数部分的字符串,在返回类型中添加参数部分的属性
谷粒商城分布式高级篇(上)_第149张图片
谷粒商城分布式高级篇(上)_第150张图片
直接去掉当前attrs参数,注意链接的编码转化问题

条件筛选联动

end

你可能感兴趣的:(学习笔记,java基础,自学笔记,java,spring,boot,elasticsearch,分布式)