文章目录

  • SpringCloud 商城项目 - 高级篇(上)
  • 1. ElaticSearch全文检索
    • 1.1 docker安装elasticsearch和kibana
    • 1.2 elasticsearch入门
    • 1.3 elasticsearch进阶
    • 1.4 Elasticsearch-Rest-Client
  • 2. 商城业务 - 商品上架
    • 2.1 构造基本数据
    • 2.2 查询当前sku可以被用来检索的规格属性
    • 2.3 远程调用gulimall-ware服务查询是否有库存
    • 2.4 远程调用gulimall-search将数据放在es保存实现上架
    • 2.5 上架接口调试
  • 3. 商城业务 - 首页
    • 3.1 整合Thymleaf渲染首页
    • 3.2 渲染一级分类
    • 3.3 渲染二级和三级分类
    • 3.4 nginx搭建域名访问环境
    • 3.5 nginx负载均衡到网关
  • 4. 性能压测 - 压力测试
    • 4.1 Apahce Jmeter的使用
    • 4.2 jconsole 和 jvisualvm
    • 4.3 中间件对性能的影响
    • 4.5 优化-nginx动静分离
    • 4.6 模拟线上应用内存崩溃宕机
  • 5. 缓存 - 缓存使用
    • 5.1 本地缓存与分布式缓存
    • 5.2 改造三级分类
    • 5.3 本地锁
    • 5.4 redis分布式锁
    • 5.5 Redisson分布式锁
    • 5.6 缓存一致性
    • 5.7 Spring Cache
  • 6. 商城业务 - 检索服务
    • 6.1 搭建页面环境
    • 6.2 通过检索条件进行检索
    • 6.3 页面数据渲染
  • 7. 商城业务 - 商品详情
    • 7.1 线程池
    • 7.2 CompletableFuture 异步编排
    • 7.3 商品详情页
    • 7.4 异步编排优化商品详情

SpringCloud 商城项目 - 高级篇(上)

1. ElaticSearch全文检索

1.1 docker安装elasticsearch和kibana

① 拉取镜像:

docker pull elasticsearch:7.4.2 存储和检索数据
docker pull kibana:7.4.2 可视化检索数据mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
chmod -R 777 /mydata/elasticsearch/ 保证权限,必须设置,否则启动不了容器

② 运行elasticsearch容器:

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

运行容器时报错:

③ 开启防火墙:

# 开启
service firewalld start
# 重启
service firewalld restart
# 关闭
service firewalld stop

然后重新运行elasticsearch容器,报错:

④ 删除elasticsearch容器:

docker ps -a
docker rm ec3c5a661a9b

⑤ 重新安装elasticsearch容器后,安装成功

⑥ 关闭防火墙后在页面访问:http://192.168.38.22:9200/

⑦ 安装kibana容器:

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

再次报错:

开启防火墙后再安装,成功

⑧ 关闭防火墙后,访问:http://192.168.38.22:5601

⑨ 设置elasticsearch和kibana关闭后重启:

docker update 48c5626bd8ec --restart=always
docker update 0b7debde6153 --restart=always

1.2 elasticsearch入门

_cat

GET /_cat/nodes: 查看所有节点
GET /_cat/health: 查看 es 健康状况
GET /_cat/master: 查看主节点
GET /_cat/indices: 查看所有索引 show databases;

索引一个文档(保存)

保存一个数据, 保存在哪个索引的哪个类型下, 指定用哪个唯一标识

PUT customer/external/1; 在 customer 索引下的 external 类型下保存 1 号数据
//请求体
{"name": "John Doe"}

POST可以新增可以修改。 如果不指定 id, 会自动生成 id。 指定 id 就会修改这个数据, 并新增版本号;

PUT 可以新增可以修改。 PUT 必须指定 id; 由于 PUT 需要指定 id, 我们一般都用来做修改 操作 ;

查询文档

GET customer/external/1

如果有A和B都想修改name字段的数据,想要实现并发控制,需要带上?if_seq_no=2&if_primary_term=1字段,当A修改完以后,_seq_no的值都会改变,那么B再想修改数据时就会报错。

更新文档

带有_update和不带有_update的语法区别:

POST customer/external/1/_update
// 请求体需要加上doc
{"doc":{"name": "John Doew"}}POST customer/external/1
// 请求体带不带doc都可以
{"name": "John Doe2"}//或者
PUT customer/external/1
{"name": "John Doe"}

结论:

Post请求方式可以带有_update,而Put请求不可以

Post请求带有_update时,会对比源文档数据, 如果相同不会有什么操作, 文档 version 不增加 ;

PUT 操作总会将数据重新保存并增加 version 版本;

Put和Post都可以在更新时增加属性;

POST customer/external/1
{"doc": { "name": "Jane Doe", "age": 20 }}

删除文档和索引

DELETE customer/external/1  删除文档
ELETE customer              删除索引

_bulk批量API

POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }语法格式:
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body } 复杂实例:
POST /_bulk
{"create": { "_index": "website", "_type": "blog", "_id": "123"}}
{"title": "My first blog post" }
{"index": { "_index": "website", "_type": "blog" }}
{"title": "My second blog post" }
{"update": { "_index": "website", "_type": "blog", "_id": "123"}}
{"doc" : {"title" : "My updated blog post"} }
{"delete": { "_index": "website", "_type": "blog", "_id": "123"}}

⑦ 样本数据:https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json

1.3 elasticsearch进阶

ES 支持两种基本方式检索 :
一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
另一个是通过使用 REST request body 来发送它们(uri+请求体)

uri+检索参数:

GET bank/_search    检索 bank 下所有信息, 包括 type 和 docs
GET bank/_search?q=*&sort=account_number:asc  请求参数方式检索GET bank/_search 响应结果解释:
took - Elasticsearch 执行搜索的时间( 毫秒)
time_out - 告诉我们搜索是否超时
_ shards - 告诉我们多少个分片被搜索了, 以及统计了成功/失败的搜索分片
hits - 搜索结果
hits.total - 搜索结果
hits.hits - 实际的搜索结果数组( 默认为前 10 的文档)
sort - 结果的排序 key( 键) ( 没有则按 score 排序)
score 和 max_score –相关性得分和最高得分( 全文检索用)

uri+请求体:

GET bank/_search
{"query": {"match_all": {}},"sort": [{"account_number": {"order": "desc"}}]
}

Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL( domain-specific language 领域特
定语言) 。 这个被称为 Query DSL。

基本语法格式

查询的基本语法:

{QUERY_NAME: {ARGUMENT: VALUE,ARGUMENT: VALUE,...}
}

如果是针对某个字段, 那么它的结构如下:

{QUERY_NAME: {FIELD_NAME: {ARGUMENT: VALUE,ARGUMENT: VALUE,...}}
}

示例:

GET bank/_search
{"query": {"match_all": {}},"from": 0,"size": 5,"sort": [{"account_number": {"order": "desc"}}]
}//  query 定义如何查询,
//  match_all 查询类型【代表查询所有的所有】 , es 中可以在 query 中组合非常多的查询类型完成复杂查询
//  除了query参数之外, 我们也可以传递其它的参数以改变查询结果。 如 sort,size
//  from+size 限定, 完成分页功能
//  sort 排序, 多字段排序, 会在前序字段相等时后续字段内部排序, 否则以前序为准

返回部分字段

GET bank/_search
{"query": {"match_all": {}},"from": 0,"size": 5,"_source": ["age","balance"]
}

match【 匹配查询】

基本类型( 非字符串) , 精确匹配 :

GET bank/_search
{"query": {"match": {"account_number": "20"}}
}
// match 返回 account_number=20 的

字符串, 全文检索 :

GET bank/_search
{"query": {"match": {"address": "mill"}}
}
// 最终查询出 address 中包含 mill 单词的所有记录
// match 当搜索字符串类型的时候, 会进行全文检索, 并且每条记录有相关性得分。

字符串, 多个单词( 分词+全文检索) :

GET bank/_search
{"query": {"match": {"address": "mill road"}}
}
//最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录, 并给出相关性得分

multi_match【 多字段匹配】

GET bank/_search
{"query": {"multi_match": {"query": "mill","fields": ["state","address"]}}
}
// state 或者 address 包含 mill

bool【 复合查询】

bool 用来做复合查询:复合语句可以合并 任何 其它查询语句, 包括复合语句

must: 必须达到 must 列举的所有条件

GET bank/_search
{"query": {"bool": {"must": [{ "match": { "address": "mill" } },{ "match": { "gender": "M" } }]}}
}

must_not: 必须不是指定的情况

should: 应该达到 should 列举的条件, 如果达到会增加相关文档的评分, 并不会改变查询的结果。 如果 query 中只有 should 且只有一种匹配规则, 那么 should 的条件就会被作为默认匹配条件而去改变查询结果

GET bank/_search
{"query": {"bool": {"must": [{ "match": { "address": "mill" } },{ "match": { "gender": "M" } }],"should": [{"match": { "address": "lane" }}],"must_not": [{"match": { "email": "baluba.com" }}]}}
}// address 包含 mill, 并且 gender 是 M, 如果 address 里面有 lane 最好不过, 但是 email 必须不包含 baluba.com

filter【结果过滤】

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-search.html

must,should,must_not都会计算相关性得分 ,分数越高,文档就越符合您的搜索条件。默认情况下,Elasticsearch返回按这些相关性分数排名的文档。但并不是所有的查询都需要产生分数, 特别是那些仅用于 “filtering”(过滤) 的文档。 为了不计算分数 Elasticsearch 会自动检查场景并且优化查询的执行 。

GET bank/_search
{"query": {"bool": {"must": [{"match": { "address": "mill"}}],"filter": {"range": {"balance": {"gte": 10000,"lte": 20000}}}}}
}

GET bank/_search
{"query": {"bool": {"filter": {"range": {"balance": {"gte": 10000,"lte": 20000}}}}}
}

term查询

和 match 一样。 匹配某个属性的值。 全文检索字段用 match, 其他非 text 字段匹配用 term。

GET bank/_search
{"query": {"bool": {"must": [{"term": {"age": {"value": "28"}}},{"match": {"address": "990 Mill Road"}}]}}
}//如果是精确值(age,balance等数字)字段,建议使用term,如果是全文检索字段,建议使用match

aggregations( 执行聚合)

聚合提供了从数据中分组和提取数据的能力。 最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。 在 Elasticsearch 中, 您有执行搜索返回 hits( 命中结果) , 并且同时返回聚合结果, 把一个响应中的所有 hits( 命中结果) 分隔开的能力。 这是非常强大且有效的,您可以执行查询和多个聚合, 并且在一次使用中得到各自的( 任何一个的) 返回结果, 使用一次简洁和简化的 API 来避免网络往返

1、搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄, 但不显示这些人的详情 :

GET bank/_search
{"query": {"match": {"address": "mill"}}, "aggs": {"group_by_state": {"terms": {"field": "age"}},"avg_age": {"avg": {"field": "age"}}},"size": 0
}
// size: 0 不显示搜索数据
// aggs: 执行聚合。 聚合语法如下
// "aggs": {//          "aggs_name 这次聚合的名字(随便起名), 方便展示在结果集中": {//              "AGG_TYPE 聚合的类型( avg,term,terms) ": {}
//      }
//  }

2、按照年龄聚合, 并且请求这些年龄段的这些人的平均薪资 :

GET bank/account/_search
{"query": {"match_all": {}},"aggs": {"age_avg": {"terms": {"field": "age","size": 1000},"aggs": {"banlances_avg": {"avg": {"field": "balance"}}}}} , "size": 1000
}

3、查出所有年龄分布, 并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资 :

GET bank/account/_search
{"query": {"match_all": {}},"aggs": {"age_agg": {"terms": {"field": "age","size": 100},"aggs": {"gender_agg": {"terms": {"field": "gender.keyword","size": 100},"aggs": {"balance_avg": {"avg": {"field": "balance"}}}},"balance_avg":{"avg": {"field": "balance"}}}}} , "size": 1000
}

Mapping映射

1、 创建索引并指定映射

PUT /my-index
{"mappings": {"properties": {"age": { "type": "integer" },"email": { "type": "keyword" },"name": { "type": "text" }}}
}

2、 添加新的字段映射

PUT /my-index/_mapping
{"properties": {"employee-id": {"type": "keyword","index": false}}
}

3、 更新映射

对于已经存在的映射字段, 我们不能更新。 更新必须创建新的索引进行数据迁移 ,先创建出 new_twitter 的正确映射。 然后使用如下方式进行数据迁移 :

POST _reindex [固定写法]
{"source": {"index": "twitter"},"dest": {"index": "new_twitter"}
}//将旧索引的 type 下的数据进行迁移
POST _reindex
{"source": {"index": "twitter","type": "tweet"},"dest": {"index": "new_twitter"}
}

示例:修改bank中的映射,

