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

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

文章目录


全文检索-ElasticSearch

简介

在这里插入图片描述
类比到MySQL里

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

在这里插入图片描述

在这里插入图片描述

Docker安装ES

在这里插入图片描述

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
在这里插入图片描述
赋予权限
在这里插入图片描述
重新启动容器
在这里插入图片描述

Docker安装Kibana

postman 发送查询
在这里插入图片描述

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

在这里插入图片描述

put&post新增数据

在这里插入图片描述

在这里插入图片描述

get查询数据&乐观锁字段

在这里插入图片描述
乐观锁做并发修改
在这里插入图片描述
每次修改 _seq_no 都会改变 修改时带上这个值才能成功
在这里插入图片描述

put&post修改数据

在这里插入图片描述

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

put 同上

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

在这里插入图片描述
没有删除类型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
导入测试数据,测试数据上传到了网盘

在这里插入图片描述
链接:https://pan.baidu.com/s/1BJ6_6EAhjmTNdSgXjB4TcQ
提取码:hnfd

进阶

两种查询方式

docker 容器自启动
在这里插入图片描述
文档
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
将查询条件写为json的方式成为 Query DSL(查询领域对象语言)

QueryDSL基本使用&match_all

在这里插入图片描述
在这里插入图片描述
请求体中的各个参数就像sql中的查询条件
在这里插入图片描述
match_all = select *

match全文检索 匹配查询

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

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

又可以模糊查询

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

match_phrase短语匹配

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

在这里插入图片描述

multi_match多字段匹配

在这里插入图片描述
也做了分词
在这里插入图片描述

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区间
在这里插入图片描述

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

用filter过滤区间就不会如上查询获得相关性得分
在这里插入图片描述

term查询

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

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

.keyword 精确匹配

每一个文本字段都可以 .keyword 代表匹配文本字段的整个精确值(不分词匹配)
和 match_phrase 短语匹配的区别
在这里插入图片描述
match_phrase 短语匹配的区别
.keyword 匹配的值中 只能全等于 这个值
match_phrase 匹配的值中 包含 这个值 (此短语)
在这里插入图片描述

aggregations聚合分析

在这里插入图片描述
聚合语法
在这里插入图片描述
为query的查询结果做聚合,有多少种不同的 age字段的值 size 前10个可能
terms聚合,用来查看值有多少种可能

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

在这里插入图片描述
指定显示hits的条数size
在这里插入图片描述
聚合中再子聚合 先聚合出年龄段 再聚合年龄段的平均薪资
在这里插入图片描述

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

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 )
在这里插入图片描述
在这里插入图片描述
_mapping可以查看当前所有的所有属性的类型
在这里插入图片描述
第一次存数据的时候,ES就会猜测 属性的类型

可以在第一次保存数据前可以给索引指定映射,创建索引时指定映射
在这里插入图片描述

Mapping Type 过时

在6.0版本移除了映射类型
因为ES底层是Lucene开发的
在这里插入图片描述

在这里插入图片描述

添加新的字段映射

新增一个属性的映射
index:false为此映射不需要被索引
不写的话 都是默认 为 index:true
是用来控制这个属性是不是来参与检索的
相当于做了一个冗余字段
在这里插入图片描述

修改映射&数据迁移

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

创建新索引并指定好映射

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 没有的就不用指定
在这里插入图片描述

分词

分词&安装ik分词

测试标准分词器的分词

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

标准分词器会将中文分词一个一个字,不好用
在这里插入图片描述
测试ik分词器的效果
在这里插入图片描述

vagrant 密码登录

在这里插入图片描述

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

这两个文件就是网卡文件
在这里插入图片描述
修改eth1 文件
在这里插入图片描述
重启网卡 service network restart
下载新 的 yum
设置国内的 yum 源
安装 wget 和 unzip

自定义扩展词库

free -m 查看虚拟机内存
在这里插入图片描述
为虚拟机重新分配内存

之前创建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/ 目录下再复制到当前目录
在这里插入图片描述

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 目录中创建 词库
在这里插入图片描述
词库中输入单词,回车分隔

进入 ES的plugins 中修改 ik分词的配置
在这里插入图片描述
将刚创建的词库的地址填入
在这里插入图片描述

整合

SpringBoot整合high-level-client

创建ES模块

只导入 WEB 依赖
在这里插入图片描述

在pom中导入 ES依赖

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
</dependency>

发现包管理中的 ES版本不对,因为spring-boot 会对ES的版本做管理
在这里插入图片描述
所以在pom中单独指定版本
在这里插入图片描述
做好nacos配置
参考官网的ES配置
官网文档
在这里插入图片描述
编写配置类
在这里插入图片描述
测试
在这里插入图片描述

测试保存

在对ES做所有操作前要做 RequestOptions(请求的设置项)
带上安全头等设置信息
在这里插入图片描述
测试保存 详细在 IndexAPI文档
在这里插入图片描述

测试复杂检索

详细在文档 Search APIs

在这里插入图片描述

