谷粒商城分布式基础篇
谷粒商城分布式高级篇(上)
谷粒商城分布式高级篇(中)
谷粒商城分布式高级篇(下)
全文检索-ElasticSearch
简介
类比到MySQL里
ElasticSearch | MySQL |
---|---|
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"
}
}
]
}
}
}
条件关键词
- must 必须满足
- must_not 必须不满足
- 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容器
- 停止容器 docker stop 容器id
- 移除容器 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渲染首页
- 导入thymeleaf依赖
- 关闭缓存
整合dev-tools渲染一级分类数据
- 引入依赖并设置为true
渲染二级三级分类数据
nginx
搭建域名访问环境一(反向代理配置)
- 修改host文件 增加
192.168.56.10 guliamll.com
- 查看nginx配置文件的组成,发现 总配置文件中集成多个 sever块,每个server块相当于一个站点
每一个站点都可以在 conf.d 文件夹中配置一个文件 - 复制conf.d 文件夹 中默认的配置文件为新的配置
- 查看本机ip,更改nginx站点配置文件
搭建域名访问环境二(负载均衡到网关)
在总配置文件中的http块中配置 upstream (上游服务器) 命名为 gulimall
设置为路由到88端口,路由到网关模块,再由网关分配
站点配置文件中 配置上
重启nginx
配置网关断言规则,网关接受nginx负载均衡来的请求 -Host
断言 判断请求头中 的Host 的域名并 路由到指定 服务
配置完成后发现无法访问
因为nginx代理给网关的时候,会丢失请求的host信息,导致网关无法判断,其实nginx会丢掉很多信息
必须在站点负载均衡时手动设置上请求头需要携带的信息
性能压测
压力测试
基本介绍
Apache JMeter安装使用
-
安装JMeter
-
添加线程租,设置线程数
-
添加取样器和监听器,在取样器中设置测试地址
JMeter在windows下地址占用bug解决
性能监控
堆内存与垃圾回收
jvisualvm使用
- cmd 输入
jvisualvm
启动 - 输入正确插件地址后还是链接不上
- 手动安装,点击下载后
- 添加已下载的插件
优化
中间件对性能的影响
压测网关加简单请求
压测内容 | 压测线程数 | 吞吐量/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)
- 开thymeleaf缓存
- 关日志
- 为这个字段加上索引
nginx动静分离
- 将静态资源上传至nginx html 静态资源目录下,删除本地的 静态资源
- 替换资源路径
index/img/xxx.xxx
---->staitc/index/img/xxx.xxx
- 增加nginx站点配置
配置意为 路径为/static/
的地址 资源在 root /usr/share/nginx/html;
下寻找
模拟线上应用内存崩溃宕机情况
发现设置动静分离后提升不大,而老年代几乎爆满,慢在老年代的GC
设置服务的最大内存占用重新分配内存
配置意思分别为 最大内存 最小内存 新生代内存(伊甸园区+幸存者区)
优化三级分类数据获取
优化业务,只做一次查询
缓存
缓存使用
本地缓存与分布式缓存
分布式部署时-本地缓存模式的问题
- 请求可能会到各个服务器,造成缓存不一致
- 更改缓存可能只更改到一台服务器,造成缓存不一致
解决方法 - 引入缓存中间件redis
整合redis测试
-
引入redis springboot 的启动器
-
配置文件配置redis地址
-
使用
springboot
自动配置好的StringRedisTemplate
来操作redis
-
测试
@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 |
缓存击穿、穿透、雪崩
加锁解决缓存击穿问题
测试本地锁
@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 命令为
代码方法为
这种也会产生一个问题,删锁问题
- 业务超时,锁已经过期了,这时候别的线程进来了,再删锁就删掉了别的线程的锁
- 解决办法,给自己的锁设置唯一id,且删锁时获取锁的id和删除锁作为原子操作
- 锁的自动续期,避免业务还没执行完,而锁却过期了,给锁的过期时间设置长点如300秒,因为不可能有业务执行时间超过300秒,并给整体业务加入 try final ,无论业务结果如何,最终都会释放锁
主要注意点
- 获取锁和设置锁的过期时间为原子操作
- 找到锁和删除锁为原子操作
Redisson简介&整合
- 引入依赖
- 写入配置整合 官方文档
Redisson-lock锁测试
可重入锁:嵌套调用可以重复使用的锁,所有的锁都应该设置为可重入锁,避免死锁问题
简单解释: 方法A中调用方法B ,方法A有lock1
方法B也能使用 lock1
B执行完后 A释放锁程序结束,如果不可重入锁,B无法拿到方法持有的lock1
锁,这就形成了死锁,所以所有的锁都应该设计为可重入锁
测试
- 开启两个线程,同时发请求,一个线程拿到锁后,还没释放锁就关闭线程
- 另一个线程还是能拿到锁并访问成功
- 因为
redisson
的锁是一个阻塞式等待lock.lock();
方法 如果拿不到锁就会一直停留在这儿等待锁的释放,默认加锁时间是30s - redisson看门狗,能对锁进行自动续期,如果业务超长,运行期间会自动给锁续上新的30s,不用担心业务时间长,锁自动过期被删掉
- 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在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;
}
信号量测试
信号量可以用来限量
acquire
和 tryAcquire
的区别
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 锁
如何保持缓存一致性
- 设置缓存过期时间能解决大部分业务的缓存需求,一段时间过后,缓存必然会更新,做到最终一致性
- 读写锁,在写的时候不能读,写完后再都就是一致性了
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值
自定义缓存配置
- 自定义配置类中配置存入redis key和value的数据为json
- 生效配置文件
@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)
原理与不足
商城业务
检索服务
搭建页面环境
-
search 服务加入thymeleaf
-
template目录放入html页面
-
静态资源导入nginx 静态资源目录下
-
配置本地转发到虚拟机
-
修改nginx站点配置文件 统一转至88网关模块端口,发现
*.
不起作用
换成一一列出
-
网关模块转发至 search 服务
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
- 引入
devtools
和关闭thymeleaf
缓存
调整页面跳转
-
侧边栏的点击跳转
-
搜索框的点击跳转
检索查询参数模型分析抽取
所有的检索条件抽取为一个对象传输
抽象一个处理检索的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依次包含有
- 模糊匹配
- 过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
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构建-检索
分为三大部分
- 准备检索请求。动态构建出查询需要的DSL语句
(将上一节分析列出的DSL用 api 构造出)
- 执行检索请求
(传入用api构造的DSL )
- 分析响应数据封装成我们需要的格式
(操作上一步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渲染到页面
页面筛选条件渲染
编写一个统一函数,点击属性值统一调用跳转
报错无法处理特殊字符
改为双引号的转义字符 "
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代码部分就不记录了
页面排序字段回显
页面价格区间搜索
面包屑导航
- 返回模型
SearchResult
中加入面包屑的集合属性
2. buildSearchResult
方法内继续封装面包屑,重新包装远程接口返回的AttrRespVo
为AttrResponseVo
//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