PUT /newbank1
{"mappings" : {"properties" : {"account_number" : { "type" : "long"},"address" : {"type" : "text"},"age" : {"type" : "integer"},"balance" : { "type" : "integer"},"city" : {"type" : "keyword"},"email" : {"type" : "keyword"},"employer" : {"type" : "keyword"},"firstname" : {"type" : "keyword"},"gender" : {"type" : "keyword"},"lastname" : { "type" : "keyword"},"state" : {"type" : "keyword"}}}
}POST _reindex{"source": {"index": "bank","type": "account"},"dest": {"index": "newbank1"}}

ik分词器

下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v7.4.2

下载后解压到ik文件夹下,并通过FileZilla等工具将ik文件夹上传到/mydata/elasticsearch/plugins目录下

然后开启防火墙,重启es服务器:docker restart 2b3c94dbe0d6

如果es服务器启动报错,可以使用docker logs 2b3c94dbe0d6,查看报错原因

关闭防火墙,刷新kibana:http://192.168.38.22:5601/

安装ngnix

1、增大es启动时jvm堆内存:

docker stop 2b3c94dbe0d6
docker rm 2b3c94dbe0d6

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

2、安装ngnix:

进入/mydata文件夹,并新建一个nginx文件夹,随便启动一个 nginx 实例, 只是为了复制出配置

docker run -p 80:80 --name nginx -d nginx:1.10

将容器内的配置文件拷贝到当前目录,别忘了后面的点:

docker container cp nginx:/etc/nginx .

修改文件名称: mv nginx conf ,把这个 conf 移动到/mydata/nginx 下

终止原容器: docker stop nginx

删除原容器: docker rm $ContainerId

执行以下命令运行ngnix容器:

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

3、自定义分词词库 :

[root@localhost nginx]# cd html/
[root@localhost html]# ls
[root@localhost html]# vi index.html
[root@localhost html]# mkdir es
[root@localhost html]# ls
es  index.html
[root@localhost html]# cd es
[root@localhost es]# vi fenci.txt

修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties><comment>IK Analyzer 扩展配置</comment><!--用户可以在这里配置自己的扩展字典 --><entry key="ext_dict"></entry><!--用户可以在这里配置自己的扩展停止词字典--><entry key="ext_stopwords"></entry><!--用户可以在这里配置远程扩展字典 --><entry key="remote_ext_dict">http://192.168.38.22/es/fenci.txt</entry><!--用户可以在这里配置远程扩展停止词字典--><!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

1.4 Elasticsearch-Rest-Client

官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

① 导入依赖:

<dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></exclusion></exclusions>
</dependency><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.4.2</version>
</dependency>

② 主配置类:

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSearchApplication {public static void main(String[] args) {SpringApplication.run(GulimallSearchApplication.class, args);}
}

② 配置类:

@Configuration
public class GulimallElasticSearchConfig {@Beanpublic RestHighLevelClient esRestClient(){RestClientBuilder builder = RestClient.builder(new HttpHost("192.168.38.22",9200,"http"));RestHighLevelClient client = new RestHighLevelClient(builder);return client;}
}

③ 参照官方文档测试:

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-document-index.html

1、测试保存一个文档:

@Test
public void indexData() throws IOException {//索引名称IndexRequest indexRequest = new IndexRequest("user");//数据idindexRequest.id("1");User user = new User();user.setUserName("zhangsan");user.setAge(15);user.setGender("女");String jsonString = JSON.toJSONString(user);//要保存的数据indexRequest.source(jsonString, XContentType.JSON);//执行操作IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
}@Data
class User{private String userName;private String gender;private Integer age;
}

2、测试检索请求:搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄, 但不显示这些人的详情

@Test
public void searchData() throws IOException {//1、创建检索请求SearchRequest searchRequest = new SearchRequest();//指定索引searchRequest.indices("bank");//指定DSL,检索条件//SearchSourceBuilder sourceBuilde 封装的条件SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//1.1)、构造检索条件//        sourceBuilder.query();//        sourceBuilder.from();//        sourceBuilder.size();//        sourceBuilder.aggregation()sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));//1.2)、按照年龄的值分布进行聚合TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);sourceBuilder.aggregation(ageAgg);//1.3)、计算平均薪资AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");sourceBuilder.aggregation(balanceAvg);System.out.println("检索条件"+sourceBuilder.toString());searchRequest.source(sourceBuilder);//2、执行检索;SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);System.out.println(searchResponse.toString());//3.1)、获取所有查到的数据SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();for (SearchHit hit : searchHits) {String string = hit.getSourceAsString();Accout accout = JSON.parseObject(string, Accout.class);System.out.println("accout:"+accout);}//3.2)、获取这次检索到的分析信息;Aggregations aggregations = searchResponse.getAggregations();Terms ageAgg1 = aggregations.get("ageAgg");for (Terms.Bucket bucket : ageAgg1.getBuckets()) {String keyAsString = bucket.getKeyAsString();System.out.println("年龄:"+keyAsString+"==>"+bucket.getDocCount());}Avg balanceAvg1 = aggregations.get("balanceAvg");System.out.println("平均薪资:"+balanceAvg1.getValue());
}@Data
@ToString
static class Accout{private int account_number;private int balance;private String firstname;private String lastname;private int age;private String gender;private String address;private String employer;private String email;private String city;private String state;
}

2. 商城业务 - 商品上架

上架的商品才可以在网站展示。上架的商品需要可以被检索。

① 上架是将后台的商品放在 es 中可以提供检索和查询功能:

1) hasStock: 代表是否有库存。 默认上架的商品都有库存。 如果库存无货的时候才需要更新一下 es
2) 库存补上以后, 也需要重新更新一下 es
3) hotScore 是热度值, 我们只模拟使用点击率更新热度。 点击率增加到一定程度才更新热度值。
4) 下架就是从 es 中移除检索项, 以及修改 mysql 状态

② 商品上架步骤:

1) 先在 es 中按照之前的 mapping 信息, 建立 product 索引。
2) 点击上架, 查询出所有 sku 的信息, 保存到 es 中
3) es 保存成功返回, 更新数据库的上架状态信息。

③ 数据一致性:

1) 商品无库存的时候需要更新 es 的库存信息
2) 商品有库存也要更新 es 的信息

2.1 构造基本数据

① 先在 es 中建立 product 索引:

index:默认 true,如果为 false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能 当做检索条件

doc_values: 默认 true, 设置为 false, 表示不可以做排序、 聚合以及脚本操作, 这样更节省磁盘空间。 还可以通过设定 doc_values 为 true, index 为 false 来让字段不能被搜索但可以用于排序、 聚 合以及脚本操作

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

② 商品上架接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/DhOtFr4A

@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){spuInfoService.up(spuId);return R.ok();
}

③ es中映射mapping对应的数据传输对象TO:

@Data
public class SkuEsModel {private Long skuId;private Long spuId;private String skuTitle;private BigDecimal skuPrice;private String skuImg;private Long saleCount;private Boolean hasStock;private Long hotScore;private Long brandId;private Long catalogId;private String brandName;private String brandImg;private String catalogName;private List<Attrs> attrs;@Datapublic static class Attrs{private Long attrId;private String attrName;private String attrValue;}
}
@Override
public void up(Long spuId) {List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);// TODO 查询当前sku所有可以被用来检索的规格属性List<SkuEsModel> upProducts =  skuInfoEntities.stream().map(skuInfoEntity -> {SkuEsModel skuEsModel = new SkuEsModel();BeanUtils.copyProperties(skuInfoEntity,skuEsModel);//skuPrice 、skuImgskuEsModel.setSkuPrice(skuInfoEntity.getPrice());skuEsModel.setSkuImg(skuInfoEntity.getSkuDefaultImg());//hasStock 、 hasScore//TODO 发送远程调用gulimall-ware服务查询是否有库存//TODO 热度评分,默认设为0BrandEntity brandEntity = brandService.getById(skuInfoEntity.getBrandId());skuEsModel.setBrandName(brandEntity.getName());skuEsModel.setBrandImg(brandEntity.getLogo());CategoryEntity categoryEntity = categoryService.getById(skuInfoEntity.getCatalogId());skuEsModel.setCatalogName(categoryEntity.getName());return skuEsModel;}).collect(Collectors.toList());//TODO 将数据发送给es进行保存 、gulimall-search
}

2.2 查询当前sku可以被用来检索的规格属性

可以通过spu_id来获取所有属性信息的attr_id:

通过attr_id找到search_type=1的属性 :

@Override
public void up(Long spuId) {List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);// 查询当前sku所有可以被用来检索的规格属性List<ProductAttrValueEntity> productAttrValueEntities = attrValueService.baseAttrlistforspu(spuId);List<Long> attrIds = productAttrValueEntities.stream().map(productAttrValueEntity -> {return productAttrValueEntity.getAttrId();}).collect(Collectors.toList());//找到attrIds中search_type=1的属性,并返回相应的attrIdsList<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);//封装attrsSet<Long> idSet = new HashSet<>(searchAttrIds);List<SkuEsModel.Attrs> attrsList = productAttrValueEntities.stream().filter(item -> {return idSet.contains(item.getAttrId());}).map(item -> {SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();BeanUtils.copyProperties(item, attrs1);return attrs1;}).collect(Collectors.toList());List<SkuEsModel> upProducts = skuInfoEntities.stream().map(skuInfoEntity -> {SkuEsModel skuEsModel = new SkuEsModel();//.....省略.....//attrsskuEsModel.setAttrs(attrsList);return skuEsModel;}).collect(Collectors.toList());//TODO 将数据发送给es进行保存 、gulimall-search
}

2.3 远程调用gulimall-ware服务查询是否有库存

① 需要在gulimall-ware服务中写一个查询是否有库存的接口方法:

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {@Autowiredprivate WareSkuService wareSkuService;//查询是否有库存@PostMapping("/hasstock")public R<List<SkuHasStockVo>> getSkusHasStock(@RequestBody List<Long> skuIds){List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);R<List<SkuHasStockVo>> ok = R.ok();ok.setData(vos);return ok;}
}

我们需要查询出skuId=1的所有库存总量,然后减去锁定的库存,如果库存总量大于0,就说明有库存:

@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map((skuId) -> {SkuHasStockVo skuHasStockVo = new SkuHasStockVo();//查询当前sku的库存总量:skuId=1的可能在几个库存中都有,因此需要求出总库存//总库存还需要减去已经被锁定的库存(已经下单还没有支付的库存)Long count = baseMapper.getStuckStock(skuId);skuHasStockVo.setSkuId(skuId);skuHasStockVo.setHasStock(count ==null?false:count>0);return skuHasStockVo;}).collect(Collectors.toList());return skuHasStockVos;
}
<select id="getStuckStock" resultType="java.lang.Long">select sum(stock-stock_locked) from wms_ware_sku where sku_id=#{skuId}
</select>

② 在gulimall-product服务中写一个openFeign接口,调用远程接口:

@FeignClient("gulimall-ware")
public interface WareFeignService {@PostMapping("/ware/waresku/hasstock")public R<List<SkuHasStockVo>> getSkusHasStock(@RequestBody List<Long> skuIds);
}

③ 在gulimall-product中调用feign接口:

@Override
public void up(Long spuId) {//TODO 发送远程调用gulimall-ware服务查询是否有库存List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);List<Long> skuIds = skuInfoEntities.stream().map((skuInfoEntity -> {Long skuId = skuInfoEntity.getSkuId();return skuId;})).collect(Collectors.toList());Map<Long, Boolean> stockMap = null;try {R<List<SkuHasStockVo>> skusHasStock = wareFeignService.getSkusHasStock(skuIds);stockMap = skusHasStock.getData().stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item->item.getHasStock()));} catch (Exception e) {log.error("库存服务异常,原因{}",e);}Map<Long, Boolean> finalStockMap = stockMap;List<SkuEsModel> upProducts = skuInfoEntities.stream().map(skuInfoEntity -> {SkuEsModel skuEsModel = new SkuEsModel();//.....省略......// hasStockif(finalStockMap ==null){skuEsModel.setHasStock(true);}else{skuEsModel.setHasStock(finalStockMap.get(skuInfoEntity.getSkuId()));}return skuEsModel;}).collect(Collectors.toList());//TODO 将数据发送给es进行保存 、gulimall-search
}

2.4 远程调用gulimall-search将数据放在es保存实现上架

① 在gulimall-search服务中编写一个商品上架的接口方法:

@Slf4j
@RequestMapping("/search")
@RestController
public class ElasticSearchController {@Autowiredprivate ProductSaveService productSaveService;@PostMapping("/product")public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {boolean b= false;try {b = productSaveService.productStatusUp(skuEsModels);} catch (IOException e) {log.error("ElasticSearchController商品上架错误,{}",e);return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());}if(!b){return R.ok();}else{return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());}}
}
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {@Autowiredprivate RestHighLevelClient restHighLevelClient;@Overridepublic boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {//保存到es//1、给es中建立索引,product,在kibana中建立号映射关系//2、在es中保存这些数据BulkRequest bulkRequest = new BulkRequest();for(SkuEsModel skuEsModel:skuEsModels){//指定索引: PRODUCT_INDEX = "product";IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);//指定索引对应数据的idindexRequest.id(skuEsModel.getSkuId().toString());//要保存的数据indexRequest.source(JSON.toJSONString(skuEsModel), XContentType.JSON);bulkRequest.add(indexRequest);}BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);//批量保存出现错误boolean b = bulk.hasFailures();List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {return item.getId();}).collect(Collectors.toList());log.error("商品上架成功:{}",collect);return b;}
}

② 在guilimall-product服务中编写一个openFeign接口,用来远程调用guliamll-search服务:

@FeignClient("gulimall-search")
public interface SearchFeignService {@PostMapping("/search/product")public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}

③ 在gulimall-product中,调用openfeign接口,将数据传送给gulimall-search服务闭关保存在es中:

@Override
public void up(Long spuId) {List<SkuEsModel> upProducts = skuInfoEntities.stream().map(skuInfoEntity -> {SkuEsModel skuEsModel = new SkuEsModel();// ......省略........return skuEsModel;}).collect(Collectors.toList());//TODO 将数据发送给es进行保存 、gulimall-searchR r = searchFeignService.productStatusUp(upProducts);if(r.getCode()==0){//远程调用成功,修改spu的上架状态baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());}else{// 远程调用失败// 接口幂等性}
}
<update id="updateSpuStatus">update pms_spu_info set publish_status=#{code},update_time=NOW() where id=#{spuId};
</update>

2.5 上架接口调试

debug方法:

① 前端发送请求http://localhost:88/api/product/spuinfo/11/up到后端接口,开始进行debug,首先到达Controller层的断点处,如果需要进入Controller层中调用的Service层方法,需要在Service层对应方法上加上断点,然后F8 (即第一个按钮即可进入),就是步行,,一步步的向下执行,只要加了断点就会进入

② 这样我们便进入了Service层的接口方法,步行(第一个按钮),往下走看执行逻辑:

③ 现在F8步行到执行远程服务的方法,想要进入getSkusHasStock()方法内部,就在getSkusHasStock()方法内部上加上断点,F8即可进入:

④ 我们现在进入了gulimall-ware服务中的getSkusHasStock()方法中,如果想回到gulimall-product之前的断点处(即回到方法的调用处),只需要点击gulimall-product服务即可:

⑤ 如果我想从一个断点直接跳到下一个断点处执行,可以放行到下一个断点处:

比如,从断点1处直接跳到断点2处去执行:

经过debug我们发现库存接口有问题,因为无论我们是否有库存,居然都上架成功了,因此需要调整,之前我们给R类上加上了泛型,现在不再加泛型,因为R类继承的是 HashMap<String, Object> 接口:

public class R  extends HashMap<String, Object> {//利用fastjson进行逆转public <T> T getData(String key,TypeReference<T> typeReference){Object data = get(key);//默认是mapString s = JSON.toJSONString(data);T t = JSON.parseObject(s, typeReference);return t;}//利用fastjson进行逆转public <T> T getData(TypeReference<T> typeReference){Object data = get("data");//默认是mapString s = JSON.toJSONString(data);T t = JSON.parseObject(s, typeReference);return t;}public R setData(Object data){put("data",data);return this;}
}

相应需要修改的地方,在WareSkuController ()方法中:

@PostMapping("/hasstock")
public R getSkusHasStock(@RequestBody List<Long> skuIds){List<SkuHasStockVo> vos =  wareSkuService.getSkusHasStock(skuIds);return R.ok().setData(vos);
}

在SpuInfoServiceImpl () 方法中:

try {R r = wareFeignService.getSkusHasStock(skuIds);TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item->item.getHasStock()));
} catch (Exception e) {log.error("库存服务异常,原因{}",e);
}

3. 商城业务 - 首页

3.1 整合Thymleaf渲染首页

① 在gulimall-product服务中导入thymleaf坐标依赖:

<!-- 模板引擎: thymeleaf -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

② 需要关闭thymeleaf的缓存:

spring:thymeleaf:cache: false

③ 重启项目,发现页面加载不到,图片加载不出来,先发现项目运行产生的target中不存在static文件夹,因此clean项目然后重启项目即可,访问http://localhost:10000/

3.2 渲染一级分类

① 需求:渲染一级分类,将一级分类的数据查询出来,经过model传给页面并渲染

package com.atguigu.gulimall.product.web;@Controller
public class IndexController {@Autowiredprivate CategoryService categoryService;@GetMapping({"/","/index.html"})public String indexPage(Model model){List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();model.addAttribute("categorys",categoryEntities);//默认加上前缀和后缀:classpath:templates/index.htmlreturn "index";}
}

一级分类的parent_cid=0 或者cat_level=1

@Override
public List<CategoryEntity> getLevel1Categorys() {return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
}

② 修改页面后不再重启项目,可以整合dev-tools,然后按ctrl+shift+F9,刷新页面:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional>
</dependency>

③ 在前端页面渲染页面:

<!--每遍历一次就会产生一个li标签-->
<li th:each="category:${categorys}"><a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器</b></a>
</li>

可以看到前端页面中一级分类展示出来了:

3.3 渲染二级和三级分类

① 需要响应的json数据内容:

对应json数据封装需要响应的vo对象:

//二级分类vo
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {private String catalog1Id; //1级父分类idprivate List<Catelog3Vo> catalog3List;  //三级子分类private String id;private String name;//三级分类Vo@NoArgsConstructor@AllArgsConstructor@Datapublic static class Catelog3Vo{private String catalog2Id;//父分类,2级分类idprivate String id;private String name;}
}

② 在catalogLoader.js中发送ajax请求到后端:

$(function(){$.getJSON("index/catalog.json",function (data) {.....}
}

③ 在IndexCotroller中编写请求映射路径:

@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatelogJson(){Map<String, List<Catelog2Vo>> catelogJson = categoryService.getCatelogJson();return catelogJson;
}
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {List<CategoryEntity> level1Categorys = getLevel1Categorys();//k和v代表每个一级分类Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v -> {//查询到一级分类下的二级分类List<CategoryEntity> level2Catelogs = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));//将二级分类封装成响应数据voList<Catelog2Vo> catelog2Vos = null;if (level2Catelogs != null) {catelog2Vos = level2Catelogs.stream().map(level2Catelog -> {Catelog2Vo catelog2Vo = new Catelog2Vo();catelog2Vo.setCatalog1Id(v.getCatId().toString());catelog2Vo.setId(level2Catelog.getCatId().toString());catelog2Vo.setName(level2Catelog.getName());//将当前二级分类的三级分类封装成VoList<CategoryEntity> level3Catelogs = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", level2Catelog.getCatId()));if(level3Catelogs!=null){List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Catelogs.stream().map((level3Catelog)->{Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();catelog3Vo.setCatalog2Id(level2Catelog.getCatId().toString());catelog3Vo.setId(level3Catelog.getCatId().toString());catelog3Vo.setName(level3Catelog.getName());return catelog3Vo;}).collect(Collectors.toList());catelog2Vo.setCatalog3List(catelog3Vos);}return catelog2Vo;}).collect(Collectors.toList());}return catelog2Vos;}));return parent_cid;
}

④ 访问http://localhost:10000/ 可以看到三级分类数据,修改数据库中某个数据,验证是否从数据库中加载,或者直接访问后端接口,看响应数据是否正确,如果响应数据正确但是页面书记不正确,清除浏览器缓存即可。

3.4 nginx搭建域名访问环境

① 在C:\Windows\System32\drivers\etc\host文件中配置域名,或者直接在SwitchHosts中配置:

② 启动nginx服务器,nginx监听的是虚拟机的80端口,访问gulimall.com此时就会访问到nginx的index页面:

③ 我们希望nginx帮我们进行反向代理,将来自于gulimall.com的请求都转发到商品服务,在虚拟机上修改gulimall.conf配置文件:

server {listen       80; # 监听的端口server_name  gulimall.com;  # host中配置的域名名称location / {proxy_pass http://192.168.15.1:10000;  # 转到本机的商品服务}
}

winsows本机的Ip地址为以下三个中任意一个:

原理:浏览器访问gulimall.com,windows中的hosts文件中指明了gulimall.com映射的是虚拟机IP,因此gulimall.com就会来到虚拟机,来到虚拟机之后,虚拟机的nginx又监听了80端口,而且域名是gulimall.com的请求,nginx就会帮我们代理到windows本机上的服务地址。

使用gulimall.com访问,应该转到到商品服务的http://192.168.15.1:10000,但是404 ,开启虚拟机防火墙即可。

3.5 nginx负载均衡到网关

gulimall.com会来到虚拟机中的nginx,由nginx再代理给我们的商品服务,但是商品服务可能是一个集群环境,多台服务器,而且有上线和下线,如果我们直接使用nginx代理我们的商品服务,那么就需要nginx负载均衡到商品服务中,而且商品服务的机器上下线也是动态的,那么就需要经常修改配置,因此我们希望nginx将请求交给网关,由网关通过nacos服务注册中心,获取上线的商品服务,由网关负载均衡到商品服务。

[root@localhost conf]# pwd
/mydata/nginx/conf

① 在nginx.conf文件中添加上游服务器,上游服务器名称为gulimall:

upstream gulimall{server 192.168.15.1:88;
}

② 在gulimall.conf文件中将nginx代理给网关服务,而不是商品服务:

server {listen       80;server_name  gulimall.com;location / {# 上游服务器名称proxy_pass http://gulimall; }
}

③ 在网关中配置路由规则:

- id: gulimall_host_routeuri: lb://gulimall-productpredicates:- Host=gulimall.com

④ nginx代理的时候会把请求头中的host主机丢掉,我们需要设置一下:

server {listen       80;server_name  gulimall.com;location / {proxy_set_header Host $host;proxy_pass http://gulimall;}
}

⑤ 再次访问gulimall.com即可访问到商品服务

4. 性能压测 - 压力测试

① 响应时间(Response Time: RT):响应时间指用户从客户端发起一个请求开始, 到客户端接收到从服务器端返回的响应结束, 整个过程所耗费的时间。

HPS(Hits Per Second) : 每秒点击次数, 单位是次/秒。

TPS(Transaction per Second) : 系统每秒处理交易数, 单位是笔/秒。

QPS(Query per Second) : 系统每秒处理查询次数, 单位是次/秒。

② 性能测试主要关注如下三个指标 :

吞吐量: 每秒钟系统能够处理的请求数、 任务数。

响应时间: 服务处理一个请求或一个任务的耗时。

错误率: 一批请求中结果出错的请求所占比例

4.1 Apahce Jmeter的使用

下载地址:https://jmeter.apache.org/download_jmeter.cgi,下载对应的压缩包, 解压运行 jmeter.bat 即可

打开Jmeter之后,点击options—> choose lanuage —>Chinese将英文切换为中文

发现在win10系统上菜单栏字体很小,可以点击选项—>放大 (多次点击)

① 右键Test Plan-----》添加----》线程------》线程组:

线程组参数详解:

线程数:虚拟用户数,一个虚拟用户占用一个进程或线程,设置多少虚拟用户数在这里也就是设置多少个线程数。

Ramp-Up Period(in seconds)准备时长: 设置的虚拟用户数需要多长时间全部启动。 如果线程数为 10, 准备时长为 2, 那么需要 2 秒钟启动 10 个线程, 也就是每秒钟启动 5 个线程。

循环次数: 每个线程发送请求的次数。 如果线程数为 10, 循环次数为 100, 那么每个线程发送 100 次请求。 总请求数为 10*100=1000 。 如果勾选了“永远”, 那么所有线程会一直发送请求, 一到选择停止运行脚本。

② 添加HTTP请求:

③ 查看聚合报告:

④ 点击运行,测试:

⑤ Jmeter在windows下地址占用问题:

windows 本身提供的端口访问机制的问题。
Windows 提供给 TCP/IP 链接的端口为 1024-5000, 并且要四分钟来循环回收他们。 就导致我们在短时间内跑大量的请求时将端口占满了。

在cmd 中, 用 regedit 命令打开注册表

在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 下,右击 parameters,

添加一个新的 DWORD, 名字为 MaxUserPort,然后双击 MaxUserPort, 输入数值数据为 65534, 基数选择十进制(如果是分布式运行的话, 控制机器和负载机器都需要这样操作哦),再添加一个新的 DWORD,名字为TCPTimedWaitDelay,值为30 基数为十进制。

修改配置完毕之后记得重启机器才会生效

4.2 jconsole 和 jvisualvm

Jdk 的两个小工具 jconsole、 jvisualvm(升级版的 jconsole) ; 通过命令行启动, 可监控本地和远程应用。 远程应用需要配置。

① jconsole :打开cmd窗口,然后直接运行jconsole

② jvisualvm :打开cmd窗口,然后直接运行jconsole

监控内存泄露, 跟踪垃圾回收, 执行时内存、 cpu 分析, 线程分析

③ 安装插件:工具—》插件—》可用插件:

打开网址 https://visualvm.github.io/pluginscenters.html
cmd 查看自己的 jdk 版本, 找到对应的版本链接,复制下面查询出来的链接。 并点击设置,重新设置上即可

④ 安装 Visual GC:

4.3 中间件对性能的影响

吞吐量:每秒处理的请求数

90%响应时间:每个请求的响应时间(单位是ms)

压测内容 压测线程数 吞吐量/s 90%响应时间 99%响应时间
Nginx 50 2335 11 944
Gateway 50 10367 8 31
简单服务 50 11341 8 17
首页一级菜单渲染 50 270(db,thymeleaf) 267 365
首页渲染(开缓存) 50 290 251 365
首页渲染(开缓存、 优化数据库、 关日 志) 50 700 105 183
三级分类数据获取 50 2(db)/8(加索引)
三级分类( 优化业 务) 50 111 571 896
三 级 分 类 ( 使 用 redis 作为缓存) 50 411 153 217
首页全量数据获取 50 7(静态资源)
Nginx+Gateway 50

中间件越多, 性能损失越大, 大多都损失在网络交互了;

优化业务:Db(MySQL 优化,加索引)、模板的渲染速度(缓存,开启缓存)、静态资源

① 优化-数据库:给数据库表pms_category的parent_id字段添加索引,优化getCatelogJson()方法

② 优化模板引擎的渲染速度:开启thymeleaf的缓存

4.5 优化-nginx动静分离

1、以后将所有项目的静态资源都应该放在nginx里面
2、规则:/static/**所有请求都由nginx直接返回

① 将gulimall-product服务下的static文件夹下的index文件夹放到虚拟机中/mydata/nginx/html/static文件夹下

② 将gulimall-product服务下的index文件夹删除

③ 给gulimall-product下的index.html文件中的href,img,script标签中对静态资源的访问链接加上/static路径

④ 修改nginx的配置文件guliamall.conf:

server {listen       80;server_name  gulimall.com;location /static {root /usr/share/nginx/html;}location / {proxy_set_header Host $host;proxy_pass http://gulimall;}
}

⑤ 重启nginx,然后访问gulimall.com

docker restart nginx

注意:如果js文件没有访问成功,可能是由于浏览器缓存导致的,打开 f12 看一下js文件的访问路径,如果不正确,清除浏览器缓存。

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

① 开启模板引擎缓存

② nginx静态资源和动态资源(thymeleaf)分离

③ 开启 jvisualvm监测 visual gc情况

④ 先使用50个线程来进行压测:

可以看到吞吐量可以达到8左右,仍然很低

同过观察可以发现,老年代和伊甸园区经常爆满,频繁的垃圾回收,垃圾回收太浪费时间了

⑤ 改用200个线程压力测试:

可以看到老年代已满,内存溢出,服务已经崩溃:

⑥ 优化三级分类:

@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {//改为查询所有分类List<CategoryEntity> selectList = baseMapper.selectList(null);List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);//k和v代表每个一级分类Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v -> {//查询到一级分类下的二级分类List<CategoryEntity> level2Catelogs = getParent_cid(selectList,v.getCatId());//将二级分类封装成响应数据voList<Catelog2Vo> catelog2Vos = null;if (level2Catelogs != null) {catelog2Vos = level2Catelogs.stream().map(level2Catelog -> {Catelog2Vo catelog2Vo = new Catelog2Vo();catelog2Vo.setCatalog1Id(v.getCatId().toString());catelog2Vo.setId(level2Catelog.getCatId().toString());catelog2Vo.setName(level2Catelog.getName());//找当前二级分类的三级分类封装成VoList<CategoryEntity> level3Catelogs = getParent_cid(selectList,level2Catelog.getCatId());if(level3Catelogs!=null){List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Catelogs.stream().map((level3Catelog)->{Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();catelog3Vo.setCatalog2Id(level2Catelog.getCatId().toString());catelog3Vo.setId(level3Catelog.getCatId().toString());catelog3Vo.setName(level3Catelog.getName());return catelog3Vo;}).collect(Collectors.toList());catelog2Vo.setCatalog3List(catelog3Vos);}return catelog2Vo;}).collect(Collectors.toList());}return catelog2Vos;}));return parent_cid;
}private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());return collect;
}

使用50个线程压测:http://localhost:10000/index/catalog.json

5. 缓存 - 缓存使用

为了系统性能的提升, 我们一般都会将部分数据放入缓存中, 加速访问。 而 db 承担数据落盘工作。

即时性、 数据一致性要求不高的,访问量大且更新频率不高的数据(读多, 写少)适合使用缓存

data = cache.load(id);//从缓存加载数据
If(data == null){data = db.load(id);//从数据库加载数据cache.put(id,data);//保存到 cache 中
}
return data;

5.1 本地缓存与分布式缓存

① 本地缓存:就是缓存组件和业务组件属于同一个进程,即在同一个项目中

如果时单体应用,永远只有一个服务,那么本地缓存即可,但是如果是分布式系统,可能会部署很多服务器,那么每个服务器都会部署一个本地缓存,就会产生问题。

假如第一次请求负载均衡来到第一个服务器,第一个服务器查数据的时候,缓存中没有,就会从数据库中查询并回设到缓存中,如果第二次请求还能负载均衡到第一个服务器,那么没有问题,但是如果负载均衡到下一个服务器,那么缓存中仍然没有数据,又要查询一次数据库。

当更新缓存的时候,假如我们修改数据的请求来到第一个服务器,那么缓存就修改了,但是后面两个服务器的缓存没有修改,就会导致第一个服务器缓存中的数据和后面两个服务器中缓存中的数据不一致,产生数据一致性问题。

② 分布式缓存:

将所有商品服务缓存的数据都放在一个缓存中间件中

在开发中, 凡是放入缓存中的数据我们都应该指定过期时间, 使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。 避免业务崩溃导致的数据永久不一致问题 。

③ 整合redis:在gulimall-product服务中引入redis坐标,并在application.yml中配置

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:redis:host: 192.168.38.22pool: 6379

5.2 改造三级分类

提高性能的终极大法,加入redis缓存:

@Override
public Map<String, List<Catelog2Vo>> getCatelogJson(){//1、从缓存中查询数据String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");if(StringUtils.isEmpty(catalogJSON)){//2、缓存中没有数据,查询数据库,从数据库查询分类数据Map<String, List<Catelog2Vo>> catelogJsonfromDb = getCatelogJsonfromDb();//3、查到的数据放入缓存中,将对象转换为json传入redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catelogJsonfromDb));return catelogJsonfromDb;}//4、如果缓存有数据,将查询出的数据转为对象,指明转换为的对象类型Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});return result;
}

使用50个线程压测 http://localhost:10000/index/catalog.json,结果爆出大量异常:

这是因为产生了 io.netty.util.internal.OutOfDirectMemoryError,即堆外内存溢出,原因是Spring-boot 2.0 默认使用redis client是lettuce客户端,换为jedis客户端,将redis坐标依赖改为:

<!--引入redis-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions>
</dependency>
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>

加入redis缓存之后,吞吐量明显提升了,从200多提升到了900多,四倍左右:

5.3 本地锁

① 缓存穿透:

缓存穿透是指查询一个一定不存在的数据, 由于缓存是不命中, 将去查询数据库, 但是数据库也无此记录, 我们没有将这次查询的 null 写入缓存, 这将导致这个不存在的数据每次请求都要到存储层去查询, 失去了缓存的意义。在流量大时, 可能 DB 就挂掉了, 要是有人利用不存在的 key 频繁攻击我们的应用, 这就是漏洞。

解决:缓存空结果、 并且设置短的过期时间。

② 缓存雪崩:

缓存雪崩是指在我们设置缓存时采用了相同的过期时间, 导致缓存在某一时刻同时失效, 请求全部转发到 DB, DB 瞬时压力过重雪崩。

解决:原有的失效时间基础上增加一个随机值, 比如 1-5 分钟随机, 这样每一个缓存的过期时间的重复率就会降低, 就很难引发集体失效的事件。

③ 缓存击穿:

对于一些设置了过期时间的 key, 如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候, 需要考虑一个问题: 如果这个 key 在大量请求同时进来前正好失效, 那么所有对这个 key 的数据查询都落到 db, 我们称为缓存击穿。

解决:加锁 ,大量并发只让一个人去查,其他人等待,查到锁之后释放锁,其他人获取到锁,先查缓存就会有数据,不用取db。

④ 本地锁和分布式锁:

无论是给方法上加锁还是同步代码块上加锁,都是给当前实例加锁,当前实例在容器中是单实例的,但是一个项目(一个服务器)是一个容器,也就是说一个商品服务就是一个容器,那么8个商品服务就是8个容器,即8个实例,那么每一个this锁都是不同的锁,就是说我们会给数据库加8把锁,最后的现象就是,第一个商品服务的this锁相当于把10000个请求锁住了,只留一个请求放进来了,第二个商品服务的this锁相当于把10000个请求锁住了,只留一个请求放进来了,以此类推,我们有8台服务器,就会放进来8个线程同时进来取查询数据库相同的数据,所以说本地锁只能锁住当前线程,但是如果高并发请求下,80万个请求同时进来,我们只希望一个请求到达数据库而不是8个请求到达数据库,我们就需要使用分布式锁,当然,分布式锁带来的缺点就是性能比较慢 ,而进程锁性能较快,但是本地锁的缺点就是在分布式情况下,锁不住所有的服务。

对于synchronized和juc包下面的锁都是本地锁;

⑤ 测试本地锁:

使用压力测试,100个线程压测http://localhost:10000/index/catalog.json,我们想要的效果是一旦缓存不命中,我们应该查询一次数据库,如果查询了多次,那就说明加锁是失败的。

@Override
public Map<String, List<Catelog2Vo>> getCatelogJson(){/*** 空结果缓存:解决缓存穿透问题* 设置过期时间(加随机值):解决缓存雪崩* 加锁:解决缓存击穿问题*///1、从缓存中查询数据String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");if(StringUtils.isEmpty(catalogJSON)){System.out.println("缓存不命中,查询数据库。。。。。");//2、缓存中没有数据,查询数据库,从数据库查询分类数据Map<String, List<Catelog2Vo>> catelogJsonfromDb = getCatelogJsonfromDb();//3、查到的数据放入缓存中,将对象转换为json传入redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catelogJsonfromDb),1, TimeUnit.DAYS);return catelogJsonfromDb;}System.out.println("缓存命中。。。。。");//4、如果缓存有数据,将查询出的数据转为对象,指明转换为的对象类型Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});return result;
}//从数据库查询分类数据
public Map<String, List<Catelog2Vo>> getCatelogJsonfromDb() {synchronized (this){//得到锁之后,取查看缓存中是否有数据,如果没有继续查询数据库String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");if(!StringUtils.isEmpty(catalogJSON)){Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});return result;}System.out.println("查询了数据库。。。。。");List<CategoryEntity> selectList = baseMapper.selectList(null);List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(//。。。。。。。省略。。。。。。。。。。。。。。。。return catelog2Vos;}));return parent_cid;}
}

但是压测的结果显示我们并不是只查询了一次数据库,而是出现了多次查询数据库:

原因分析:看下面代码逻辑,我们锁住的是查询数据库那一段逻辑,查询完数据库之后,就会释放锁,然后回设缓存,当线程1释放完锁之后,还没来得及将数据放入缓存中,线程2又查询缓存,发现缓存中没有数据,就会又查询数据库,导致布置一次的查询了数据库。

if(StringUtils.isEmpty(catalogJSON)){System.out.println("缓存不命中,查询数据库。。。。。");//2、缓存中没有数据,查询数据库,从数据库查询分类数据Map<String, List<Catelog2Vo>> catelogJsonfromDb = getCatelogJsonfromDb();//3、查到的数据放入缓存中 redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catelogJsonfromDb));return catelogJsonfromDb;
}

解决方法:线程1查询完数据库之后还不能释放锁,应该把数据回设到缓存之后再释放锁,这样黑色区域就是一个原子操作。

@Override
public Map<String, List<Catelog2Vo>> getCatelogJson(){/*** 空结果缓存:解决缓存穿透问题* 设置过期时间(加随机值):解决缓存雪崩* 加锁:解决缓存击穿问题*///1、从缓存中查询数据String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");if(StringUtils.isEmpty(catalogJSON)){System.out.println("缓存不命中,查询数据库。。。。。");//2、缓存中没有数据,查询数据库,从数据库查询分类数据Map<String, List<Catelog2Vo>> catelogJsonfromDb = getCatelogJsonfromDb();return catelogJsonfromDb;}System.out.println("缓存命中。。。。。");//4、如果缓存有数据,将查询出的数据转为对象,指明转换为的对象类型Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});return result;
}//从数据库查询分类数据
public Map<String, List<Catelog2Vo>> getCatelogJsonfromDb() {synchronized (this){//得到锁之后,取查看缓存中是否有数据,如果没有继续查询数据库String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");if(!StringUtils.isEmpty(catalogJSON)){Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});return result;}System.out.println("查询了数据库。。。。。");List<CategoryEntity> selectList = baseMapper.selectList(null);List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);//k和v代表每个一级分类Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(// 。。。。。。。。。。省略 。。。。。。。。。。。return catelog2Vos;}));//3、查到的数据放入缓存中,将对象转换为json传入redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(parent_cid),1, TimeUnit.DAYS);return parent_cid;}
}

经过压测,只查询了一次数据库:

⑥ 本地锁再分布式下的问题:

除了gulimall-product项目,我们再重新创建几个商品服务项目:

使用100个线程来压测商品服务,请求先到达nginx,nginx再将请求交给网关,由网关负载均衡的到达每个服务:

经过测试发现,每个商品服务都有一次查询了数据库,和我们分析的相同,会有四个线程查询数据库:

5.4 redis分布式锁

我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。等待可以自旋的方式。

所有的商品服务都到一个公共的地方占锁,如果占到了,就能就绪执行逻辑,如果没占到,就等待。

key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”set if not exists”的简写。

① 分布式锁演进1:

public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedisLock() {//1、占分布式锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");if(lock){// 加锁成功,执行业务Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();// 删除锁redisTemplate.delete("lock");return dataFromDb;}else{// 加锁失败,重试return getCatelogJsonfromDbWithRedisLock();//自旋}
}

问题:如果我们执行业务的时候,出现了异常,导致程序抛出异常直接推出了,都没有执行删除锁的逻辑,那么其他人想要获取锁的时候就获取不到了,因此就会出现死锁。有人说可以将这段业务代码try catch,释放锁的逻辑放在finally中,但是如果要执行finally时程序断电了,仍然会导致死锁。

解决方法:给redis加一个过期时间,即使我们没能释放锁,redis也会把锁自动删除

② 分布式锁演进2:

public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedisLock() {//1、占分布式锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");if(lock){// 加锁成功,执行业务//2、设置过期时间redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();// 删除锁redisTemplate.delete("lock");return dataFromDb;}else{// 加锁失败,重试return getCatelogJsonfromDbWithRedisLock();//自旋}
}

问题:刚要去设置过期时间,突然断电,那么又会出现死锁。原因时获取锁和设置过期时间不是原子的,因此占分布式锁和设置过期时间这2步必须是原子的,redis支持setnx ex命令。

③ 分布式锁演进3:

public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedisLock() {//1、占分布式锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);if(lock){// 加锁成功,执行业务// 2、设置过期时间// redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();// 删除锁redisTemplate.delete("lock");return dataFromDb;}else{// 加锁失败,重试return getCatelogJsonfromDbWithRedisLock();//自旋}
}

问题:由于业务代码执行时间很长,有可能自己的锁过期了,其他线程发现锁过期了就会占用锁,那么当第一个线程执行删除锁的时候,就会将其他线程正在持有的锁删除了。

解决:占锁的时候,值指定为uuid,每个人匹配自己的锁才可以删除。

④ 分布式锁演进4:

public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedisLock() {String uuid = UUID.randomUUID().toString();//1、占分布式锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);if(lock){// 加锁成功,执行业务// 2、设置过期时间// redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();// 删除锁// redisTemplate.delete("lock");String lockValue = redisTemplate.opsForValue().get("lock");if(uuid.equals(lockValue)){//删除自己的锁redisTemplate.delete("lock");}return dataFromDb;}else{// 加锁失败,重试return getCatelogJsonfromDbWithRedisLock();//自旋}
}

问题:网络传输的原因,加入线程1获取到了lockvalue,也比较成功了,但是正要删除锁的时候,锁已经过期了,别人已经设置了新的值,那么我们删除的就是别人的锁。

解决:因此获取值lockvalue对比,对比成功后删除锁,这2步必须是原子性的。

⑤ 分布式锁演进5:

public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedisLock() {String uuid = UUID.randomUUID().toString();//1、占分布式锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);if(lock){// 加锁成功,执行业务// 2、设置过期时间// redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();// 删除锁// redisTemplate.delete("lock");// String lockValue = redisTemplate.opsForValue().get("lock");// if(uuid.equals(lockValue)){//删除自己的锁// redisTemplate.delete("lock");// }String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";//删除锁Integer lock1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);return dataFromDb;}else{// 加锁失败,重试return getCatelogJsonfromDbWithRedisLock();//自旋}
}

官方文档:http://redis.cn/commands/set.html

解决锁的自动续期问题:将锁的过期时间设置的长一点,当执行完业务代码后就删除锁

public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedisLock() {String uuid = UUID.randomUUID().toString();//1、占分布式锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);if(lock){Map<String, List<Catelog2Vo>> dataFromDb;try {// 加锁成功,执行业务dataFromDb = getDataFromDb();} finally {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";//删除锁Integer lock1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);}return dataFromDb;}else{// 加锁失败,重试return getCatelogJsonfromDbWithRedisLock();//自旋}
}

⑥ 进行压力测试:500个请求,当执行完之后,4个商品服务只有一个商品服务查询了数据库。

5.5 Redisson分布式锁

官方文档:https://github.com/redisson//redisson/wiki/1.-%E6%A6%82%E8%BF%B0

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

redisson和jedis以及lettuce一样都是redis客户端,只不过redission功能更强大。

① 引入redssion坐标依赖:

<!-- 以后使用redisson作为所有分布式锁,分布式对象等功能框架-->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.12.0</version>
</dependency>

② 配置redisson,程序化的配置方法是通过构建Config对象实例来实现的:

package com.atguigu.gulimall.product.config;@Configuration
public class MyRedissonConfig {//注册RedissonClient对象@Bean(destroyMethod="shutdown")RedissonClient redisson() throws IOException {Config config = new Config();config.useSingleServer().setAddress("redis://192.168.38.22:6379");RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
}

③ Redisson测试可重入锁(Reentrant Lock):

@ResponseBody
@GetMapping("/hello")
public String hello(){//获取一把锁RLock lock = redissonClient.getLock("my-lock");//加锁lock.lock();//锁的自动续期,如果业务执行时间超长,运行期间会自动给锁续期30秒时间,不用担心业务时间长,锁自动过期//加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30秒后也会自动删除try {System.out.println("加锁成功,执行业务.... "+Thread.currentThread().getId());Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();} finally {//手动解锁System.out.println("解锁..."+Thread.currentThread().getId());lock.unlock();}return "hello";
}

如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

@ResponseBody
@GetMapping("/hello")
public String hello(){//获取一把锁RLock lock = redissonClient.getLock("my-lock");// 加锁// 设置的自动解锁时间一定要大于业务执行时间,因为在锁时间到了以后,不会自动续期lock.lock(10, TimeUnit.SECONDS);try {System.out.println("加锁成功,执行业务.... "+Thread.currentThread().getId());Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();} finally {//解锁System.out.println("解锁..."+Thread.currentThread().getId());lock.unlock();}return "hello";
}

结论:

lock.lock(),即没有指定锁的过期时间,就是用30s,即看门狗的默认时间,只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒就会自动续期到30秒。

lock.lock(10, TimeUnit.SECONDS),默认锁的过期时间就是我们指定的时间。

在实际中,使用 lock.lock(10, TimeUnit.SECONDS),可以省略整个续期时间,可以把时间加大一点比如30秒。

④ Redisson测试读写锁:

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

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

1、只加读锁:

@GetMapping("/read")
@ResponseBody
public String readValue(){RReadWriteLock lock = redissonClient.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(30000);} catch (Exception e) {e.printStackTrace();} finally {rLock.unlock();System.out.println("读锁释放"+Thread.currentThread().getId());}return  s;
}

测试:先在writeValue中加入值111,测试http://localhost:10000/read,加读锁后,可以读到数据111。

2、先加写锁,再加读锁:

@GetMapping("/write")
@ResponseBody
public String writeValue(){// 获取一把锁RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");String s = "";// 加写锁RLock rLock = lock.writeLock();try {//1、改数据加写锁,读数据加读锁rLock.lock();System.out.println("写锁加锁成功..."+Thread.currentThread().getId());s = UUID.randomUUID().toString();Thread.sleep(30000);redisTemplate.opsForValue().set("writeValue",s);} catch (Exception e) {e.printStackTrace();} finally {rLock.unlock();System.out.println("写锁释放"+Thread.currentThread().getId());}return  s;
}

测试:先访问http://localhost:10000/write,就会给数据writeValue加写锁,然后再访问http://localhost:10000/read,此时并不会立刻给数据加读锁,而是需要等待写锁释放后,才能加读锁。

3、先加读锁,再加写锁:有读锁,写锁需要等待

4、先加读锁,再加读锁:并发读锁相当于无锁模式,会同时加锁成功

总结:只要有写锁的存在,都必须等待,写锁是一个排他锁,只能有一个写锁存在,读锁是一个共享锁,可以有多个读锁同时存在。

⑤ Redisson 测试信号量Semaphore :

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。

/*** 车库停车,* 3车位* 信号量也可以用作分布式限流;
*/
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {RSemaphore park = redissonClient.getSemaphore("park");//当车位减为0时,阻塞等待,等待车位,当调用go()方法时,车位就会释放,就继续执行park.acquire();//获取一个信号,获取一个值,占一个车位return "ok";
}@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {RSemaphore park = redissonClient.getSemaphore("park");park.release();//释放一个车位return "ok";
}

⑥ Redisson 测试闭锁CountDownLatch:

/*** 放假,锁门* 5个班全部走完,我们可以锁大门*/
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {RCountDownLatch door = redissonClient.getCountDownLatch("door");door.trySetCount(5);door.await(); //等待闭锁都完成return "放假了...";
}@GetMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") Long id){RCountDownLatch door = redissonClient.getCountDownLatch("door");door.countDown();//计数减一;return id+"班的人都走了...";
}

测试:访问http://localhost:10000/lockDoor,发现请求一直在阻塞,count的数量为5

依次调用http://localhost:10000/gogogo/1,http://localhost:10000/gogogo/2 … http://localhost:10000/gogogo/5,当count减为0时,就会打印放假了…

5.6 缓存一致性

将之前的三级分类业务代码中使用的redis分布式锁改为Redisson分布式锁:

//缓存中的数据如何和数据库中的数据保持一致
public Map<String, List<Catelog2Vo>> getCatelogJsonfromDbWithRedissonLock() {String uuid = UUID.randomUUID().toString();//1、占分布式锁RLock lock = redissonClient.getLock("CatelogJson-lock");lock.lock();Map<String, List<Catelog2Vo>> dataFromDb;try {dataFromDb = getDataFromDb();} finally {lock.unlock();}return dataFromDb;
}

问题:如果我们三级分类中的数据修改了,数据库中的数据就改变了,那么缓存中还是之前的数据,就会出现缓存和数据库中的数据不一致的情况。

缓存里面的数据如何和数据库中的数据保持一致:双写模式和失效模式

① 双写模式 :

所谓双写模式,就是写数据库之后同时写缓存。

出现问题:由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致 (数据库和缓存中数据不一致,数据库是2中的数据,缓存中是1的数据),脏数据问题: 这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据 。

解决方法1:加锁,将写数据库和写缓存这段逻辑加锁,同时只有一个线程可以操作

解决方法2:看业务是否允许暂时性的数据不一致问题,如果允许,可以不管这个缓存不一致的事情,将数据放入缓存的时候,设置缓存过期时间,只要数据过期了,就会重新从数据库中加载数据。(数据库修改后的值和最终的值之间有一段延迟时间,但是最终数据是一致的)

② 失效模式 :

所谓失效模式,就是先写数据库然后删除缓存。

出现的问题:一个线程先写数据库db-1,然后删除缓存,另一个线程接着写数据库db-2,还没来得写,第三个线程就读取了数据库db-1,并更新了缓存,因此数据库中存放的是db-2,而缓存中存放的是db-1。

解决方法1:缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
解决方法2:读写数据的时候,加上分布式的读写锁。(写的时候,其他线程别想读,就不会出现问题)

如果数据经常修改,不需要设置缓存,直接查数据库即可,不经常修改的数据才加换缓存。

③ 缓存数据一致性-解决方案 :

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

(1) 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可;

(2) 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式;

(3) 缓存数据+过期时间也足够解决大部分业务对于缓存的要求;

(4) 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

总结:

(1) 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。

(2) 我们不应该过度设计,增加系统的复杂性,遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点 。

经过分析我们系统采用的缓存一致性解决方案为:失效模式

(1) 缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
(2) 读写数据的时候,加上分布式的读写锁。(写的时候,其他线程别想读),如果经常写又经常读就会对性能产生极大的影响,如果只是经常读偶尔写一次 ,就不会有太大影响。

5.7 Spring Cache

缓存的读模式:先从缓存中读取数据,缓存中没有再从数据库中读取数据,读取后放入缓存

缓存的写模式:写数据的时候,可以使用双写模式或者失效模式。

① 导入缓存依赖坐标:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>

② 写配置application.properties:

# 使用redis作为缓存
spring.cache.type=redis

③ 开启缓存功能:@EnableCaching

@EnableCaching
@EnableFeignClients(basePackages ="com.atguigu.gulimall.product.feign" )
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {public static void main(String[] args) {SpringApplication.run(GulimallProductApplication.class,args);}
}

④ 缓存的读模式:@Cacheable ,主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。

1)、如果缓存中有,方法不用调用,如果缓存中没有,才会调用方法
2)、key默认自动生成;缓存的名字:SimpleKey [](自主生成的key值)
3)、缓存的value的值。默认使用jdk序列化机制,将序列化后的数据存到redis
4)、默认ttl时间 -1;

// 当前方法的结果需要缓存
// 如果缓存中有,方法不用调用,如果缓存中没有,会调用该方法,并将方法的结果缓存
// 每一个需要缓存的数据都要来指定要放到哪个名字的缓存
@Cacheable({"category"})
@Override
public List<CategoryEntity> getLevel1Categorys() {return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
}

我们发现缓存中的数据key就是自动生成的SimpleKey [],值是二进制数据。

⑤ 自定义缓存配置 :

指定生成的缓存使用的key, key属性指定,接受一个SpEL

@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
}