商城业务

商品上架

sku在es中存储模型分析

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

商品的数据模型

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数据类型场景

在这里插入图片描述
nested 嵌入式的数据类型 官方解释
因为数组默认被扁平化处理了会出现错误的查询结果

构造基本数据

在这里插入图片描述

构造sku检索属性

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
重新设计 R 工具类,加上泛型,可以解决远程调用后还要强转一次返回值
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

远程上架接口

上架接口调试&feign源码

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

首页

整合thymeleaf渲染首页

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

在这里插入图片描述

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

  1. 引入依赖并设置为true
    在这里插入图片描述

渲染二级三级分类数据

nginx

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

在这里插入图片描述
在这里插入图片描述

  1. 修改host文件 增加 192.168.56.10 guliamll.com
  2. 查看nginx配置文件的组成,发现 总配置文件中集成多个 sever块,每个server块相当于一个站点
    在这里插入图片描述
    每一个站点都可以在 conf.d 文件夹中配置一个文件
  3. 复制conf.d 文件夹 中默认的配置文件为新的配置
  4. 查看本机ip,更改nginx站点配置文件

在这里插入图片描述
在这里插入图片描述

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

在总配置文件中的http块中配置 upstream (上游服务器) 命名为 gulimall
设置为路由到88端口,路由到网关模块,再由网关分配
在这里插入图片描述
站点配置文件中 配置上
在这里插入图片描述
重启nginx
配置网关断言规则,网关接受nginx负载均衡来的请求 -Host断言 判断请求头中 的Host 的域名并 路由到指定 服务
在这里插入图片描述

在这里插入图片描述

配置完成后发现无法访问
因为nginx代理给网关的时候,会丢失请求的host信息,导致网关无法判断,其实nginx会丢掉很多信息
在这里插入图片描述
必须在站点负载均衡时手动设置上请求头需要携带的信息

性能压测

压力测试

基本介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Apache JMeter安装使用

  1. 安装JMeter

  2. 添加线程租,设置线程数
    在这里插入图片描述
    在这里插入图片描述

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

JMeter在windows下地址占用bug解决

在这里插入图片描述

性能监控

堆内存与垃圾回收


在这里插入图片描述

jvisualvm使用

  1. cmd 输入 jvisualvm启动
  2. 输入正确插件地址后还是链接不上
  3. 手动安装,点击下载后
    在这里插入图片描述
  4. 添加已下载的插件
    在这里插入图片描述

优化

中间件对性能的影响

压测网关加简单请求
在这里插入图片描述

在这里插入图片描述

压测内容压测线程数吞吐量/s90%响应时间99%响应时间压测地址
Nginx508459767192.168.60.10:80/
GateWay503043037localhost:88/
简单服务503455226localhost:10000/hello
首页一级菜单渲染50903(db,thymeleaf)64207localhost:10000/
首页渲染(开缓存)5011075489localhost:10000/ (开缓存)
首页渲染(开缓存、优化Db、关日志)5019773454localhost:10000/ (开缓存、优化Db、关日志)
三级分类数据获取508 (db)69087109loaclhost:10000/index/catalog.json
三级分类数据获取(业务优化)50207263689loaclhost:10000/index/catalog.json
首页全量数据获取5047 (静态资源)11701370localhost:10000/ (高级设置)
首页全量数据获取(动静分离后,内存分配后)502689666192localhost:10000/ (高级设置)
Nginx+Gateway50
Gateway+简单服务508,445916localhost:88/hello
全链路5025393046gulimall.com:80/hello
  • 中间件越多,性能损失越大,大多损失在网络交互了
  • 业务
    • Db (MySQL 优化 )
    • 模板的渲染速度
    • 静态资源

简单优化吞吐量测试

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

  1. 开thymeleaf缓存
  2. 关日志
  3. 为这个字段加上索引
    在这里插入图片描述
    在这里插入图片描述

nginx动静分离

在这里插入图片描述

  1. 将静态资源上传至nginx html 静态资源目录下,删除本地的 静态资源
    在这里插入图片描述
  2. 替换资源路径 index/img/xxx.xxx ----> staitc/index/img/xxx.xxx
  3. 增加nginx站点配置
    在这里插入图片描述

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

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

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

在这里插入图片描述
设置服务的最大内存占用重新分配内存
在这里插入图片描述
配置意思分别为 最大内存 最小内存 新生代内存(伊甸园区+幸存者区)

优化三级分类数据获取

优化业务,只做一次查询

缓存

缓存使用

本地缓存与分布式缓存

在这里插入图片描述
分布式部署时-本地缓存模式的问题

  1. 请求可能会到各个服务器,造成缓存不一致
  2. 更改缓存可能只更改到一台服务器,造成缓存不一致
    在这里插入图片描述

解决方法 - 引入缓存中间件redis
在这里插入图片描述

整合redis测试

  1. 引入redis springboot 的启动器
    在这里插入图片描述

  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 出现 异常
在这里插入图片描述
堆外内存溢出
在这里插入图片描述