指定缓存的数据的存活时间: 配置文件中修改ttl

spring.cache.type=redis
spring.cache.redis.time-to-live=3600000

将数据保存为json格式, 自定义RedisCacheConfiguration即可:

@Configuration
@EnableCaching
public class MyCacheConfig {@BeanRedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){RedisCacheConfiguration config  = RedisCacheConfiguration.defaultCacheConfig();//修改key和value的序列化机制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.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true# 是否缓存null值,防止缓存穿透
spring.cache.redis.cache-null-values=true

⑥ 缓存的写模式:失效模式,@CacheEvict 删除缓存

@CacheEvict(value = "category",key = "'getLevel1Categorys'")
@Transactional
@Override
public void updateCascade(CategoryEntity category) {this.updateById(category);categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

现在缓存中是有数据的:

现在我们来修改三级菜单,再来查看缓存,发现缓存被删除了。

⑦ 重新改造getCatelogJson()方法,加上@Cacheable注解

@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {System.out.println("getLevel1Categorys..............");return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
}@Cacheable(value = "category",key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson(){System.out.println("查询了数据库.......................");List<CategoryEntity> selectList = baseMapper.selectList(null);List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);//k和v代表每个一级分类Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v -> {//查询到一级分类下的二级分类List<CategoryEntity> level2Catelogs = getParent_cid(selectList, v.getCatId());//将二级分类封装成响应数据voList<Catelog2Vo> catelog2Vos = null;if (level2Catelogs != null) {catelog2Vos = level2Catelogs.stream().map(level2Catelog -> {Catelog2Vo catelog2Vo = new Catelog2Vo();catelog2Vo.setCatalog1Id(v.getCatId().toString());catelog2Vo.setId(level2Catelog.getCatId().toString());catelog2Vo.setName(level2Catelog.getName());//找当前二级分类的三级分类封装成VoList<CategoryEntity> level3Catelogs = getParent_cid(selectList, level2Catelog.getCatId());if (level3Catelogs != null) {List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Catelogs.stream().map((level3Catelog) -> {Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();catelog3Vo.setCatalog2Id(level2Catelog.getCatId().toString());catelog3Vo.setId(level3Catelog.getCatId().toString());catelog3Vo.setName(level3Catelog.getName());return catelog3Vo;}).collect(Collectors.toList());catelog2Vo.setCatalog3List(catelog3Vos);}return catelog2Vo;}).collect(Collectors.toList());}return catelog2Vos;}));return parent_cid;
}

测试:我们访问gulimall.com,两个方法的数据都缓存了

现在我改了菜单数据之后,应该将这两个缓存的缓存数据都删掉,之前我们删除一个缓存的时候只需要指明缓存的key和value即可,如:

@CacheEvict(value = "category",key = "'getLevel1Categorys'")
@Transactional
@Override
public void updateCascade(CategoryEntity category) {this.updateById(category);categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

但是现在我想将两个缓存的缓存数据都删除掉,怎么办呢?

方法1:同时进行多种缓存操作

@Caching(evict = {@CacheEvict(value = "category",key = "'getLevel1Categorys'"),@CacheEvict(value = "category",key = "'getCatelogJson'")
})
@Transactional
@Override
public void updateCascade(CategoryEntity category) {this.updateById(category);categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

方法2:指定删除某个分区下的所有数据

@CacheEvict(value = "category",allEntries = true)
@Transactional
@Override
public void updateCascade(CategoryEntity category) {this.updateById(category);categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

存储同一类型的数据,都可以指定同一分区,配置文件可以不指定缓存缓存前缀,这样分区名就是缓存前缀。

⑧ Spring Cache的不足:

1、读模式:

缓存穿透:查询一个null数据,解决:开启缓存空数据,spring.cache.redis.cache-null-values=true

缓存击穿:大量并发进来查询一个正好过期的数据,解决:加锁

缓存雪崩:大量的key同时过期,解决:加过期时间,spring.cache.redis.time-to-live=3600000

2、写模式:缓存与数据库一致

读写加锁。

引入Canal,感知到MySQL的更新去更新数据库。

读多写多,直接去数据库查询就行。

6. 商城业务 - 检索服务

6.1 搭建页面环境

① 引入thymeleaf坐标依赖:

<!-- 模板引擎: thymeleaf -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

② 将项目提供的搜索页中的index.html文件放在gulimall-search服务中的templates文件夹下面,并通过ctrl+R进行替换修改其中的href和src路径。

③ 将静态资源文件上传到Linux中的/mydata/nginx/html/static/search文件中

④ 配置域名转发:

配置域名search.gulimall.com :

search.gulimall.com这个域名应该到nginx,nginx再转交给后台网关,网关再转交给gulimall-search服务。所有的静态资源都应该由nginx来返回,其他的请求交给网关转发给微服务。

进入linux的路径/mydata/nginx/conf/conf.d下的gulimall.conf文件夹:

server {listen       80;server_name  gulimall.com  *.gulimall.com;
}

配置网关的域名转发:

- id: gulimall_search_routeuri: lb://gulimall-searchpredicates:- Host=search.gulimall.com

⑤ 关闭thymeleaf缓存:

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search
server.port=12000
spring.thymeleaf.cache=false

⑥ 引入热启动依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional>
</dependency>

⑦ 通过在首页点击分类名称,跳转到检索页面,并根据分类名称进行检索:

在首页点击手机,页面却跳转到了http://search.gmall.com/list.html?catalog3Id=225,路径错误

经过排查,发现是catalogLoader.js文件中的路径错了,进入linux的/mydata/nginx/html/static/index/js文件夹下的catalogLoader.js文件,将文件中的请求路径改正确即可。

根据分类名称进行检索,路径会带上分类catelog3Id:

⑧ 通过在首页输入检索关键字,点击搜索跳转到检索页面,并根据检索关键字进行检索:

在首页gulimall.com页面输入dada点击搜索,发现跳转的页面不正确,

修改index.html文件中search()方法的加载地址:

<script type="text/javascript">function search() {var keyword=$("#searchText").val()window.location.href = "http://search.gulimall.com/list.html?keyword=" + keyword;}
</script>

根据检索关键字进行检索,路径会带上检索关键字:

6.2 通过检索条件进行检索

除了在检索页面通过catelog3Id和检索关键字keyword进行检索商品外,还有其他的检索条件进行检索。

① 封装页面所有可能传递过来的参数:

完整的 url 参数 :keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1 &catalogId=1&attrs=1_3G:4G:5G&attrs=2_骁龙 845&attrs=4_高清屏

/*** 封装页面所有可能传递过来的参数*/
@Data
public class SearchParam {private String keyword;//页面传递过来的全文匹配关键字  vprivate Long catalog3Id;//三级分类id   v/***   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*  attrs=2_5存:6寸*/private Integer hasStock;//是否只显示有货    0(无库存)1(有库存)private String skuPrice;//价格区间查询  private List<Long> brandId;//按照品牌进行查询,可以多选  private List<String> attrs;//按照属性进行筛选  private Integer pageNum = 1;//页码
}

② 封装根据检索条件返回给页面的检索结果:

@Data
public class SearchResult {//查询到的所有商品信息private List<SkuEsModel> products;/*** 以下是分页信息*/private Integer pageNum;//当前页码private Long total;//总记录数private Integer totalPages;//总页码private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的所有分类private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的所有属性//==========以上是返回给页面的所有信息============@Datapublic static class BrandVo{private Long brandId;private String brandName;private String brandImg;}@Datapublic static class CatalogVo{private Long catalogId;private String catalogName;}@Datapublic static class AttrVo{private Long attrId;private String attrName;private List<String> attrValue;}
}

③ 根据传递来的页面的查询参数,去es中检索商品并响应:

GET product/_search
{"query": { "bool": {"must": [  {"match": {  "skuTitle": "Apple iPhone"  }}],"filter": [ {"term": {  "catalogId": "225" }},{"terms": { "brandId": [  "8","11"]}},{"nested": { "path": "attrs","query": {"bool": {"must": [{"term": { "attrs.attrId": { "value": "8"}}},{"terms": {  "attrs.attrValue": [ "高通","intel i11"]}}]}}}},{"term": {"hasStock": {  "value": true}}},{"range": {  "skuPrice": { "gte": 0, "lte": 7000  }}}]}},"sort": [  {"skuPrice": { "order": "desc"}}],"from": 0,  "size": 4,  "highlight": { "fields": {"skuTitle": {}},  "pre_tags": "<b style='color:red'>",  "post_tags": "</b>"  },"aggs": { "brand_agg": { "terms": {"field": "brandId", "size": 50},"aggs": {  "brand_name_agg": {"terms": {"field": "brandName",  "size": 1}},"brand_img_agg": {"terms": {"field": "brandImg",  "size": 1}}}},"catalog_agg": {  "terms": {"field": "catalogId",  "size": 20},"aggs": {  "catalog_name_agg": {"terms": {"field": "catalogName",  "size": 1}}}},"attr_agg": { "nested": {  "path": "attrs"  },"aggs": {  "attr_id_agg": {"terms": {"field": "attrs.attrId",  "size": 10},"aggs": {  "attr_name_agg": {"terms": {"field": "attrs.attrName",  "size": 1}},"attr_value_agg": {"terms": {"field": "attrs.attrValue",  "size": 50}}}}}}}
}

④ SearchRequest构建 :

@Controller
public class SearchController {@AutowiredMallSearchService mallSearchService;@GetMapping("/list.html")public String listPage(SearchParam searchParam, Model model){SearchResult result = mallSearchService.search(searchParam);return "list";}
}
/*** 准备检索请求* @param searchParam* @return*/
private SearchRequest buildSearchRequest(SearchParam searchParam) {//构建DLS语句SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();/*** 查询:模糊匹配,过滤(按照属性,分裂,品牌,价格区间,库存)*/BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// mustif(!StringUtils.isEmpty(searchParam.getKeyword())) {boolQuery.must(QueryBuilders.matchQuery("skuTitle",searchParam.getKeyword()));}// filter ---- Catalog3Idif(searchParam.getCatalog3Id()!=null){boolQuery.filter(QueryBuilders.termQuery("catalogId",searchParam.getCatalog3Id()));}// filter ---- brandIdif(searchParam.getBrandId()!=null && searchParam.getBrandId().size()>0){boolQuery.filter(QueryBuilders.termsQuery("brandId",searchParam.getBrandId()));}// filter ---- hasStockboolQuery.filter(QueryBuilders.termsQuery("hasStock",searchParam.getHasStock()==1));// filter ---- skuPriceif(!StringUtils.isEmpty(searchParam.getSkuPrice())){RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");// 1_500   _500  500_String[] s = searchParam.getSkuPrice().split("_");if(s.length==2){rangeQuery.gte(s[0]).lte(s[1]);}else if(s.length==1){if(searchParam.getSkuPrice().startsWith("_")){rangeQuery.lte(s[0]);}if(searchParam.getSkuPrice().endsWith("_")){rangeQuery.gte(s[0]);}}boolQuery.filter(rangeQuery);}// filter ---- attrsif(searchParam.getAttrs()!=null && searchParam.getAttrs().size()>0){//attrs=1_5寸:8寸&attrs=2_16G:8Gfor(String attrStr:searchParam.getAttrs()){BoolQueryBuilder nestedboolQuery = QueryBuilders.boolQuery();String[] s = attrStr.split("_");String attrId = s[0];String[] attrValues = s[1].split(":");nestedboolQuery.must(QueryBuilders.termsQuery("attrs.attrId",attrId));nestedboolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedboolQuery, ScoreMode.None);boolQuery.filter(nestedQuery);}}sourceBuilder.query(boolQuery);/*** 排序,分页,高亮*/// sort ------- skuPriceif(!StringUtils.isEmpty(searchParam.getSort())){String sort = searchParam.getSort();String[] s = sort.split("_");SortOrder order = s[1].equalsIgnoreCase("asc")? SortOrder.ASC:SortOrder.DESC;sourceBuilder.sort(s[0],order);}// 分页sourceBuilder.from((searchParam.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);//高亮if(!StringUtils.isEmpty(searchParam.getKeyword())){HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field("skuTitle");highlightBuilder.preTags("<b style='color:red'>");highlightBuilder.postTags("</b>");sourceBuilder.highlighter(highlightBuilder);}/*** 聚合分析*/// 品牌聚合TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);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);// 分类聚合TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));sourceBuilder.aggregation(catalog_agg);// 属性聚合NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId").size(10);attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));attr_agg.subAggregation(attr_id_agg);sourceBuilder.aggregation(attr_agg);String s = sourceBuilder.toString();System.out.println("构建的DSL:"+s);SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},sourceBuilder);return searchRequest;
}

测试:localhost:12000/list.html?keyword=Apple iPhone&catelog3Id=225&atts=8_高通:Intel i11&skuPrice=_7000

⑤ SearchResponse构建 :

private SearchResult buildSearchResult(SearchResponse response,SearchParam searchParam) {SearchResult result = new SearchResult();SearchHits hits = response.getHits();//查询到的所有商品List<SkuEsModel> esModels = new ArrayList<>();if (hits.getHits() != null && hits.getHits().length > 0) {for (SearchHit hit : hits.getHits()) {String sourceAsString = hit.getSourceAsString();SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);if (!StringUtils.isEmpty(searchParam.getKeyword())) {HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");String string = skuTitle.getFragments()[0].string();esModel.setSkuTitle(string);}esModels.add(esModel);}}result.setProducts(esModels);// 分页信息-页码result.setPageNum(searchParam.getPageNum());// 分页信息-总记录树long total = hits.getTotalHits().value;result.setTotal(total);// 分页信息-总页码-计算  11/2 = 5 .. 1int totalPages = (int) total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int) total / EsConstant.PRODUCT_PAGESIZE : ((int) total / EsConstant.PRODUCT_PAGESIZE + 1);result.setTotalPages(totalPages);//当前所有商品涉及的分类信息ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();for (Terms.Bucket bucket : buckets) {SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();//得到分类idString keyAsString = bucket.getKeyAsString();catalogVo.setCatalogId(Long.parseLong(keyAsString));//得到分类名ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();catalogVo.setCatalogName(catalog_name);catalogVos.add(catalogVo);}result.setCatalogs(catalogVos);//当前所有商品涉及的品牌信息List<SearchResult.BrandVo> brandVos = new ArrayList<>();ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");for (Terms.Bucket bucket : brand_agg.getBuckets()) {SearchResult.BrandVo brandVo = new SearchResult.BrandVo();//1、得到品牌的idlong brandId = bucket.getKeyAsNumber().longValue();//2、得到品牌的名String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();//3、得到品牌的图片String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();brandVo.setBrandId(brandId);brandVo.setBrandName(brandName);brandVo.setBrandImg(brandImg);brandVos.add(brandVo);}result.setBrands(brandVos);//当前所有商品涉及到的所有属性信息List<SearchResult.AttrVo> attrVos = new ArrayList<>();ParsedNested attr_agg = response.getAggregations().get("attr_agg");ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {SearchResult.AttrVo attrVo = new SearchResult.AttrVo();//1、得到属性的idlong attrId = bucket.getKeyAsNumber().longValue();//2、得到属性的名字String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();//3、得到属性的所有值List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {String keyAsString = ((Terms.Bucket) item).getKeyAsString();return keyAsString;}).collect(Collectors.toList());attrVo.setAttrId(attrId);attrVo.setAttrName(attrName);attrVo.setAttrValue(attrValues);attrVos.add(attrVo);}result.setAttrs(attrVos);return result;
}

6.3 页面数据渲染

① 商品

② 品牌、分类和属性

③ 分页

在 SearchResult 类中添加一个响应字段

private List<Integer> pageNavs;

在MallSearchServiceImpl类中:

List<Integer> pageNavs = new ArrayList<>();
for(int i=1;i<totalPages;i++){pageNavs.add(i);
}
result.setPageNavs(pageNavs);

④ 综合排序

⑤ 是否有货

⑥ 面包屑导航

/*** 自动将页面提交过来的所有请求查询参数封装成指定的对象*/
@GetMapping("/list.html")
public String listPage(SearchParam searchParam, Model model, HttpServletRequest request){//获取查询请求参数searchParam.set_queryString(request.getQueryString());//1、根据传递来的页面的查询参数,去es中检索商品SearchResult result = mallSearchService.search(searchParam);model.addAttribute("result",result);return "list";
}

在 SearchParam 中添加:

private String _queryString;//原生的所有查询条件

在SearchResult 中添加:

//面包屑导航数据
private List<NavVo> navs = new ArrayList<>();
private List<Long> attrIds = new ArrayList<>();@Data
public static class NavVo{private String navName;private String navValue;private String link;
}

在MallSearchServiceImpl类中的buildSearchResult()方法中添加面包屑导航功能:

// 构建面包屑导航功能
if(searchParam.getAttrs()!=null && searchParam.getAttrs().size()>0){List<SearchResult.NavVo> collect = searchParam.getAttrs().stream().map(attr -> {//1、分析每个attrs传过来的查询参数值。SearchResult.NavVo navVo = new SearchResult.NavVo();// 导航栏属性的前面是id,后面是属性值// attrs=2_5存:6寸String[] s = attr.split("_");navVo.setNavValue(s[1]);//远程调用商品服务,根据id查询属性信息R r = productFeignService.attrInfo(Long.parseLong(s[0]));result.getAttrIds().add(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]);}//2、取消了这个面包屑以后,我们要跳转到那个地方.将请求地址的url里面的当前置空// 拿到所有的查询条件,去掉当前。// attrs=  15_海思(Hisilicon)String replace = replaceQueryString(searchParam, attr,"attrs");navVo.setLink("http://search.gulimall.com/list.html?"+replace);return navVo;}).collect(Collectors.toList());result.setNavs(collect);
}//品牌,分类
if(param.getBrandId()!=null && param.getBrandId().size()>0){List<SearchResult.NavVo> navs = result.getNavs();SearchResult.NavVo navVo = new SearchResult.NavVo();navVo.setNavName("品牌");// 远程调用商品服务,查询所有品牌R r = productFeignService.brandsInfo(param.getBrandId());if(r.getCode() == 0){List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {});StringBuffer buffer = new StringBuffer();String replace = "";for (BrandVo brandVo : brand) {buffer.append(brandVo.getBrandName()+";");replace = replaceQueryString(param, brandVo.getBrandId()+"","brandId");}navVo.setNavValue(buffer.toString());navVo.setLink("http://search.gulimall.com/list.html?"+replace);}navs.add(navVo);
}
private String replaceQueryString(SearchParam param, String value,String key) {String encode = null;try {encode = URLEncoder.encode(value, "UTF-8");encode = encode.replace("+","%20");//浏览器对空格编码和java不一样} catch (UnsupportedEncodingException e) {e.printStackTrace();}return param.get_queryString().replace("&"+key+"=" + encode, "");
}

远程调用的feign接口:

@FeignClient("gulimall-product")
public interface ProductFeignService {@GetMapping("/product/attr/info/{attrId}")public R attrInfo(@PathVariable("attrId") Long attrId);@GetMapping("/product/brand/infos")public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);
}

在gulimall-product服务的BrandController中共添加:

@GetMapping("/infos")
public R info(@RequestParam("brandIds") List<Long> brandIds){List<BrandEntity> brand =  brandService.getBrandsByIds(brandIds);return R.ok().put("brand", brand);
}
@Override
public List<BrandEntity> getBrandsByIds(List<Long> brandIds) {return baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id",brandIds));
}

最终达到的效果就是,当我们选择一个标签时,对应的标签就会消失:

7. 商城业务 - 商品详情

7.1 线程池

① 初始化线程的 4 种方式 :

1) 、 继承 Thread
2) 、 实现 Runnable 接口
3) 、 实现 Callable 接口 + FutureTask (可以拿到返回结果, 可以处理异常)
4) 、 线程池

方式 1 和方式 2: 主进程无法获取线程的运算结果。 不适合当前场景
方式 3: 主进程可以获取线程的运算结果, 但是不利于控制服务器中的线程资源。 可以导致服务器资源耗尽。
方式 4: 通过如下两种方式初始化线程池

通过线程池性能稳定, 也可以获取执行结果, 并捕获异常。 但是, 在业务复杂情况下, 一个异步调用可能会依赖于另一个异步调用的执行结果

Executors.newFiexedThreadPool(3);
//或者
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit,
workQueue, threadFactory, handler);

通过线程池性能稳定, 也可以获取执行结果, 并捕获异常。 但是, 在业务复杂情况下, 一个异步调用可能会依赖于另一个异步调用的执行结果。

② 常见的 4 种线程池:

newCachedThreadPool :创建一个可缓存线程池, 如果线程池长度超过处理需要, 可灵活回收空闲线程, 若
无可回收, 则新建线程。
newFixedThreadPool:创建一个定长线程池, 可控制线程最大并发数, 超出的线程会在队列中等待。
newScheduledThreadPool:创建一个定长线程池, 支持定时及周期性任务执行。
newSingleThreadExecutor:创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

③ 开发中为什么使用线程池?

降低资源的消耗: 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗。

提高响应速度:因为线程池中的线程数没有超过线程池的最大上限时, 有的线程处于等待分配任务的状态, 当任务来时无需创建新的线程就能执行。

提高线程的可管理性:线程池会根据当前系统特点对池内的线程进行优化处理, 减少创建和销毁线程带来的系统开销。 无限的创建和销毁线程不仅消耗系统资源, 还降低系统的稳定性, 使用线程池进行统一分配 。

7.2 CompletableFuture 异步编排

业务场景:查询商品详情页的逻辑比较复杂, 有些数据还需要远程调用, 必然需要花费更多的时间。

假如商品详情页的每个查询, 需要如下标注的时间才能完成 ,那么, 用户需要 5.5s 后才能看到商品详情页的内容。 很显然是不能接受的。如果有多个线程同时完成这 6 步操作, 也许只需要 1.5s 即可完成响应。

Future 是 Java 5 添加的类, 用来描述一个异步计算的结果。 你可以使用isDone方法检查计算是否完成, 或者使用get阻塞住调用线程, 直到计算完成返回结果, 你也可以使用cancel方法停止任务的执行。

虽然Future以及相关使用方法提供了异步执行任务的能力, 但是对于结果的获取却是很不方便, 只能通过阻塞或者轮询的方式得到任务的结果。 阻塞的方式显然和我们的异步编程的初衷相违背, 轮询的方式又会耗费无谓的 CPU 资源, 而且也不能及时地得到计算结果, 为什么不能用观察者设计模式当计算结果完成及时通知监听者呢?

在 Java 8 中, 新增加了一个包含 50 个方法左右的类: CompletableFuture, 提供了非常强大的Future 的扩展功能, 可以帮助我们简化异步编程的复杂性, 提供了函数式编程的能力, 可以通过回调的方式处理计算结果, 并且提供了转换和组合 CompletableFuture 的方法。CompletableFuture 类实现了 Future 接口, 所以你还是可以像以前一样通过get方法阻塞或者轮询的方式获得结果, 但是这种方式不推荐使用。

CompletableFuture 和 FutureTask 同属于 Future 接口的实现类, 都可以获取线程的执行结果。

① CompletableFuture 提供了四个静态方法来创建一个异步操作 :

runXxxx 都是没有返回结果的, supplyXxx 都是可以获取返回结果的,可以传入自定义的线程池, 否则就用默认的线程池 。

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {/*** runAsync(Runnable runnable)*/CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {System.out.println("当前线程:" + Thread.currentThread().getId());int i = 10 / 2;System.out.println("运行结果:" + i);}, executor);}
}

② 计算完成时回调方法 :

whenComplete 可以处理正常和异常的计算结果, exceptionally 处理异常情况。
whenComplete 和 whenCompleteAsync 的区别:
whenComplete: 是执行当前任务的线程执行继续执行 whenComplete 的任务。
whenCompleteAsync: 是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {/***  方法完成后的感知*/CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {System.out.println("当前线程:" + Thread.currentThread().getId());int i = 10 / 0;System.out.println("运行结果:" + i);return i;}, executor).whenComplete((res,excption)->{//虽然能得到异常信息,但是没法修改返回数据。System.out.println("异步任务成功完成了...结果是:"+res+";异常是:"+excption);}).exceptionally(throwable -> {//可以感知异常,同时返回默认值return 10;});}
}

③ handle 方法:

和 complete 一样, 可对结果做最后的处理(可处理异常) , 可改变返回值

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {/*** 方法执行完成后的处理*/CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {System.out.println("当前线程:" + Thread.currentThread().getId());int i = 10 / 4;System.out.println("运行结果:" + i);return i;}, executor).handle((res, thread) -> {if (res != null) {return res * 2;}if (thread != null) {return 0;}return 0;});}
}

④ 线程串行化方法 :

thenApply 方法: 当一个线程依赖另一个线程时, 获取上一个任务返回的结果, 并返回当前任务的返回值。
thenAccept 方法: 消费处理结果。 接收任务的处理结果, 并消费处理, 无返回结果。
thenRun 方法:只要上面的任务执行完成就开始执行 thenRun, 只是处理完任务后, 执行thenRun 的后续操作