压测内容压测线程数吞吐量/s90%响应时间99%响应时间压测地址
三级分类数据获取508 (db)69087109loaclhost:10000/index/catalog.json
三级分类数据获取(业务优化)50207263689loaclhost:10000/index/catalog.json
三级分类数据获取(业务优化、redis缓存)509157099loaclhost:10000/index/catalog.json

缓存击穿、穿透、雪崩

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

加锁解决缓存击穿问题

在这里插入图片描述
测试本地锁


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

}


要保证查询和放入是一个原子操作,否则会出现在释放锁后放入缓存的间隙其他线程拿到锁再查询
在这里插入图片描述

本地锁在分布式下的问题

idea启动多个商品服务,测试分布式下的本地锁功能
在这里插入图片描述
更改名字和端口就能启动多个服务
在这里插入图片描述
压测地址为 http gulimall.com 80 由网关分配给各个服务
测试结果如预期每一个服务都单独查询了数据 库

分布式锁

分布式锁原理与使用

在这里插入图片描述
用 xshell 测试 redis 占坑
在这里插入图片描述
撰写栏中发送给所有会话
在这里插入图片描述
改造
在这里插入图片描述
这种方法也会出现死锁问题,因为在 执行业务时可能会发生异常导致没有及时删除锁,这就造成死锁
解决办法,给锁设置自动过期时间
在这里插入图片描述
这种也会出现问题,可能在拿到锁后发生意外,程序没有执行到设置过期时间而造成了死锁
解决办法,拿锁和设置时间一条命令完成
redis 命令为
在这里插入图片描述
代码方法为
在这里插入图片描述
这种也会产生一个问题,删锁问题

  1. 业务超时,锁已经过期了,这时候别的线程进来了,再删锁就删掉了别的线程的锁
  2. 解决办法,给自己的锁设置唯一id,且删锁时获取锁的id和删除锁作为原子操作
    在这里插入图片描述
  3. 锁的自动续期,避免业务还没执行完,而锁却过期了,给锁的过期时间设置长点如300秒,因为不可能有业务执行时间超过300秒,并给整体业务加入 try final ,无论业务结果如何,最终都会释放锁
    在这里插入图片描述

在这里插入图片描述
主要注意点

  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);但是这个方法无法给锁自动续期,到时锁自动删除
在这里插入图片描述
在这里插入图片描述
推荐手动设置超时时间

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

读写锁补充

读锁还没释放时,写锁也需要等待
在这里插入图片描述

闭锁测试

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

@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. 读写锁,在写的时候不能读,写完后再都就是一致性了
    在这里插入图片描述

SpringCache

简介

在这里插入图片描述

在这里插入图片描述

整合&体验@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值

在这里插入图片描述
在这里插入图片描述

自定义缓存配置

  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缓存

在这里插入图片描述
更改业务代码,加入缓存注解,取消手动缓存的操作

@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 注解 多个操作,同时删除多个缓存
在这里插入图片描述
指定删除分区下的数据
@CacheEvict(value = "category", allEntries = true)
在这里插入图片描述

原理与不足

在这里插入图片描述

商城业务

检索服务

搭建页面环境

  1. search 服务加入thymeleaf

  2. template目录放入html页面

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

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

  5. 修改nginx站点配置文件 统一转至88网关模块端口,发现 *. 不起作用
    在这里插入图片描述
    换成一一列出
    在这里插入图片描述

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

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

调整页面跳转

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

  2. 搜索框的点击跳转

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

所有的检索条件抽取为一个对象传输
在这里插入图片描述
抽象一个处理检索的service
在这里插入图片描述

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

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

@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": "<b style='color:red'>",
    "post_tags": "</b>"
  }
}

检索DSL测试-聚合部分

按品牌id聚合就会列出所含有的brandId
在这里插入图片描述
嵌套聚合,用上一层聚合出来的brandId 聚合出 brandName
在这里插入图片描述
发现报错,错误为,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返回的数据,封装成需要的格式)
    在这里插入图片描述
    分步编写
    一、通过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("<b style='color:red'>");
        highlightBuilder.postTags("</b>");
        sourceBuilder.highlighter(highlightBuilder);
    }


    /**
     * 聚合分析
     */

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

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

在这里插入图片描述

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

在这里插入图片描述
就可以根据断点分析所需要封装的数据,和数据的类型
在这里插入图片描述

验证结果封装正确性

debug

页面基本数据渲染

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

页面筛选条件渲染

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

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 循环

在这里插入图片描述
在这里插入图片描述
页面部分,上一页就是当前页码减一,并自定义一个属性值pn。循环出封装的页数,
在这里插入图片描述
页码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中加入面包屑的集合属性

在这里插入图片描述
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 可获取到参数部分的字符串,在返回类型中添加参数部分的属性
在这里插入图片描述
在这里插入图片描述
直接去掉当前attrs参数,注意链接的编码转化问题
在这里插入图片描述

条件筛选联动

end

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值