带有 Async 默认是异步执行的

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {/*** 线程串行化* 1)、thenRun:不能获取到上一步的执行结果,无返回值*  .thenRunAsync(() -> {*             System.out.println("任务2启动了...");*         }, executor);* 2)、thenAcceptAsync;能接受上一步结果,但是无返回值* 3)、thenApplyAsync:;能接受上一步结果,有返回值*/CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {System.out.println("当前线程:" + Thread.currentThread().getId());int i = 10 / 4;System.out.println("运行结果:" + i);return i;}, executor).thenApplyAsync(res -> {System.out.println("任务2启动了..." + res);return "Hello " + res;}, executor);}
}

⑤ 两任务组合 - 都要完成 :

两个任务必须都完成, 触发该任务。

thenCombine: 组合两个 future, 获取两个 future 的返回结果, 并返回当前任务的返回值
thenAcceptBoth: 组合两个 future, 获取两个 future 任务的返回结果, 然后处理任务, 没有返回值。
runAfterBoth: 组合两个 future, 不需要获取 future 的结果, 只需两个 future 处理完任务后,处理该任务。

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {/*** 两个都完成*/CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {System.out.println("任务1线程:" + Thread.currentThread().getId());int i = 10 / 4;System.out.println("任务1结束:" );return i;}, executor);CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {System.out.println("任务2线程:" + Thread.currentThread().getId());try {Thread.sleep(3000);System.out.println("任务2结束:" );} catch (InterruptedException e) {e.printStackTrace();}return "Hello";}, executor);future01.runAfterBothAsync(future02,()->{System.out.println("任务3开始...");},executor);future01.thenAcceptBothAsync(future02,(f1,f2)->{System.out.println("任务3开始...之前的结果:"+f1+"--》"+f2);},executor);CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {return f1 + ":" + f2 + " -> Haha";}, executor);}
}

⑥ 两任务组合 - 一个完成 :

当两个任务中, 任意一个 future 任务完成的时候, 执行任务

applyToEither: 两个任务有一个执行完成, 获取它的返回值, 处理任务并有新的返回值。
acceptEither: 两个任务有一个执行完成, 获取它的返回值, 处理任务, 没有新的返回值。
runAfterEither: 两个任务有一个执行完成, 不需要获取 future 的结果, 处理任务, 也没有返回值

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {/*** 两个任务,只要有一个完成,我们就执行任务3* runAfterEitherAsync:不感知结果,自己没有返回值* acceptEitherAsync:感知结果,自己没有返回值* applyToEitherAsync:感知结果,自己有返回值*/future01.runAfterEitherAsync(future02,()->{System.out.println("任务3开始...之前的结果:");},executor);future01.acceptEitherAsync(future02,(res)->{System.out.println("任务3开始...之前的结果:"+res);},executor);CompletableFuture<String> future = future01.applyToEitherAsync(future02, res -> {System.out.println("任务3开始...之前的结果:" + res);return res.toString() + "->哈哈";}, executor);}
}

⑦ 多任务组合 :

allOf: 等待所有任务完成
anyOf: 只要有一个任务完成

public class ThreadTest {public static ExecutorService executor = Executors.newFixedThreadPool(10);public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {System.out.println("查询商品的图片信息");return "hello.jpg";},executor);CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {System.out.println("查询商品的属性");return "黑色+256G";},executor);CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(3000);System.out.println("查询商品介绍");} catch (InterruptedException e) {e.printStackTrace();}return "华为";},executor);CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);// 等待三个任务执行完成,即等待所有结果完成allOf.get();CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);// 只要有一个任务完成,就会结束,得到的就是执行成功的任务结果anyOf.get();System.out.println("main....end...."+anyOf.get());}
}

7.3 商品详情页

① 添加域名:

② 配置nginx :

③ 配置网关 :

- id: gulimall_host_routeuri: lb://gulimall-productpredicates:- Host=gulimall.com,item.gulimall.com

④ 将项目提供的前端页面放到gulimall-product服务中的templates文件下

⑤ 将项目提供的静态资源放到nginx下的static文件夹下的item文件夹中

⑥ 商品详情模型抽取 :

@Controller
public class ItemController {@AutowiredSkuInfoService skuInfoService;@GetMapping("/{skuId}.html")public String skuItem(@PathVariable("skuId") Long skuId){skuInfoService.item(skuId);return "item";}
}

sku的图片信息:

获取spu的介绍 :

获取spu的规格参数信息:

获取spu的销售属性组合信息:

@Override
public SkuItemVo item(Long skuId) {SkuItemVo skuItemVo = new SkuItemVo();//1、sku基本信息获取  pms_sku_infoSkuInfoEntity skuInfoEntity = getById(skuId);skuItemVo.setInfo(skuInfoEntity);Long spuId = skuInfoEntity.getSpuId();Long catalogId = skuInfoEntity.getCatalogId();//2、sku的图片信息 pms_sku_imagesList<SkuImagesEntity> skuImagesEntities = skuImagesService.getImagesBySkuId(skuId);skuItemVo.setImages(skuImagesEntities);//3、获取spu的销售属性组合信息List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);skuItemVo.setSaleAttr(saleAttrVos);//4、获取spu的介绍 pms_spu_info_descSpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(spuId);skuItemVo.setDesp(spuInfoDescEntity);//5、获取spu的规格参数信息// 获取当前商品的所有属性分组以及属性值List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);skuItemVo.setGroupAttrs(attrGroupVos);return skuItemVo;
}

⑦ 页面term.html处理:

7.4 异步编排优化商品详情

在上面商品详情页进行查询的时候,都是顺序查询,即同步方式,可以使用异步编排进行优化。

① 自定义线程池:

绑定配置文件中的属性:

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {private Integer coreSize;private Integer maxSize;private Integer keepAliveTime;
}

application.properties文件:

gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
@Configuration
public class MyThreadConfig {//创建线程池@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties properties){return new ThreadPoolExecutor(properties.getCoreSize(),properties.getMaxSize(),properties.getKeepAliveTime(),TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}
}

② 优化商品详情逻辑,并使用自定义的线程池:

@Autowired
private ThreadPoolExecutor executor;@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {SkuItemVo skuItemVo = new SkuItemVo();CompletableFuture<SkuInfoEntity> skuInfoFuture = CompletableFuture.supplyAsync(() -> {//1、sku基本信息获取  pms_sku_infoSkuInfoEntity skuInfoEntity = getById(skuId);skuItemVo.setInfo(skuInfoEntity);return skuInfoEntity;}, executor);CompletableFuture<Void> saleAttrFuture = skuInfoFuture.thenAcceptAsync((res) -> {//3、获取spu的销售属性组合信息List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());skuItemVo.setSaleAttr(saleAttrVos);}, executor);CompletableFuture<Void> spuInfoDescFuture = skuInfoFuture.thenAcceptAsync((res) -> {//4、获取spu的介绍 pms_spu_info_descSpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());skuItemVo.setDesp(spuInfoDescEntity);}, executor);CompletableFuture<Void> attrGroupFuture = skuInfoFuture.thenAcceptAsync((res) -> {//5、获取spu的规格参数信息// 获取当前商品的所有属性分组以及属性值List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());skuItemVo.setGroupAttrs(attrGroupVos);}, executor);CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {//2、sku的图片信息 pms_sku_imagesList<SkuImagesEntity> skuImagesEntities = skuImagesService.getImagesBySkuId(skuId);skuItemVo.setImages(skuImagesEntities);}, executor);//等到所有任务都完成再返回CompletableFuture.allOf(saleAttrFuture,spuInfoDescFuture,attrGroupFuture,imageFuture).get();return skuItemVo;
}

实战 - 谷粒商城项目:高级上篇First相关推荐

  1. 实战 - 谷粒商城项目:基础篇First

    文章目录 SpringCloud商城- 基础篇 1. 环境搭建 1.1 centos7安装docker 1.2 docker安装MySQL5.7 1.3 docker安装Redis 1.4 环境安装配 ...

  2. 谷粒商城-分布式高级篇[商城业务-检索服务]

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  3. 谷粒商城项目笔记总结(1/2)

    文章目录 商城项目 - 基础篇 1. 环境搭建 2. MyBatis-plus 引入MyBatis-plus的步骤 3. SpringCloud Alibaba Nacos注册中心 Nacos配置中心 ...

  4. M5(项目)-01-尚硅谷谷粒商城项目分布式基础篇开发文档

    M5(项目)-01-尚硅谷谷粒商城项目分布式基础篇开发文档 分布式基础篇 一.环境搭建 各种开发软件的安装 虚拟机: docker,mysql,redis 主机: Maven, idea(后端),Vs ...

  5. 谷粒商城分布式高级篇(中)

    谷粒商城分布式基础篇 谷粒商城分布式高级篇(上) 谷粒商城分布式高级篇(中) 谷粒商城分布式高级篇(下) 文章目录 商城业务 异步 异步复习 线程池详解 CompletableFuture Compl ...

  6. 谷粒商城-分布式高级篇【业务编写】

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  7. 谷粒商城--认证中心--高级篇笔记八

    谷粒商城–认证中心–高级篇笔记八 1. 环境搭建 1.1 新建模块gulimall-auth-server 1.2 pom文件 上面没选好直接复制下面的pom文件,记得排除gulimall-commo ...

  8. 谷粒商城-分布式高级篇[商城业务-秒杀服务]

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  9. 谷粒商城项目工具准备

    谷粒商城项目新手 返回导航页 1.前置知识-安装虚拟机 虚拟机问题 虚拟机安装让必要软件 docker安装其余用到的软件 项目架构图 项目划分图 创建项目 创建数据库 人人开源项目 人人开源前端 代码 ...

最新文章

  1. ajax跨界表单,ajax使用jsonp解决跨域问题
  2. Android EditText+ListPopupWindow实现可编辑的下拉列表
  3. 009_Spring Data JPA一对一关系
  4. 亚马逊 AWS 免费云服务操作流程
  5. 浏览器无法打开摄像头
  6. 电脑技巧:如何更改Win10桌面文件路径,轻松给系统盘瘦身!
  7. MindSpore小笔记
  8. android自定义View学习(一)----创建一个视图类
  9. 目录行距怎么设置_硕士论文格式设置方法
  10. 数据库备份,及清理备份计划
  11. 设置对话框的小三角方法
  12. 基于 opencv图像去噪
  13. 区块链 xuperchain 同步模式 纯异步模式 异步阻塞模式 怎么启动
  14. iOS ASI--文件下载
  15. 【优化算法】混合增强灰狼优化布谷鸟搜索算法(AGWOCS)【含Matlab源码 1331期】
  16. window版本下载安装kafka和ZooKeeper并调试
  17. mybatis批量导入
  18. JavaScript 打开新页面
  19. QNX系统配置NFS实战
  20. 1 Apache启动失败,请检查相关配置 √MySQL5 1已启动 解决方案

热门文章

  1. Python字典的操作小技巧——索引、增添、删除、修改与取键和值
  2. appcan案例书目录
  3. iOS 13 安装率贼多,你为什么升级它?
  4. Git 安装与卸载 gitk安装与优化
  5. [POJ2891] Strange Way to Express Integers
  6. spring cloud微服务分布式云架构(三)-服务消费者(Feign)
  7. 铁大课表 测试分析报告
  8. WebGIS第十二课:智慧校园项目(4)
  9. (原創) 為什麼要學C/C++? (C/C++)
  10. pet缩聚流程图_PET生产工艺流程.ppt