当前位置:   article > 正文

谷粒商城-个人笔记(高级篇二)_field redissonclient in com.atguigu.gulimall.produ

field redissonclient in com.atguigu.gulimall.product.service.impl.categoryse

目录

二、商城业务-首页

1、整合thymeleaf渲染首页

1)、在“gulimall-product”项目中导入前端代码:

2)、渲染一级分类数据&&整合dev-tools

3)、渲染二级三级分类数据

2、搭建域名访问环境

1)、商城业务-nginx-搭建域名访问环境一(反向代理配置)

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

3、性能压测与优化

3.1、压力测试

1)、基本介绍

2)、Apache JMeter安装使用

3)、性能监控-jvisualvm使用

3.2、性能优化

1)、中间件对性能的影响

2)、简单优化吞吐量测试

3)、Nginx动静分离

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

5)、优化三级分类数据获取

三、缓存

1、缓存使用

1)、缓存的演化

2)、分布式缓存

2、整合redis测试

3、改造三级分类业务

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

5、高并发下缓存失效问题--缓存击穿、穿透、雪崩

1)、加锁解决缓存击穿问题

2)、锁时序问题

6、本地锁在分布式下的问题

7、分布式锁原理与使用

1) 、本地缓存面临问题

2)、分布式锁

3) 、分布式锁的演进

8、Redisson

1)、Redisson-lock锁测试

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

4)、读写锁测试

6)、信号量测试

7)、缓存一致性解决

9、SpringCache-简介

1)、简洁

2)、基础概念

3)、SpringCache-整合&体验@Cacheable

4)、@Cacheable细节设置

5)、自定义缓存配置

6)、@CacheEvict

7)、SpringCache-原理与不足

四、检索

1. 检索条件分析

2. DSL分析

3. 检索代码编写

4. 页面效果

五、异步

1、线程池

1)、七大参数

2)、工作顺序

3)、常见的4种线程池

4)、使用线程池的好处

2、CompletableFuture组合式异步编程

1)、创建异步对象

2)、计算结果完成时的回调方法

3)、handle 方法

4)、线程串行化

5)、两任务组合-都要完成

6)、两任务组合-只要有一个任务完成就执行第三个

7)、多任务组合

六、商品详情

1、搭建好域名跳转环境

2、模型抽取

3、 封装商品属性

3、页面渲染

4、页面的sku切换

5、使用异步编排


二、商城业务-首页


1、整合thymeleaf渲染首页

1)、在“gulimall-product”项目中导入前端代码:

项目在发布的时候,将静态资源放到nginx中,实现动静分离 

引入"thymeleaf"依赖:

前端使用了thymeleaf开发,因此要导入该依赖,并且为了改动页面实时生效导入devtools

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>

 将静态资源放入到static文件夹下,而将index.html放入到templates文件夹下:

在“application.yml”文件中设置thymeleaf,关闭thymeleaf缓存,路径为:spring.thymeleaf

  1. spring:
  2. thymeleaf:
  3. cache: false

 同时将“controller”修改为app,以后它都是被移动APP所访问的地方。

创建web文件夹:

启动“gulimall-product”服务,根据路径就可以直接访问静态资源

在静态资源目录static下的资源,可以直接访问,如:http://localhost:10000/index/css/GL.css

SpringBoot在访问项目的时候,会默认找到index文件夹下的文件。

这些规则是配置在“ResourceProperties”文件中指定的:

  1. private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
  2. "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
  3. 关于欢迎页,它是在静态文件夹中,寻找index.html页面的:
  4. org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
  1. private Optional<Resource> getWelcomePage() {
  2. String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
  3. return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
  4. }
  5. private Resource getIndexHtml(String location) {
  6. return this.resourceLoader.getResource(location + "index.html");
  7. }

2)、渲染一级分类数据&&整合dev-tools

 现在想要实现的效果是访问http://localhost:10000/index.html能访问,另外当

在thymeleaf中,默认的访问的前缀和后缀

  1. //前缀
  2. public static final String DEFAULT_PREFIX = "classpath:/templates/";
  3. //后缀
  4. public static final String DEFAULT_SUFFIX = ".html";

当controller中返回的是一个视图地址,它就会使视图解析器进行拼串。

查询出所有的一级分类:

 2.1)、渲染一级分类菜单

由于访问首页时就要加载一级目录,所以我们需要在加载首页时获取该数据

修改“com.atguigu.gulimall.product.web.IndexController”类,修改如下:

  1. @GetMapping({"/", "index.html"})
  2. public String getIndex(Model model) {
  3. //获取所有的一级分类
  4. List<CategoryEntity> catagories = categoryService.getLevel1Catagories();
  5. model.addAttribute("catagories", catagories);
  6. return "index";
  7. }

 修改“com.atguigu.gulimall.product.service.CategoryService”类,修改如下:

List<CategoryEntity> getLevel1Category();

  修改“com.atguigu.gulimall.product.service.impl.CategoryServiceImpl”类,修改如下:

  1. @Override
  2. public List<CategoryEntity> getLevel1Category() {
  3. List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
  4. return categoryEntities;
  5. }

2.2)、dev-tools实现不重启服务实时生效

1、添加devtools依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-devtools</artifactId>
  4. <optional>true</optional>
  5. </dependency>

2、编译页面
      执行“ctrl+shift+F9”重新编译页面或“ctrl+F9”重新编译整个项目。

3、代码配置方面,还是建议重启服务

      通过引入该devtools,能够实现在IDEA中修改了页面后,无效重启整个服务就可以实现刷新页面,但是在修改页面后,需要执行“ctrl+shift+F9”重新编译页面或“ctrl+F9”重新编译整个项目。

thymeleaf下载地址:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf

渲染页面index.html页面:

引入Thymeleaf

<html lang="en" xmlns:th="http://www.thymeleaf.org">

页面遍历菜单数据

  1. <div class="header_main_left">
  2. <ul>
  3. <li th:each="category : ${categorys}">
  4. <a href="#" class="header_main_left_a" ctg-data="3" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器111</b></a>
  5. </li>
  6. </ul>
  7. </div>

3)、渲染二级三级分类数据

首页加载的数据默认来自于静态资源的“index/catalog.json”,现在需要让它重数据库实时读取

 添加“com.atguigu.gulimall.product.web.IndexController”类,代码如下:

  1. @ResponseBody
  2. @GetMapping("/index/catelog.json")
  3. public Map<String, List<Catelog2Vo>> getCatelogJson(){
  4. Map<String, List<Catelog2Vo>> catelogJson = categoryService.getCatelogJson();
  5. return catelogJson;
  6. }

添加“com.atguigu.gulimall.product.vo.Catelog2Vo”类,代码如下:

  1. //二级分类
  2. @Data
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class Catelog2Vo {
  6. private String catelog1Id; //一级父分类id
  7. private List<Catalog3Vo> catalog3List; //三级子分类
  8. private String id;
  9. private String name;
  10. //三级分类
  11. @Data
  12. @AllArgsConstructor
  13. @NoArgsConstructor
  14. public static class Catalog3Vo{
  15. private String catelog2Id; //父分类,2级分类id
  16. private String id;
  17. private String name;
  18. }
  19. }

 修改“com.atguigu.gulimall.product.service.impl.CategoryServiceImpl”类,代码如下:

  1. /**
  2. * 逻辑是
  3. * (1)根据一级分类,找到对应的二级分类
  4. * (2)将得到的二级分类,封装到Catelog2Vo中
  5. * (3)根据二级分类,得到对应的三级分类
  6. * (3)将三级分类封装到Catalog3List
  7. * @return
  8. */
  9. @Override
  10. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  11. //查出所有1级分类
  12. List<CategoryEntity> level1Category = getLevel1Category();
  13. //2、封装数据
  14. Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
  15. //1、每一个的一级分类,查到这个一级分类的二级分类
  16. List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
  17. //2、封装上面的结果
  18. List<Catelog2Vo> catelog2Vos = null;
  19. if (categoryEntities != null) {
  20. catelog2Vos = categoryEntities.stream().map(l2 -> {
  21. Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
  22. //1、找当前二级分类的三级分类封装vo
  23. List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
  24. if (level3Catelog != null){
  25. List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
  26. //2、封装成指定格式
  27. Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
  28. return catelog3Vo;
  29. }).collect(Collectors.toList());
  30. catelog2Vo.setCatelog3List(Collections.singletonList(collect));
  31. }
  32. return catelog2Vo;
  33. }).collect(Collectors.toList());
  34. }
  35. return catelog2Vos;
  36. }));
  37. return parent_cid;
  38. }

2、搭建域名访问环境

1)、商城业务-nginx-搭建域名访问环境一(反向代理配置)

Nginx+windows搭建域名访问环境

正向代理和反向代理

修改本地的C:\Windows\System32\drivers\etc\hosts文件,添加域名映射规则:

先把只读模式去掉,才可以编辑

192.168.43.125 gulimall.com

192.168.43.125 为Nginx所在的设备

测试Nginx的访问:http://gulimall.com/

 

关于Nginx的配置文件:

  1. user nginx;
  2. worker_processes 1;
  3. error_log /var/log/nginx/error.log warn;
  4. pid /var/run/nginx.pid;
  5. events {
  6. worker_connections 1024;
  7. }
  8. http {
  9. include /etc/nginx/mime.types;
  10. default_type application/octet-stream;
  11. log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  12. '$status $body_bytes_sent "$http_referer" '
  13. '"$http_user_agent" "$http_x_forwarded_for"';
  14. access_log /var/log/nginx/access.log main;
  15. sendfile on;
  16. #tcp_nopush on;
  17. keepalive_timeout 65;
  18. #gzip on;
  19. include /etc/nginx/conf.d/*.conf;
  20. }

注意这里的“include /etc/nginx/conf.d/*.conf;”,它是将“/etc/nginx/conf.d/*.conf”目录下的所有配置文件包含到nginx.conf文件中。下面是该文件的内容: 

 

  1. server {
  2. listen 80;
  3. server_name localhost;
  4. #charset koi8-r;
  5. #access_log /var/log/nginx/host.access.log main;
  6. location / {
  7. root /usr/share/nginx/html;
  8. index index.html index.htm;
  9. }
  10. #error_page 404 /404.html;
  11. # redirect server error pages to the static page /50x.html
  12. #
  13. error_page 500 502 503 504 /50x.html;
  14. location = /50x.html {
  15. root /usr/share/nginx/html;
  16. }
  17. # proxy the PHP scripts to Apache listening on 127.0.0.1:80
  18. #
  19. #location ~ \.php$ {
  20. # proxy_pass http://127.0.0.1;
  21. #}
  22. # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  23. #
  24. #location ~ \.php$ {
  25. # root html;
  26. # fastcgi_pass 127.0.0.1:9000;
  27. # fastcgi_index index.php;
  28. # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
  29. # include fastcgi_params;
  30. #}
  31. # deny access to .htaccess files, if Apache's document root
  32. # concurs with nginx's one
  33. #
  34. #location ~ /\.ht {
  35. # deny all;
  36. #}
  37. }

在Nginx上配置代理,使得所有到gulimall.com的请求,都转到gulimall-product服务。

现在需要明确部署情况,我们的nginx部署在172.20.10.3(虚拟机ip)上,而且是以docker容器的方式部署的,部署时将本机的/mydata/nginx/conf/挂载到了nginx容器的“/etc/nginx ”目录,gulimall-product服务部署在172.20.10.2(主机ip)上。

 在172.20.10.3上创建“gulimall.conf”文件:

 

 

 修改配置文件完毕后,重启nginx。

访问:http://gulimall.com/

整个的数据流是这样的:浏览器请求gulimall.com,在本机被解析为172.20.10.3(虚拟机ip),172.20.10.3的80端口接收到请求后,解析请求头求得host,在然后使用在“gulimall.conf”中配置的规则,将请求转到“http://172.20.10.2:10002”(主机ip),然后该服务响应请求,返回响应结果。

但是这样做还是有些不够完美,“gulimall-product”可能部署在多台服务器上,通常请求都会被负载到不同的服务器上,这里我们直接指定一台设备的方式,显然不合适。

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

2.1)关于Nginx的负载均衡

使用Nginx作为Http负载均衡器

http://nginx.org/en/docs/http/load_balancing.html

默认的负载均衡配置:

  1. http {
  2. upstream myapp1 {
  3. server srv1.example.com;
  4. server srv2.example.com;
  5. server srv3.example.com;
  6. }
  7. server {
  8. listen 80;
  9. location / {
  10. proxy_pass http://myapp1;
  11. }
  12. }
  13. }

In the example above, there are 3 instances of the same application running on srv1-srv3. When the load balancing method is not specifically configured, it defaults to round-robin. All requests are proxied to the server group myapp1, and nginx applies HTTP load balancing to distribute the requests.

在上面的例子中,同一个应用程序有3个实例在srv1-srv3上运行。如果没有特别配置负载平衡方法,则默认为 round-robin。所有请求都代理到服务器组myapp1, nginx应用HTTP负载平衡来分发请求。

Reverse proxy implementation in nginx includes load balancing for HTTP, HTTPS, FastCGI, uwsgi, SCGI, memcached, and gRPC.

nginx中的反向代理实现包括HTTP、HTTPS、FastCGI、uwsgi、SCGI、memcached和gRPC的负载均衡。

To configure load balancing for HTTPS instead of HTTP, just use “https” as the protocol.

要为HTTPS而不是HTTP配置负载平衡,只需使用“HTTPS”作为协议。

When setting up load balancing for FastCGI, uwsgi, SCGI, memcached, or gRPC, use fastcgi_passuwsgi_passscgi_passmemcached_pass, and grpc_pass directives respectively.

在为FastCGI、uwsgi、SCGI、memcached或gRPC设置负载均衡时,分别使用fastcgi_pass、uwsgi_pass、scgi_pass、memcached_pass和grpc_pass指令。

2)配置负载均衡:

1)修改“/mydata/nginx/conf/nginx.conf”,添加如下内容

注意:这里的88端口为“gulimall-gateway”服务的监听端口,也即访问“gulimall-product”服务通过该网关进行路由。

(2)修改“/mydata/nginx/conf/conf.d/gulimall.conf”文件 

(3)在“gulimall-gateway”添加路由规则: 

  1. - id: gulimall_host_route
  2. uri: lb://gulimall-product
  3. predicates:
  4. - Host=**.gulimall.com

注意:这个路由规则,一定要放置到最后,否则会优先进行Host匹配,导致其他路由规则失效。

(4)配置完成后,重启Nginx,再次访问

docker restart nginx

再次访问的时候,返回404状态码

但是通过niginx请求“gulimall-product”服务的其他controller却能够顺利访问。

http://gulimall.com/api/product/category/list/tree

原因分析:Nginx代理给网关的时候,会丢失请求头的很多信息,如HOST信息,Cookie等。

解决方法:需要修改Nginx的路径映射规则,加上“ proxy_set_header Host       $host;”

http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header

However, if this field is not present in a client request header then nothing will be passed. In such a case it is better to use the $host variable - its value equals the server name in the “Host” request header field or the primary server name if this field is not present:

proxy_set_header Host $host;

修改“gulimall.conf”文件

 

 

重启Nginx容器,再次访问Success: 

docker restart nginx

 

3、性能压测与优化

3.1、压力测试

1)、基本介绍

2)、Apache JMeter安装使用

jmeter压力测试工具

查看响应结果

汇总图:

JMeter在windows下地址占用bug解决 

 

 堆内存与垃圾回收

3)、性能监控-jvisualvm使用

https://visualvm.github.io/uc/8u131/updates.xml.gz

实例:如下面我们想要可视化GC的过程,可以安装这个插件

安装完成后,重启jvisualvm

然后连接我们的gulimall-product服务进程

在GC选项卡能够看到GC发生过程:

3.2、性能优化

1)、中间件对性能的影响

(1)启动Jmeter

(2)创建线程组

3)添加一个HTTP请求

这里我们测试Nginx的性能,先看Nginx是否能正常访问:

添加HTTP请求:

填写请求指标:

(4)添加“查看结果树”,“汇总报告”和“聚合报告”

(5)在nginx所在的机器172.20.10.3上,执行docker stats

通过docker stats命令,能够看到nginx容器的内存,cpu的占用情况

(6)启动jmeter

大概50秒左右,停止jmeter

(7)停止jmeter,查看报告

查看结果数,部分请求因socket关闭发送失败

查看汇总报告

查看聚合报告

2)、简单优化吞吐量测试

3)、Nginx动静分离

Nginx动静分离,也就是将访问静态资源和动态资源区分开,将静态资源放置到Nginx上

通过前面的吞吐量测试,我们能够看到访问静态资源对于“gulimall-product”服务造成的访问压力,在生产中我们可以考虑将这部分静态资源部署到Nginx上来优化。

在“gulimall-product”微服务中,原来的所有静态资源都放置到了resources/static,在做动静分离的时候,我们可以考虑将这些资源迁移到Nginx上。

迁移方法:

(1)找到Nginx存放静态资源的位置。这里我们使用的是Nginx容器,将本地“/mydata/nginx/html”映射到远程的“/usr/share/nginx/html”,所以应该将静态资源放置到该目录下。

(2)在/mydata/nginx/html目录下创建static文件夹: mkdir static

(3)使用SecureFX传送工具(换了一个连接虚拟机的客户端工具),将“gulimall-product/src/main/resources/static ”下的“index”静态资源放置到Nginx路径中。

(4)ctrl+R  修改index.html“”页面,将所有的对于静态资源的访问加上static路径

(5)修改Nginx的“gulimall.conf”文件,这里指定所有static的访问,都是到“/usr/share/nginx/html”路径下寻找

(6)、重启Nginx

docker restart nginx

(7)测试效果

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

再次进行压测:

执行压测

查看测试报告

同时通过jvisualvm查看GC过程

能够看到在老年代中存在耗时的GC过程,并且随着并发量增加,已经开始出现OutOfMemoryError异常了

并且服务也会down掉:

现在我们可以通过增加服务占用的内存大小,来控制减少Full GC发生的频率

5)、优化三级分类数据获取

来看我们之前编写的获取三级分类的业务逻辑:

  1. @Override
  2. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  3. //1.查出所有一级分类
  4. List<CategoryEntity> level1Categories = getLevel1Categories();
  5. Map<String, List<Catelog2Vo>> parent_cid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
  6. //2. 根据一级分类的id查找到对应的二级分类
  7. List<CategoryEntity> level2Categories = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", level1.getCatId()));
  8. //3. 根据二级分类,查找到对应的三级分类
  9. List<Catelog2Vo> catelog2Vos =null;
  10. if(null != level2Categories || level2Categories.size() > 0){
  11. catelog2Vos = level2Categories.stream().map(level2 -> {
  12. //得到对应的三级分类
  13. List<CategoryEntity> level3Categories = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", level2.getCatId()));
  14. //封装到Catalog3List
  15. List<Catalog3List> catalog3Lists = null;
  16. if (null != level3Categories) {
  17. catalog3Lists = level3Categories.stream().map(level3 -> {
  18. Catalog3List catalog3List = new Catalog3List(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
  19. return catalog3List;
  20. }).collect(Collectors.toList());
  21. }
  22. return new Catelog2Vo(level1.getCatId().toString(), catalog3Lists, level2.getCatId().toString(), level2.getName());
  23. }).collect(Collectors.toList());
  24. }
  25. return catelog2Vos;
  26. }));
  27. return parent_cid;
  28. }

(1)先从数据库中查询所有的一级分类

(2)根据一级分类的ID到数据库中找到对应的二级分类

(3)根据二级分类的ID,到数据库中寻找到对应的三级分类

在这个逻辑实现中,每一个一级分类的ID,至少要经过3次数据库查询才能得到对应的三级分类,所以在大数据量的情况下,频繁的操作数据库,性能比较低。

我们可以考虑将这些分类数据一次性的load到内存中,在内存中来操作这些数据,而不是频繁的进行数据库交互操作,下面是优化后的查询

  1. @Override
  2. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  3. //一次性查询出所有的分类数据,减少对于数据库的访问次数,后面的数据操作并不是到数据库中查询,而是直接从这个集合中获取,
  4. // 由于分类信息的数据量并不大,所以这种方式是可行的
  5. List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
  6. //1.查出所有一级分类
  7. List<CategoryEntity> level1Categories = getParentCid(categoryEntities,0L);
  8. Map<String, List<Catelog2Vo>> parent_cid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
  9. //2. 根据一级分类的id查找到对应的二级分类
  10. List<CategoryEntity> level2Categories = getParentCid(categoryEntities,level1.getCatId());
  11. //3. 根据二级分类,查找到对应的三级分类
  12. List<Catelog2Vo> catelog2Vos =null;
  13. if(null != level2Categories || level2Categories.size() > 0){
  14. catelog2Vos = level2Categories.stream().map(level2 -> {
  15. //得到对应的三级分类
  16. List<CategoryEntity> level3Categories = getParentCid(categoryEntities,level2.getCatId());
  17. //封装到Catalog3List
  18. List<Catalog3List> catalog3Lists = null;
  19. if (null != level3Categories) {
  20. catalog3Lists = level3Categories.stream().map(level3 -> {
  21. Catalog3List catalog3List = new Catalog3List(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
  22. return catalog3List;
  23. }).collect(Collectors.toList());
  24. }
  25. return new Catelog2Vo(level1.getCatId().toString(), catalog3Lists, level2.getCatId().toString(), level2.getName());
  26. }).collect(Collectors.toList());
  27. }
  28. return catelog2Vos;
  29. }));
  30. return parent_cid;
  31. }
  32. /**
  33. * 在selectList中找到parentId等于传入的parentCid的所有分类数据
  34. * @param selectList
  35. * @param parentCid
  36. * @return
  37. */
  38. private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList,Long parentCid) {
  39. List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
  40. return collect;
  41. }

整体的逻辑就是每次根据分类ID,找到所有子分类数据的时候,不再从数据库中查找,而是在内存中查询。

我们可以通过Jmeter来测试一下优化后的查询效率

请求参数设置如下:

下面是测试的比对:

三、缓存

1、缓存使用

对于复杂的业务,已经不能够通过代码层面的优化和数据库层面的优化,来达到增加吞吐量的目的。这就想要使用到缓存。

1)、缓存的演化

本地缓存

分布式本地缓存

分布式缓存-本地模式在分布式下的问题

这种情况下,每个服务维持一个缓存,所带来的问题:

(1)缓存不共享

在这种情况下,每个服务都有一个缓存,但是这个缓存并不共享,水平上当调度到另外一个台设备上的时候,可能它的服务中并不存在这个缓存,因此需要重新查询。

(2)缓存一致性问题

在一台设备上的缓存更新后,其他设备上的缓存可能还未更新,这样当从其他设备上获取数据的时候,得到的可能就是未给更新的数据。

2)、分布式缓存

在这种下,一个服务的不同副本共享同一个缓存空间,缓存放置到缓存中间件中,这个缓存中间件可以是redis等,而且缓存中间件也是可以水平或纵向扩展的,如Redis可以使用redis集群。它打破了缓存容量的限制,能够做到高可用,高性能。

2、整合redis测试

在“gulimall-product”项目中引入redis

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

Reids是通过“RedisAutoConfiguration”来完成的,它将所有的配置信息,都放置到了“RedisProperties”中。

配置redis主机地址

  1. spring:
  2. redis:
  3. host: 172.20.10.3
  4. port: 6379

这里我们的Redis服务器为172.20.10.3,部署的是Redis容器。

在“”类中,提供了两种操作Redis的方式:

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnClass(RedisOperations.class)
  3. @EnableConfigurationProperties(RedisProperties.class)
  4. @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
  5. public class RedisAutoConfiguration {
  6. @Bean
  7. @ConditionalOnMissingBean(name = "redisTemplate")
  8. //将保存进入Redis的键值都是Object
  9. public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
  10. throws UnknownHostException {
  11. RedisTemplate<Object, Object> template = new RedisTemplate<>();
  12. template.setConnectionFactory(redisConnectionFactory);
  13. return template;
  14. }
  15. @Bean
  16. @ConditionalOnMissingBean
  17. //保存进Redis的数据,键值是(String,String)
  18. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
  19. throws UnknownHostException {
  20. StringRedisTemplate template = new StringRedisTemplate();
  21. template.setConnectionFactory(redisConnectionFactory);
  22. return template;
  23. }
  24. }

关于“StringRedisTemplate”

  1. public class StringRedisTemplate extends RedisTemplate<String, String> {
  2. /**
  3. * Constructs a new <code>StringRedisTemplate</code> instance. {@link #setConnectionFactory(RedisConnectionFactory)}
  4. * and {@link #afterPropertiesSet()} still need to be called.
  5. */
  6. public StringRedisTemplate() {
  7. setKeySerializer(RedisSerializer.string());//键序列化为String
  8. setValueSerializer(RedisSerializer.string());//key序列化为String
  9. setHashKeySerializer(RedisSerializer.string());
  10. setHashValueSerializer(RedisSerializer.string());
  11. }

综上:SpringBoot整合Redis的方式

(1)引入“spring-boot-starter-data-redis”

(2)简单配置Redis的host等信息

(3)使用SpringBoot自动配置好的"StringRedisTemplate"来操作redis。

测试:

引入StringRedisTemplate

  1. @Autowired
  2. StringRedisTemplate redisTemplate;

  1. @Test
  2. public void testStringRedisTemplate(){
  3. ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
  4. //保存
  5. ops.set("hello","world_"+ UUID.randomUUID().toString());
  6. //查询
  7. String hello = ops.get("hello");
  8. System.out.println("之前保存的数据是:"+hello);
  9. }

在Redis上查看结果

3、改造三级分类业务

先从缓存中获取分类三级分类数据,如果没有再从数据库中查询,并且将查询结果以JSON字符串的形式存放到Reids中的。

  1. @Override
  2. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  3. //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
  4. //1、加入缓存逻辑
  5. //JSON好处是跨语言,跨平台兼容。
  6. String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
  7. if (StringUtils.isEmpty(catalogJSON)){
  8. //2、缓存中没有,查询数据库
  9. Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
  10. //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
  11. String jsonString = JSON.toJSONString(catelogJsonFromDB);
  12. redisTemplate.opsForValue().set("catalogJSON",jsonString);
  13. return catelogJsonFromDB;
  14. }
  15. //转为我们指定的对象。
  16. Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
  17. return result;
  18. }
  1. private Map<String, List<Catelog2Vo>> getDataFromDB() {
  2. String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
  3. if (!StringUtils.isEmpty(catalogJSON)) {
  4. //如果缓存不为null直接缓存
  5. Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
  6. });
  7. return result;
  8. }
  9. System.out.println("查询了数据库。。。。。");
  10. List<CategoryEntity> selectList = baseMapper.selectList(null);
  11. //查出所有一级分类
  12. List<CategoryEntity> level1Category = getParent_cid(selectList, 0L);
  13. //2、封装数据
  14. Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
  15. //1、每一个的一级分类,查到这个一级分类的二级分类
  16. List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
  17. //2、封装上面的结果
  18. List<Catelog2Vo> catelog2Vos = null;
  19. if (categoryEntities != null) {
  20. catelog2Vos = categoryEntities.stream().map(l2 -> {
  21. Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
  22. //1、找当前二级分类的三级分类封装vo
  23. List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
  24. if (level3Catelog != null) {
  25. List<Catelog2Vo.Catalog3Vo> collect = level3Catelog.stream().map(l3 -> {
  26. //2、封装成指定格式
  27. Catelog2Vo.Catalog3Vo catelog3Vo = new Catelog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
  28. return catelog3Vo;
  29. }).collect(Collectors.toList());
  30. catelog2Vo.setCatalog3List(collect);
  31. }
  32. return catelog2Vo;
  33. }).collect(Collectors.toList());
  34. }
  35. return catelog2Vos;
  36. }));
  37. //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
  38. String jsonString = JSON.toJSONString(parent_cid);
  39. redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
  40. return parent_cid;
  41. }

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

启动“gulimall-product”和“gulimall-gateway”

执行压测:

测试报告:

再次访问页面出现了异常:

堆外内存溢出的原因,

解决方法:

修改“gulimall-product”的“pom.xml”文件,更换为Jedis

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. <exclusions>
  5. <exclusion>
  6. <groupId>io.lettuce</groupId>
  7. <artifactId>lettuce-core</artifactId>
  8. </exclusion>
  9. </exclusions>
  10. </dependency>
  11. <dependency>
  12. <groupId>redis.clients</groupId>
  13. <artifactId>jedis</artifactId>
  14. </dependency>

再次执行压测,没有出现Error:

使用Redis作为缓存后,吞吐量得到了很大的提升,响应时间也缩短了很多:

5、高并发下缓存失效问题--缓存击穿、穿透、雪崩

前面我们将查询三级分类数据的查询进行了优化,将查询结果放入到Redis中,当再次获取到相同数据的时候,直接从缓存中读取,没有则到数据库中查询,并将查询结果放入到Redis缓存中

但是在分布式系统中,这样还是会存在问题。

缓存穿透

缓存雪崩

缓存击穿

简单来说:缓存穿透是指查询一个永不存在的数据;缓存雪崩是值大面积key同时失效问题;缓存击穿是指高频key失效问题;

1)、加锁解决缓存击穿问题

将查询db的方法加锁,这样在同一时间只有一个方法能查询数据库,就能解决缓存击穿的问题了

现在针对于单体应用上的加锁,我们来测试一下它是否能够正常工作。

(1)删除“redis”中的“catelogJson”

 (2)修改三级分类的代码 

  1. @Override
  2. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  3. //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
  4. /**
  5. * 1、空结果缓存,解决缓存穿透
  6. * 2、设置过期时间(随机加值);解决缓存雪崩
  7. * 3、加锁,解决缓存击穿
  8. */
  9. //1、加入缓存逻辑
  10. //JSON好处是跨语言,跨平台兼容。
  11. String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
  12. if (StringUtils.isEmpty(catalogJSON)){
  13. //2、缓存中没有,查询数据库
  14. System.out.println("缓存不命中.....将要查询数据库...");
  15. Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
  16. //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
  17. String jsonString = JSON.toJSONString(catelogJsonFromDB);
  18. redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
  19. return catelogJsonFromDB;
  20. }
  21. System.out.println("缓存命中...直接返回...");
  22. //转为我们指定的对象。
  23. Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
  24. return result;
  25. }
  26. //从数据库查询并封装分类数据
  27. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
  28. //只要同一把锁就能锁住需要这个锁的所有线程
  29. //1、synchronized(this):SpringBoot所有的组件在容器中都是单例的
  30. // TODO 本地锁:synchronized,JUC(lock)。在分布式情况下想要锁住所有,必须使用分布式锁
  31. //使用DCL(双端检锁机制)来完成对于数据库的访问
  32. synchronized (this){
  33. //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
  34. String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
  35. if (!StringUtils.isEmpty(catalogJSON)){
  36. //如果缓存不为null直接缓存
  37. Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
  38. return result;
  39. }
  40. System.out.println("查询了数据库。。。。。");
  41. /**
  42. * 1、将数据库的多次查询变为1次
  43. */
  44. List<CategoryEntity> selectList = baseMapper.selectList(null);
  45. //查出所有一级分类
  46. List<CategoryEntity> level1Category = getParent_cid(selectList,0L);
  47. //2、封装数据
  48. Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
  49. //1、每一个的一级分类,查到这个一级分类的二级分类
  50. List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
  51. //2、封装上面的结果
  52. List<Catelog2Vo> catelog2Vos = null;
  53. if (categoryEntities != null) {
  54. catelog2Vos = categoryEntities.stream().map(l2 -> {
  55. Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
  56. //1、找当前二级分类的三级分类封装vo
  57. List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
  58. if (level3Catelog != null){
  59. List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
  60. //2、封装成指定格式
  61. Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
  62. return catelog3Vo;
  63. }).collect(Collectors.toList());
  64. catelog2Vo.setCatelog3List(Collections.singletonList(collect));
  65. }
  66. return catelog2Vo;
  67. }).collect(Collectors.toList());
  68. }
  69. return catelog2Vos;
  70. }));
  71. return parent_cid;
  72. }
  73. }

 在上述方法中,我们将业务逻辑中的确认缓存没有查数据库放到了锁里,但是最终控制台却打印了两次查询了数据库。这是因为在将结果放入缓存的这段时间里,有其他线程确认缓存没有,又再次查询了数据库,因此我们要将结果放入缓存也进行加锁

2)、锁时序问题

(1)删除“redis”中的“catelogJson”

优化代码逻辑后

  1. @Override
  2. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  3. //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
  4. /**
  5. * 1、空结果缓存,解决缓存穿透
  6. * 2、设置过期时间(随机加值);解决缓存雪崩
  7. * 3、加锁,解决缓存击穿
  8. */
  9. //1、加入缓存逻辑
  10. //JSON好处是跨语言,跨平台兼容。
  11. String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
  12. if (StringUtils.isEmpty(catalogJSON)){
  13. //2、缓存中没有,查询数据库
  14. System.out.println("缓存不命中.....将要查询数据库...");
  15. Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
  16. }
  17. System.out.println("缓存命中...直接返回...");
  18. //转为我们指定的对象。
  19. Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
  20. return result;
  21. }
  22. //从数据库查询并封装分类数据
  23. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
  24. //只要同一把锁就能锁住需要这个锁的所有线程
  25. //1、synchronized(this):SpringBoot所有的组件在容器中都是单例的
  26. // TODO 本地锁:synchronized,JUC(lock)。在分布式情况下想要锁住所有,必须使用分布式锁
  27. //使用DCL(双端检锁机制)来完成对于数据库的访问
  28. synchronized (this){
  29. //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
  30. String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
  31. if (!StringUtils.isEmpty(catalogJSON)){
  32. //如果缓存不为null直接缓存
  33. Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
  34. return result;
  35. }
  36. System.out.println("查询了数据库。。。。。");
  37. /**
  38. * 1、将数据库的多次查询变为1次
  39. */
  40. List<CategoryEntity> selectList = baseMapper.selectList(null);
  41. //查出所有一级分类
  42. List<CategoryEntity> level1Category = getParent_cid(selectList,0L);
  43. //2、封装数据
  44. Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
  45. //1、每一个的一级分类,查到这个一级分类的二级分类
  46. List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
  47. //2、封装上面的结果
  48. List<Catelog2Vo> catelog2Vos = null;
  49. if (categoryEntities != null) {
  50. catelog2Vos = categoryEntities.stream().map(l2 -> {
  51. Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
  52. //1、找当前二级分类的三级分类封装vo
  53. List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
  54. if (level3Catelog != null){
  55. List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
  56. //2、封装成指定格式
  57. Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
  58. return catelog3Vo;
  59. }).collect(Collectors.toList());
  60. catelog2Vo.setCatelog3List(Collections.singletonList(collect));
  61. }
  62. return catelog2Vo;
  63. }).collect(Collectors.toList());
  64. }
  65. return catelog2Vos;
  66. }));
  67. //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
  68. String jsonString = JSON.toJSONString(parent_cid);
  69. redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
  70. return parent_cid;
  71. }
  72. }

这里我们使用了双端检锁机制来控制线程的并发访问数据库。一个线程进入到临界区之前,进行缓存中是否有数据,进入到临界区后,再次判断缓存中是否有数据,这样做的目的是避免阻塞在临界区的多个线程,在其他线程释放锁后,重复进行数据库的查询和放缓存操作。

注:关于双端检锁机制的简单了解,可以参照:https://www.cnblogs.com/cosmos-wong/p/11914878.html

 (3)后执行压测,使用100个线程来回发送请求:

优化后多线程访问时仅查询一次数据库

5)结论

通过观察日志,能够发现只有一个线程查询了数据库,其他线程都是直接从缓存中获取到数据的。所以在单体应用上实现了多线程的并发访问。

由于这里我们的“gulimall-product”就部署了一台,所以看上去一切祥和,但是在如果部署了多台,问题就出现了,主要问题就集中在我们所使用的锁上。我们锁使用的是“synchronized ”,这是一种本地锁,它只是在一台设备上有效,无法实现分布式情况下,锁住其他设备的相同操作。

我们现在的操作模型,表现为如下的形式:

6、本地锁在分布式下的问题

1)、删除“redis”中的“catelogJson”

2)、复制服务

为了演示在分布式情况下本地锁的工作状况,我们将“gulimall-product”按照如下的方式复制了2份

 这样形成了2个复制的和一个原生的:

3)、启动服务

同时启动四个服务,此时在Nacos中我们可以看到“gulimall-product”服务具有四个实例:

4)、执行压测

在修改Jmeter的HTTP请求参数:

注意:之前我们在配置Nginx的时候,配置了upstream,所以它会将请求转给网关,通过网关负载均衡到“gulimall”服务的多个实例上:

在Jmeter中修改本次测试所使用的线程数和循环次数:

启动Jmeter进行压测

5)、查看结果

查看各个微服务的输出:

10002

10003:

10004:

总结:

能够发现,四个服务,分别存在着四个缓存未命中的情况,也就意味着会有四次查询数据库的操作,显然我们的synchronize锁未能实现限制其他服务实例进入临界区,也就印证了在分布式情况下,本地锁只能针对于当前的服务生效。

7、分布式锁原理与使用

1) 、本地缓存面临问题

当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题

所有的服务都到同一个redis进行获取数据,就可以避免这个问题
 

2)、分布式锁

当分布式项目在高并发下也需要加锁,但本地锁只能锁住当前服务,这个时候就需要分布式锁

3) 、分布式锁的演进

基本原理

下面使用redis来实现分布式锁,使用的是SET key value [EX seconds] [PX milliseconds] [NX|XX],http://www.redis.cn/commands/set.html

(1)打开SecureCRT,创建四个Redis的redis-cli连接

(2)同时执行“set loc 1 NX”观察四个窗口的输出

将命令批量发送到四个窗口的的方式:

阶段一

  1. public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
  2. //阶段一
  3. Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
  4. //获取到锁,执行业务
  5. if (lock) {
  6. //加锁成功。。。执行业务
  7. //2、设置过期时间,必须和加锁是同步的,原子的
  8. Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
  9. redisTemplate.delete("lock");//删除锁
  10. return dataFromDB;
  11. }else {
  12. //没获取到锁,等待100ms重试
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. return getCatalogJsonDBWithRedisLock();
  19. }
  20. }

问题: 1、setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁

解决:设置锁的自动过期,即使没有删除,会自动删除

阶段二

  1. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
  2. //1、占分布式锁。去redis占坑
  3. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
  4. if (lock){
  5. //加锁成功。。。执行业务
  6. //2、设置过期时间
  7. redisTemplate.expire("lock",30,TimeUnit.SECONDS);
  8. Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
  9. redisTemplate.delete("lock");//删除锁
  10. return dataFromDB ;
  11. }else {
  12. //加锁失败。。。重试。 synchronized()
  13. //没获取到锁,等待100ms重试
  14. try {
  15. Thread.sleep(100);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. return getCatelogJsonFromDBWithRedisLock();//自旋的方式
  20. }
  21. }

问题: 1、setnx设置好,正要去设置过期时间,宕机。又死锁了。 解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令

阶段三

  1. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
  2. //1、占分布式锁。去redis占坑
  3. String uuid = UUID.randomUUID().toString();
  4. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
  5. if (lock){
  6. //加锁成功。。。执行业务
  7. //2、设置过期时间,必须和加锁是同步的,原子的
  8. Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
  9. redisTemplate.delete("lock");//删除锁
  10. return dataFromDB;
  11. }else {
  12. //加锁失败。。。重试。 synchronized()
  13. //没获取到锁,等待100ms重试
  14. try {
  15. Thread.sleep(100);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. return getCatelogJsonFromDBWithRedisLock();//自旋的方式
  20. }
  21. }

问题: 1、删除锁直接删除??? 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。 解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。

阶段四

  1. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
  2. //1、占分布式锁。去redis占坑
  3. String uuid = UUID.randomUUID().toString();
  4. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
  5. if (lock){
  6. //加锁成功。。。执行业务
  7. //2、设置过期时间,必须和加锁是同步的,原子的
  8. Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
  9. String lockValue = redisTemplate.opsForValue().get("lock");
  10. if (uuid.equals(lockValue)) {
  11. //删除我自己的锁
  12. redisTemplate.delete("lock");//删除锁
  13. }
  14. return dataFromDB;
  15. }else {
  16. //加锁失败。。。重试。 synchronized()
  17. //没获取到锁,等待100ms重试
  18. try {
  19. Thread.sleep(100);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. return getCatelogJsonFromDBWithRedisLock();//自旋的方式
  24. }
  25. }

问题: 1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁 解决: 删除锁必须保证原子性。使用redis+Lua脚本完成

阶段五-最终形态

  1. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
  2. //1、占分布式锁。去redis占坑
  3. String uuid = UUID.randomUUID().toString();
  4. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
  5. if (lock){
  6. //加锁成功。。。执行业务
  7. //2、设置过期时间,必须和加锁是同步的,原子的
  8. Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
  9. //获取值对比+对比成功删除=原子操作 Lua脚本解锁
  10. // String lockValue = redisTemplate.opsForValue().get("lock");
  11. // if (uuid.equals(lockValue)) {
  12. // //删除我自己的锁
  13. // redisTemplate.delete("lock");//删除锁
  14. // }
  15. String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
  16. " return redis.call(\"del\",KEYS[1])\n" +
  17. "else\n" +
  18. " return 0\n" +
  19. "end";
  20. //删除锁
  21. Integer lock1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);
  22. return dataFromDB;
  23. }else {
  24. //加锁失败。。。重试。 synchronized()
  25. //没获取到锁,等待100ms重试
  26. try {
  27. Thread.sleep(100);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. return getCatelogJsonFromDBWithRedisLock();//自旋的方式
  32. }
  33. }

保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期

8、Redisson

Redison使用手册:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

导入依赖:以后使用Redisson作为分布式锁,分布式对象等功能框架

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>3.12.5</version>
  5. </dependency>

另外Redison也提供了一个集成到SpringBoot上的starter

由于我们使用的单节点,所以配置了单节点的Redisson,https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95#261-%E5%8D%95%E8%8A%82%E7%82%B9%E8%AE%BE%E7%BD%AE

创建“MyRedisConfig ” 配置类:

  1. @Configuration
  2. public class MyRedisConfig {
  3. @Bean(destroyMethod="shutdown")
  4. public RedissonClient redisson() throws IOException {
  5. Config config = new Config();
  6. config.useSingleServer().setAddress("redis://192.168.137.14:6379");
  7. RedissonClient redisson = Redisson.create(config);
  8. return redisson;
  9. }
  10. }

测试是否注入了RedissonClient,

  1. @Autowired
  2. RedissonClient redissonClient;
  3. @Test
  4. public void testRedison(){
  5. System.out.println(redissonClient);
  6. }

1)、Redisson-lock锁测试

Redison分布式锁:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

在Redison中分布式锁的使用,和java.util.concurrent包中的所提供的锁的使用方法基本相同。

测试Redisson的Lock锁的使用:

  1. @GetMapping("/hello")
  2. @ResponseBody
  3. public String hello(){
  4. //1.获取一把锁,只要名字一样,就是同一把锁
  5. RLock lock = redisson.getLock("my-lock");
  6. //2.加锁和解锁
  7. try {
  8. lock.lock();
  9. System.out.println("加锁成功,执行业务方法..."+Thread.currentThread().getId());
  10. Thread.sleep(30000);
  11. } catch (Exception e){
  12. }finally {
  13. lock.unlock();
  14. System.out.println("释放锁..."+Thread.currentThread().getId());
  15. }
  16. return "hello";
  17. }

同时发送两个请求到:http://localhost:10000/hello

能够看到在加锁期间另外一个请求一直都是出于挂起状态,需要等待上一个请求处理完毕后,它才能接着执行。

查看Redis:

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

设想一种情况,一个请求线程在执行业务方法的时候,突然发生了中断,此时没有来得及执行释放锁操作,那么同时等待的另外一个线程是否会发生死锁。为了模拟这种情形,我们同时启动10000和10001,同时发送请求。

同时发送请求:

在10000端口上的服务在获取锁后,突然中断它的运行

观察Redis,能够看到这个锁仍然仍然存在:

此时在10001上运行的服务先是等待一会,然后成功获取到了锁:

观察Redis能够看大这个锁变为了针对于10001端口的了:

通过上面的实践能够看到,在加锁后,即便我们没有释放锁,也会自动的释放锁,这是因为在Redisson中会为每个锁加上“leaseTime”,默认是30秒

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

https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

在Redis中,我们能够看到这一点:

小结:redisson的lock具有如下特点

(1)阻塞式等待。默认的锁的时间是30s。

(2)锁定的制动续期,如果业务超长,运行期间会自动给锁续上新的30s,无需担心业务时间长,锁自动被删除的问题。

(3)加锁的业务只要能够运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

lock方法还有一个重载的方法,lock(long leaseTime, TimeUnit unit) :

  1. @Override
  2. public void lock(long leaseTime, TimeUnit unit) {
  3. try {
  4. lock(leaseTime, unit, false);
  5. } catch (InterruptedException e) {
  6. throw new IllegalStateException();
  7. }
  8. }
lock.lock(10,TimeUnit.SECOND);//设置10秒钟自动解锁,自动解锁时一定要大于业务执行时间

问题:lock(long leaseTime, TimeUnit unit),在锁到期后,是否会自动续期?

答:在指定了超时时间后,不会进行自动续期,此时如果有多个线程,即便业务仍然在执行,超时时间到了后,锁也会失效,其他线程就会争抢到锁。

(1)设置了超时时间后,就会发送给Redis的执行脚本,进行占锁,默认超时就是我们指定的时间。

(2)未指定超时时间,就使用30*1000【LockWatchdogTimeout看门狗的默认时间】的时间作为重新续期后的超时时间。

关于续期周期,只要锁占领成功,就会自动启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期,续成30s。这个10s中是根据( internalLockLeasTime)/3得到的。

尽管相对于lock(),lock(long leaseTime, TimeUnit unit)存在到期后自动删除的问题,但是我们对于它的使用还是比较多的,通常都会评估一下业务的最大执行用时,在这个时间内,如果仍然未能执行完成,则认为出现了问题,则释放锁执行其他逻辑。

4)、读写锁测试

保证一定能够读取到最新的数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁,写锁没释放读就必须等待。

(1)在Redis增加一个新的key“writeValue”,值为11111

(2)增加write和read的controller方法

  1. @GetMapping("/write")
  2. @ResponseBody
  3. public String writeValue(){
  4. RReadWriteLock writeLock=redisson.getReadWriteLock("rw-loc");
  5. String uuid = null;
  6. RLock lock = writeLock.writeLock();
  7. lock.lock();
  8. try {
  9. uuid = UUID.randomUUID().toString();
  10. redisTemplate.opsForValue().set("writeValue",uuid);
  11. Thread.sleep(30-000);
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }finally {
  15. lock.unlock();
  16. }
  17. return uuid;
  18. }
  19. @GetMapping("/read")
  20. @ResponseBody
  21. public String redValue(){
  22. String uuid = null;
  23. RReadWriteLock readLock=redisson.getReadWriteLock("rw-loc");
  24. RLock lock = readLock.readLock();
  25. lock.lock();
  26. try {
  27. uuid = redisTemplate.opsForValue().get("writeValue");
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }finally {
  31. lock.unlock();
  32. }
  33. return uuid;
  34. }

(3)启动gulimall-product

(4)分别访问“http://localhost:10000/read”和“http://localhost:10000/write”,观察现象。

  • 执行写操作时,读操作必须要等待;
  • 可以同时执行多个读操作,读操作之间互不影响;
  • 在写操作时查看Redis中“rw-lock”的状态

读写锁补充

(1)修改“read”和“write”的controller方法

  1. @GetMapping("/write")
  2. @ResponseBody
  3. public String writeValue(){
  4. RReadWriteLock writeLock=redisson.getReadWriteLock("rw-loc");
  5. String uuid = null;
  6. RLock lock = writeLock.writeLock();
  7. lock.lock();
  8. try {
  9. log.info("写锁加锁成功");
  10. uuid = UUID.randomUUID().toString();
  11. redisTemplate.opsForValue().set("writeValue",uuid);
  12. Thread.sleep(30000);
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }finally {
  16. lock.unlock();
  17. log.info("写锁释放");
  18. }
  19. return uuid;
  20. }
  21. @GetMapping("/read")
  22. @ResponseBody
  23. public String redValue(){
  24. String uuid = null;
  25. RReadWriteLock readLock=redisson.getReadWriteLock("rw-loc");
  26. RLock lock = readLock.readLock();
  27. lock.lock();
  28. try {
  29. log.info("读锁加锁成功");
  30. uuid = redisTemplate.opsForValue().get("writeValue");
  31. Thread.sleep(30000);
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. }finally {
  35. lock.unlock();
  36. log.info("读锁释放");
  37. }
  38. return uuid;
  39. }
  40. }

(2)先发送一个写请求,然后同时发送四个读请求

(3)观察现象

  • 在写操作期间,四个读操作被阻塞,此时查看Redis中“rw-loc”状态,是写状态
  • 写操作完毕后,查看Redis中的“rw-loc”状态,状态为读状态

    同时会出现三个读锁的
  • 查看控制台输出

    能够看到三个读操作是同时获取到锁的。

另外在先执行读操作时,写操作被阻塞。

小结:

  • 读+读:相当于无锁,并发读,只会在redis中记录,所有当前的读锁,都会同时加锁成功
  • 写+读:等待写锁释放;
  • 写+写:阻塞方式
  • 读+写:写锁等待读锁释放,才能加锁

所以只要存在写操作,不论前面是或后面执行的是读或写操作,都会阻塞。

5)、闭锁测试

6)、信号量测试

先在redis中设置park的值为3

信号量作为分布式限流:

7)、缓存一致性解决

缓存一致性是为了解决数据库和缓存的数据不同步问题的。

1)缓存一致性——双写模式

2)缓存一致性——失效模式

3)缓存一致性——解决方案

  1. 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
  • 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  • 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  • 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  • 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

  1. 总结:
  • 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点

4)缓存一致性——解决canal

使用Redisson的锁机制优化三级分类数据的查询。

  1. /**
  2. * 使用Redisson分布式锁来实现多个服务共享同一缓存中的数据
  3. * @return
  4. */
  5. public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedissonLock() {
  6. RLock lock = redissonClient.getLock("CatelogJson-lock");
  7. //该方法会阻塞其他线程向下执行,只有释放锁之后才会接着向下执行
  8. lock.lock();
  9. Map<String, List<Catelog2Vo>> catelogJsonFromDb;
  10. try {
  11. //从数据库中查询分类数据
  12. catelogJsonFromDb = getCatelogJsonFromDb();
  13. } finally {
  14. lock.unlock();
  15. }
  16. return catelogJsonFromDb;
  17. }

我们系统的一致性解决方案:

1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新

2、读写数据的时候,加上分布式的读写锁。

在更新分类数据的时候,删除缓存中的旧数据。

9、SpringCache-简介

1)、简洁

SpringCache的文档:https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/integration.html#cache

2)、基础概念

3)、SpringCache-整合&体验@Cacheable

整合SpringCache,简化缓存的开发

1)引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-cache</artifactId>
  4. </dependency>

引入spring-boot-starter-data-redis

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. <exclusions>
  5. <exclusion>
  6. <groupId>io.lettuce</groupId>
  7. <artifactId>lettuce-core</artifactId>
  8. </exclusion>
  9. </exclusions>
  10. </dependency>

2)编写配置

(1)缓存的自动配置了哪些?

  • CacheAutoConfiguration,会导入RedisCacheConfiguration
  • 自动配置了缓存管理器RedisCacheManager

(2)配置使用Redis作为缓存
修改“application.properties”文件,指定使用redis作为缓存,spring.cache.type=redis

(3)和缓存有关的注解

  • @Cacheable: Triggers cache population. 触发将数据保存到缓存的操作
  • @CacheEvict: Triggers cache eviction.     触发将数据从缓存中删除的操作
  • @CachePut: Updates the cache without interfering with the method execution. 在不影响方法执行的情况下更新缓存。
  • @Caching: Regroups multiple cache operations to be applied on a method. 组合以上多个操作
  • @CacheConfig: Shares some common cache-related settings at class-level.在类级别上共享一些公共的与缓存相关的设置。

(4)测试使用缓存

(1)开启缓存功能,在主启动类上,标注@EnableCaching
(2)只需要使用注解,就可以完成缓存操作
(3)在业务方法的头部标上@Cacheable,加上该注解后,表示当前方法需要将进行缓存,如果缓存中有,方法无效调用,如果缓存中没有,则会调用方法,最后将方法的结果放入到缓存中。
(4)指定缓存分区。每一个需要缓存的数据,我们都需要来指定要放到哪个名字的缓存中。通常按照业务类型进行划分。

如:我们将一级分类数据放入到缓存中,指定缓存名字为“category”

  1. @Cacheable({"category"})
  2. @Override
  3. public List<CategoryEntity> getLevel1Categories() {
  4. //找出一级分类
  5. List<CategoryEntity> categoryEntities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
  6. return categoryEntities;
  7. }

(5)访问:http://localhost:10000/

(6)查看Redis

能够看到一级分类信息,已经被放入到缓存中了,而且再次访问的时候,没有查询数据库,而是直接从缓存中获取。

4)、@Cacheable细节设置

上面我们将一级分类数据的信息缓存到Redis中了,缓存到Redis中数据具有如下的特点:

  • 如果缓存中有,方法不会被调用;
  • key默认自动生成;形式为"缓存的名字::SimpleKey [](自动生成的key值)"
  • 缓存的value值,默认使用jdk序列化机制,将序列化后的数据缓存到redis;
  • 默认ttl时间为-1,表示永不

然而这些并不能够满足我们的需要,我们希望:

  1. 能够指定生成缓存所使用的key;
  1. 指定缓存的数据的存活时间;
  1. 将缓存的数据保存为json形式;

针对于第一点,我们使用@Cacheable注解的时候,设置key属性,接受一个SpEL

@Cacheable(value = {"category"},key = "'level1Categorys'")

针对于第二点,在配置文件中指定ttl:

spring.cache.redis.time-to-live=3600000 #这里指定存活时间为1小时

清空redis,再次进行访问:http://localhost:10000/

查看Redis

更多关于key的设置,在文档中给予了详细的说明:https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/integration.html#cache-annotations-cacheable

5)、自定义缓存配置

上面我们解决了第一个命名问题和第二个设置存活时间问题,但是如何将数据以JSON的形式缓存到Redis呢?

这涉及到修改缓存管理器的设置,CacheAutoConfiguration导入了RedisCacheConfiguration,而RedisCacheConfiguration中自动配置了缓存管理器RedisCacheManager,而RedisCacheManager要初始化所有的缓存,每个缓存决定使用什么样的配置,如果RedisCacheConfiguration有,就用已有的,没有就用默认配置。

想要修改缓存的配置,只需要给容器中放一个“redisCacheConfiguration”即可,这样就会应用到当前RedisCacheManager管理的所有缓存分区中。

  1. private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
  2. CacheProperties cacheProperties, ClassLoader classLoader) {
  3. Redis redisProperties = cacheProperties.getRedis();
  4. org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
  5. .defaultCacheConfig();
  6. config = config.serializeValuesWith(
  7. SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
  8. if (redisProperties.getTimeToLive() != null) {
  9. config = config.entryTtl(redisProperties.getTimeToLive());
  10. }
  11. if (redisProperties.getKeyPrefix() != null) {
  12. config = config.prefixKeysWith(redisProperties.getKeyPrefix());
  13. }
  14. if (!redisProperties.isCacheNullValues()) {
  15. config = config.disableCachingNullValues();
  16. }
  17. if (!redisProperties.isUseKeyPrefix()) {
  18. config = config.disableKeyPrefix();
  19. }
  20. return config;
  21. }

Redis中的序列化器:org.springframework.data.redis.serializer.RedisSerializer

在Redis中放入自动配置类,设置JSON序列化机制

  1. package com.atguigu.gulimall.product.config;
  2. import org.springframework.boot.autoconfigure.cache.CacheProperties;
  3. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  4. import org.springframework.cache.annotation.EnableCaching;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.data.redis.cache.RedisCacheConfiguration;
  8. import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
  9. import org.springframework.data.redis.serializer.RedisSerializationContext;
  10. import org.springframework.data.redis.serializer.StringRedisSerializer;
  11. /**
  12. * @Description: MyCacheConfig
  13. * @Author: WangTianShun
  14. * @Date: 2020/11/11 12:57
  15. * @Version 1.0
  16. */
  17. @EnableConfigurationProperties(CacheProperties.class)
  18. @Configuration
  19. @EnableCaching
  20. public class MyCacheConfig {
  21. /**
  22. * 配置文件中的东西没有用到
  23. *
  24. * 1、原来和配置文件绑定的配置类是这样的
  25. * @ConfigurationProperties(prefix="spring.cache")
  26. * public class CacheProperties
  27. * 2、让他生效
  28. * @EnableConfigurationProperties(CacheProperties.class)
  29. * @return
  30. */
  31. @Bean
  32. RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
  33. RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
  34. config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
  35. config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
  36. //将配置文件中的所有配置都生效
  37. CacheProperties.Redis redisProperties = cacheProperties.getRedis();
  38. //设置配置文件中的各项配置,如过期时间
  39. if (redisProperties.getTimeToLive() != null) {
  40. config = config.entryTtl(redisProperties.getTimeToLive());
  41. }
  42. if (redisProperties.getKeyPrefix() != null) {
  43. config = config.prefixKeysWith(redisProperties.getKeyPrefix());
  44. }
  45. if (!redisProperties.isCacheNullValues()) {
  46. config = config.disableCachingNullValues();
  47. }
  48. if (!redisProperties.isUseKeyPrefix()) {
  49. config = config.disableKeyPrefix();
  50. }
  51. return config;
  52. }
  53. }

查看Redis能够看到以JSON的形式,将数据缓存下来了:

在配置文件中,还可以指定一些缓存的自定义配置

  1. spring.cache.type=redis
  2. #设置超时时间,默认是毫秒
  3. spring.cache.redis.time-to-live=3600000
  4. #设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀
  5. spring.cache.redis.key-prefix=CACHE_
  6. spring.cache.redis.use-key-prefix=true
  7. #是否缓存空值,防止缓存穿透
  8. spring.cache.redis.cache-null-values=true

基于这个配置,在如果出现了null值,也会被保存到redis中:

如果配置“spring.cache.redis.use-key-prefix=false”,则生成的key没有前缀:

6)、@CacheEvict

在上面实例中,在读模式中,我们将一级分类信息缓存到redis中,当请求再次获取数据时,直接从缓存中进行获取,但是如果执行的是写模式呢?

在写模式下,有两种方式来解决缓存一致性问题,双写模式和失效模式,在SpringCache中可以通过@CachePut来实现双写模式,使用@CacheEvict来实现失效模式。

实例:使用缓存失效机制实现更新数据库中值的是,使得缓存中的数据失效

(1)修改updateCascade方法,添加@CacheEvict注解,指明要删除哪个分类下的数据,并且确定key:

  1. @Cacheable(value = {"category"}, key = "#root.methodName") //代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,最后将方法放入缓存。
  2. @Override
  3. public List<CategoryEntity> getLevel1Category() {
  4. List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
  5. return categoryEntities;
  6. }

(2)启动gulimall-product,启动renren-fast,启动gulimall-gateway,启动项目的前端页面

(3)检查redis中是否有“category”命名空间下的数据,没有则访问http://localhost:10000/,生成数据

(4)修改数据:

(5)检查Redis中对应数据是否还存在

检查后发现数据没了,说明缓存失效策略是有效的。

另外在修改了一级缓存时,对应的二级缓存也需要更新,需要修改原来二级分类的执行逻辑。

将“getCatelogJson”恢复成为原来的逻辑,但是设置@Cacheable,非侵入的方式将查询结果缓存到redis中:

  1. @Cacheable(value = {"category"},key = "#root.methodName")
  2. @Override
  3. public Map<String, List<Catelog2Vo>> getCatelogJson() {
  4. log.info("查询数据库");
  5. //一次性查询出所有的分类数据,减少对于数据库的访问次数,后面的数据操作并不是到数据库中查询,而是直接从这个集合中获取,
  6. // 由于分类信息的数据量并不大,所以这种方式是可行的
  7. List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
  8. //1.查出所有一级分类
  9. List<CategoryEntity> level1Categories = getParentCid(categoryEntities,0L);
  10. Map<String, List<Catelog2Vo>> parent_cid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
  11. //2. 根据一级分类的id查找到对应的二级分类
  12. List<CategoryEntity> level2Categories = getParentCid(categoryEntities,level1.getCatId());
  13. //3. 根据二级分类,查找到对应的三级分类
  14. List<Catelog2Vo> catelog2Vos =null;
  15. if(null != level2Categories || level2Categories.size() > 0){
  16. catelog2Vos = level2Categories.stream().map(level2 -> {
  17. //得到对应的三级分类
  18. List<CategoryEntity> level3Categories = getParentCid(categoryEntities,level2.getCatId());
  19. //封装到Catalog3List
  20. List<Catalog3List> catalog3Lists = null;
  21. if (null != level3Categories) {
  22. catalog3Lists = level3Categories.stream().map(level3 -> {
  23. Catalog3List catalog3List = new Catalog3List(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
  24. return catalog3List;
  25. }).collect(Collectors.toList());
  26. }
  27. return new Catelog2Vo(level1.getCatId().toString(), catalog3Lists, level2.getCatId().toString(), level2.getName());
  28. }).collect(Collectors.toList());
  29. }
  30. return catelog2Vos;
  31. }));
  32. return parent_cid;
  33. }

访问:http://gulimall.com/

再次访问,发现控制台数据未更新,还是第一次访问时的输出:

查看Redis中缓存的数据:

上面我们将一级和三级分类信息都缓存到了redis中,现在我们想要实现一种场景是,更新分类数据的时候,将缓存到redis中的一级和三级分类数据都清空。

借助于“@Caching”来完成

  1. @Caching(evict={
  2. @CacheEvict(value = {"category"},key = "'level1Categorys'"),
  3. @CacheEvict(value = {"category"},key = "'getCatelogJson'")
  4. })
  5. @Override
  6. @Transactional
  7. public void updateCascade(CategoryEntity category) {
  8. this.updateById(category);
  9. relationService.updateCategory(category.getCatId(),category.getName());
  10. }

查询redis,一级和三级分类数据已经被删除。

除了可以使用@Cache外,还可以使用@CacheEvict来完成:

@CacheEvict(value = {"category"},allEntries = true)

它表示要删除“category”分区下的所有数据。

可以看到存储同一类型的数据,都可以指定未同一个分区,可以批量删除这个分区下的数据。以后建议不使用分区前缀,而是使用默认的分区前缀。

7)、SpringCache-原理与不足

Spring-Cache的不足:

1)读模式

  • 缓存穿透:查询一个null值。解决,缓存空数据;cache-null-value=true;
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方法,是进行加锁,默认是没有加锁的,查询时设置Cacheable的sync=true即可解决缓存击穿。
  • 缓存雪崩:大量的key同时过期。解决方法:加上随机时间;加上过期时间。“spring.cache.redis.time-to-live=3600000”

2)写模式(缓存与数据一致)

  • 读写加锁;
  • 引入canal,感知到mysql的更新去更新数据库;
  • 读多写少,直接去数据库查询就行;

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据):完全可以使用spring-cache;写模式,只要缓存的数据有过期时间就足够了;
  • 特殊数据:特殊设计;

四、检索

pom添加thymeleaf

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>

在虚拟机mydata/nginx/html/路径下创建search文件夹然后把搜索页的静态资源上传到该文件里

host添加

172.20.10.3       search.gulimall.com

配置nginx

重启nginx

修改网关断言配置

  1. - id: gulimall_host_route
  2. uri: lb://gulimall-product
  3. predicates:
  4. - Host=gulimall.com
  5. - id: gulimall_search_route
  6. uri: lb://gulimall-search
  7. predicates:
  8. - Host=search.gulimall.com

pom添加devtools

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-devtools</artifactId>
  4. <optional>true</optional>
  5. </dependency>

 关闭thymeleaf缓存

spring.thymeleaf.cache=false

1. 检索条件分析

  • 全文检索:skuTitle-》keyword

  • 排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)

  • 过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs

  • 聚合:attrs

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

2. DSL分析

  1. GET gulimall_product/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": [
  6. {
  7. "match": {
  8. "skuTitle": "华为"
  9. }
  10. }
  11. ],
  12. "filter": [
  13. {
  14. "term": {
  15. "catalogId": "225"
  16. }
  17. },
  18. {
  19. "terms": {
  20. "brandId": [
  21. "2"
  22. ]
  23. }
  24. },
  25. {
  26. "term": {
  27. "hasStock": "false"
  28. }
  29. },
  30. {
  31. "range": {
  32. "skuPrice": {
  33. "gte": 1000,
  34. "lte": 7000
  35. }
  36. }
  37. },
  38. {
  39. "nested": {
  40. "path": "attrs",
  41. "query": {
  42. "bool": {
  43. "must": [
  44. {
  45. "term": {
  46. "attrs.attrId": {
  47. "value": "6"
  48. }
  49. }
  50. }
  51. ]
  52. }
  53. }
  54. }
  55. }
  56. ]
  57. }
  58. },
  59. "sort": [
  60. {
  61. "skuPrice": {
  62. "order": "desc"
  63. }
  64. }
  65. ],
  66. "from": 0,
  67. "size": 5,
  68. "highlight": {
  69. "fields": {"skuTitle": {}},
  70. "pre_tags": "<b style='color:red'>",
  71. "post_tags": "</b>"
  72. },
  73. "aggs": {
  74. "brandAgg": {
  75. "terms": {
  76. "field": "brandId",
  77. "size": 10
  78. },
  79. "aggs": {
  80. "brandNameAgg": {
  81. "terms": {
  82. "field": "brandName",
  83. "size": 10
  84. }
  85. },
  86. "brandImgAgg": {
  87. "terms": {
  88. "field": "brandImg",
  89. "size": 10
  90. }
  91. }
  92. }
  93. },
  94. "catalogAgg":{
  95. "terms": {
  96. "field": "catalogId",
  97. "size": 10
  98. },
  99. "aggs": {
  100. "catalogNameAgg": {
  101. "terms": {
  102. "field": "catalogName",
  103. "size": 10
  104. }
  105. }
  106. }
  107. },
  108. "attrs":{
  109. "nested": {
  110. "path": "attrs"
  111. },
  112. "aggs": {
  113. "attrIdAgg": {
  114. "terms": {
  115. "field": "attrs.attrId",
  116. "size": 10
  117. },
  118. "aggs": {
  119. "attrNameAgg": {
  120. "terms": {
  121. "field": "attrs.attrName",
  122. "size": 10
  123. }
  124. }
  125. }
  126. }
  127. }
  128. }
  129. }
  130. }

 修改映射

  1. PUT gulimall_product
  2. {
  3. "mappings": {
  4. "properties": {
  5. "attrs": {
  6. "type": "nested",
  7. "properties": {
  8. "attrId": {
  9. "type": "long"
  10. },
  11. "attrName": {
  12. "type": "keyword"
  13. },
  14. "attrValue": {
  15. "type": "keyword"
  16. }
  17. }
  18. },
  19. "autoGeneratedTimestamp": {
  20. "type": "long"
  21. },
  22. "brandId": {
  23. "type": "long"
  24. },
  25. "brandImg": {
  26. "type": "keyword"
  27. },
  28. "brandName": {
  29. "type": "keyword"
  30. },
  31. "catalogId": {
  32. "type": "long"
  33. },
  34. "catalogName": {
  35. "type": "keyword"
  36. },
  37. "description": {
  38. "type": "text",
  39. "fields": {
  40. "keyword": {
  41. "type": "keyword",
  42. "ignore_above": 256
  43. }
  44. }
  45. },
  46. "hasStock": {
  47. "type": "boolean"
  48. },
  49. "hotScore": {
  50. "type": "long"
  51. },
  52. "parentTask": {
  53. "properties": {
  54. "id": {
  55. "type": "long"
  56. },
  57. "nodeId": {
  58. "type": "text",
  59. "fields": {
  60. "keyword": {
  61. "type": "keyword",
  62. "ignore_above": 256
  63. }
  64. }
  65. },
  66. "set": {
  67. "type": "boolean"
  68. }
  69. }
  70. },
  71. "refreshPolicy": {
  72. "type": "text",
  73. "fields": {
  74. "keyword": {
  75. "type": "keyword",
  76. "ignore_above": 256
  77. }
  78. }
  79. },
  80. "retry": {
  81. "type": "boolean"
  82. },
  83. "saleCount": {
  84. "type": "long"
  85. },
  86. "shouldStoreResult": {
  87. "type": "boolean"
  88. },
  89. "skuId": {
  90. "type": "long"
  91. },
  92. "skuImg": {
  93. "type": "keyword"
  94. },
  95. "skuPrice": {
  96. "type": "keyword"
  97. },
  98. "skuTitle": {
  99. "type": "text",
  100. "analyzer": "ik_smart"
  101. },
  102. "spuId": {
  103. "type": "keyword"
  104. }
  105. }
  106. }
  107. }

 

迁移数据

  1. POST _reindex
  2. {
  3. "source": {
  4. "index": "product"
  5. },
  6. "dest": {
  7. "index": "gulimall_product"
  8. }
  9. }

 修改gulimall-search的常量

修改com.atguigu.gulimall.search.constant.EsConstant类,代码如下

  1. public class EsConstant {
  2. public static final String PRODUCT_INDEX = "gulimall_product"; //sku数据在es中的索引
  3. }

3. 检索代码编写

1) 请求参数和返回结果

请求参数的封装

  1. package com.atguigu.gulimall.search.vo;
  2. import lombok.Data;
  3. import java.util.List;
  4. /**
  5. * @Description: SearchParam
  6. * @Author: WangTianShun
  7. * @Date: 2020/11/12 16:21
  8. * @Version 1.0
  9. *
  10. * 封装页面所有可能传递过来的查询条件
  11. * catalog3Id=225&keyword=小米&sort=saleCount_asc
  12. */
  13. @Data
  14. public class SearchParam {
  15. private String keyword;//页面传递过来的全文匹配关键字
  16. private Long catalog3Id;//三级分类id
  17. /**
  18. * sort=saleCount_asc/desc
  19. * sort=skuPrice_asc/desc
  20. * sort=hotScore_asc/desc
  21. */
  22. private String sort;//排序条件
  23. /**
  24. * 好多的过滤条件
  25. * hasStock(是否有货)、skuPrice区间、brandId、catalog3Id、attrs
  26. * hasStock=0/1
  27. * skuPrice=1_500
  28. */
  29. private Integer hasStock;//是否只显示有货
  30. private String skuPrice;//价格区间查询
  31. private List<Long> brandId;//按照品牌进行查询,可以多选
  32. private List<String> attrs;//按照属性进行筛选
  33. private Integer pageNum = 1;//页码
  34. }

返回结果

  1. package com.atguigu.gulimall.search.vo;
  2. import com.atguigu.common.to.es.SkuEsModel;
  3. import lombok.Data;
  4. import java.util.List;
  5. /**
  6. * @Description: SearchResponse
  7. * @Author: WangTianShun
  8. * @Date: 2020/11/12 16:49
  9. * @Version 1.0
  10. */
  11. @Data
  12. public class SearchResult {
  13. /**
  14. * 查询到的商品信息
  15. */
  16. private List<SkuEsModel> products;
  17. private Integer pageNum;//当前页码
  18. private Long total;//总记录数
  19. private Integer totalPages;//总页码
  20. private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
  21. private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
  22. private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
  23. //=====================以上是返给页面的信息==========================
  24. @Data
  25. public static class BrandVo{
  26. private Long brandId;
  27. private String brandName;
  28. private String brandImg;
  29. }
  30. @Data
  31. public static class CatalogVo{
  32. private Long catalogId;
  33. private String catalogName;
  34. private String brandImg;
  35. }
  36. @Data
  37. public static class AttrVo{
  38. private Long attrId;
  39. private String attrName;
  40. private List<String> attrValue;
  41. }
  42. }

2) 主体逻辑

修改“com.atguigu.gulimall.search.controller.SearchController”类,代码如下:

  1. /**
  2. * 自动将页面提交过来的所有请求查询参数封装成指定的对象
  3. * @param param
  4. * @return
  5. */
  6. @GetMapping("/list.html")
  7. public String listPage(SearchParam searchParam, Model model) {
  8. SearchResult result = mallSearchService.search(searchParam);
  9. System.out.println("===================="+result);
  10. model.addAttribute("result", result);
  11. return "list";
  12. }

 修改“com.atguigu.gulimall.search.service.MallSearchService”类,代码如下:

  1. /**
  2. *
  3. * @param param 检索的所有参数
  4. * @return 返回检索的结果,里面包含页面需要的所有信息
  5. */
  6. SearchResult search(SearchParam param);

主要逻辑在service层进行,service层将封装好的SearchParam组建查询条件,再将返回后的结果封装成SearchResult

  1. package com.atguigu.gulimall.search.service.impl;
  2. import com.alibaba.fastjson.JSON;
  3. import com.atguigu.common.to.es.SkuEsModel;
  4. import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
  5. import com.atguigu.gulimall.search.constant.EsConstant;
  6. import com.atguigu.gulimall.search.service.MallSearchService;
  7. import com.atguigu.gulimall.search.vo.SearchParam;
  8. import com.atguigu.gulimall.search.vo.SearchResult;
  9. import org.apache.commons.lang.StringUtils;
  10. import org.apache.lucene.search.join.ScoreMode;
  11. import org.elasticsearch.action.search.SearchRequest;
  12. import org.elasticsearch.action.search.SearchResponse;
  13. import org.elasticsearch.client.RestHighLevelClient;
  14. import org.elasticsearch.index.query.*;
  15. import org.elasticsearch.search.SearchHit;
  16. import org.elasticsearch.search.SearchHits;
  17. import org.elasticsearch.search.aggregations.AggregationBuilders;
  18. import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
  19. import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
  20. import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
  21. import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
  22. import org.elasticsearch.search.aggregations.bucket.terms.Terms;
  23. import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
  24. import org.elasticsearch.search.builder.SearchSourceBuilder;
  25. import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
  26. import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
  27. import org.elasticsearch.search.sort.SortOrder;
  28. import org.springframework.beans.factory.annotation.Autowired;
  29. import org.springframework.stereotype.Service;
  30. import java.io.IOException;
  31. import java.util.ArrayList;
  32. import java.util.List;
  33. import java.util.stream.Collectors;
  34. /**
  35. * @Description: MallSearchServiceImpl
  36. * @Author: WangTianShun
  37. * @Date: 2020/11/12 16:24
  38. * @Version 1.0
  39. */
  40. @Service
  41. public class MallSearchServiceImpl implements MallSearchService {
  42. @Autowired
  43. RestHighLevelClient restHighLevelClient;
  44. //去es进行检索
  45. @Override
  46. public SearchResult search(SearchParam param) {
  47. //动态构建出查询需要的DSL语句
  48. SearchResult result = null;
  49. //1、准备检索请求
  50. SearchRequest searchRequest = buildSearchRequest(param);
  51. try {
  52. //2、执行检索请求
  53. SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
  54. //分析响应数据封装我们需要的格式
  55. result = buildSearchResult(response,param);
  56. } catch (IOException e) {
  57. e.printStackTrace();
  58. }
  59. return result;
  60. }
  61. /**
  62. * 准备检索请求
  63. * 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存),排序,分页,高亮,聚合分析
  64. * @return
  65. */
  66. private SearchRequest buildSearchRequest(SearchParam param) {
  67. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
  68. /**
  69. * 过滤(按照属性、分类、品牌、价格区间、库存)
  70. */
  71. //1、构建bool-query
  72. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  73. sourceBuilder.query(boolQuery);
  74. //1.1 must-模糊匹配、
  75. if (!StringUtils.isEmpty(param.getKeyword())){
  76. boolQuery.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
  77. }
  78. //1.2.1 filter-按照三级分类id查询
  79. if (null != param.getCatalog3Id()){
  80. boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
  81. }
  82. //1.2.2 filter-按照品牌id查询
  83. if (null != param.getBrandId() && param.getBrandId().size()>0) {
  84. boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
  85. }
  86. //1.2.3 filter-按照是否有库存进行查询
  87. if (null != param.getHasStock() ) {
  88. boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
  89. }
  90. //1.2.4 filter-按照区间进行查询 1_500/_500/500_
  91. RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
  92. if (!StringUtils.isEmpty(param.getSkuPrice())) {
  93. String[] prices = param.getSkuPrice().split("_");
  94. if (prices.length == 1) {
  95. if (param.getSkuPrice().startsWith("_")) {
  96. rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
  97. }else {
  98. rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
  99. }
  100. } else if (prices.length == 2) {
  101. //_6000会截取成["","6000"]
  102. if (!prices[0].isEmpty()) {
  103. rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
  104. }
  105. rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
  106. }
  107. boolQuery.filter(rangeQueryBuilder);
  108. }
  109. //1.2.5 filter-按照属性进行查询
  110. List<String> attrs = param.getAttrs();
  111. if (null != attrs && attrs.size() > 0) {
  112. //attrs=1_5寸:8寸&2_16G:8G
  113. attrs.forEach(attr->{
  114. BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
  115. String[] attrSplit = attr.split("_");
  116. queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
  117. String[] attrValues = attrSplit[1].split(":");
  118. queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
  119. //每一个必须都得生成一个nested查询
  120. NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
  121. boolQuery.filter(nestedQueryBuilder);
  122. });
  123. }
  124. //把以前所有的条件都拿来进行封装
  125. sourceBuilder.query(boolQuery);
  126. /**
  127. * 排序,分页,高亮,
  128. */
  129. //2.1 排序 eg:sort=saleCount_desc/asc
  130. if (!StringUtils.isEmpty(param.getSort())) {
  131. String[] sortSplit = param.getSort().split("_");
  132. sourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
  133. }
  134. //2.2、分页
  135. sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
  136. sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
  137. //2.3 高亮highlight
  138. if (!StringUtils.isEmpty(param.getKeyword())) {
  139. HighlightBuilder highlightBuilder = new HighlightBuilder();
  140. highlightBuilder.field("skuTitle");
  141. highlightBuilder.preTags("<b style='color:red'>");
  142. highlightBuilder.postTags("</b>");
  143. sourceBuilder.highlighter(highlightBuilder);
  144. }
  145. /**
  146. * 聚合分析
  147. */
  148. //5. 聚合
  149. //5.1 按照品牌聚合
  150. TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
  151. //品牌聚合的子聚合
  152. TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
  153. TermsAggregationBuilder brand_img_agg = AggregationBuilders.terms("brand_img_agg").field("brandImg");
  154. brand_agg.subAggregation(brand_name_agg);
  155. brand_agg.subAggregation(brand_img_agg);
  156. sourceBuilder.aggregation(brand_agg);
  157. //5.2 按照catalog聚合
  158. TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
  159. TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
  160. catalog_agg.subAggregation(catalog_name_agg);
  161. sourceBuilder.aggregation(catalog_agg);
  162. //5.3 按照attrs聚合
  163. NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attr_agg", "attrs");
  164. //按照attrId聚合
  165. TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
  166. //按照attrId聚合之后再按照attrName和attrValue聚合
  167. TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
  168. TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
  169. attr_id_agg.subAggregation(attr_name_agg);
  170. attr_id_agg.subAggregation(attr_value_agg);
  171. nestedAggregationBuilder.subAggregation(attr_id_agg);
  172. sourceBuilder.aggregation(nestedAggregationBuilder);
  173. String s = sourceBuilder.toString();
  174. System.out.println("构建的DSL"+s);
  175. SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
  176. return request;
  177. }
  178. /**
  179. * 构建结果数据
  180. * @param response
  181. * @return
  182. */
  183. private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {
  184. SearchResult result = new SearchResult();
  185. //1、返回的所有查询到的商品
  186. SearchHits hits = response.getHits();
  187. List<SkuEsModel> esModels = new ArrayList<>();
  188. if (null != hits.getHits() && hits.getHits().length>0){
  189. for (SearchHit hit : hits.getHits()) {
  190. String sourceAsString = hit.getSourceAsString();
  191. SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
  192. if (!StringUtils.isEmpty(param.getKeyword())) {
  193. HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
  194. esModel.setSkuTitle(skuTitle.fragments()[0].string());
  195. }
  196. esModels.add(esModel);
  197. }
  198. }
  199. result.setProducts(esModels);
  200. //2、当前所有商品涉及到的所有属性
  201. List<SearchResult.AttrVo> attrVos = new ArrayList<>();
  202. ParsedNested attr_agg = response.getAggregations().get("attr_agg");
  203. ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
  204. for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
  205. SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
  206. //1、得到属性的id;
  207. long attrId = bucket.getKeyAsNumber().longValue();
  208. //2、得到属性的名字
  209. String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
  210. //3、得到属性的所有值
  211. List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
  212. String keyAsString = item.getKeyAsString();
  213. return keyAsString;
  214. }).collect(Collectors.toList());
  215. attrVo.setAttrId(attrId);
  216. attrVo.setAttrName(attrName);
  217. attrVo.setAttrValue(attrValues);
  218. attrVos.add(attrVo);
  219. }
  220. result.setAttrs(attrVos);
  221. //3、当前所有品牌涉及到的所有属性
  222. List<SearchResult.BrandVo> brandVos = new ArrayList<>();
  223. ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
  224. for (Terms.Bucket bucket : brand_agg.getBuckets()) {
  225. SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
  226. //1、得到品牌的id
  227. long brandId = bucket.getKeyAsNumber().longValue();
  228. //2、得到品牌的名
  229. String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
  230. //3、得到品牌的图片
  231. String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
  232. brandVo.setBrandId(brandId);
  233. brandVo.setBrandName(brandName);
  234. brandVo.setBrandImg(brandImg);
  235. brandVos.add(brandVo);
  236. }
  237. result.setBrands(brandVos);
  238. //4、当前商品所涉及的分类信息
  239. ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
  240. List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
  241. List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
  242. for (Terms.Bucket bucket : buckets) {
  243. SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
  244. //得到分类id
  245. String keyAsString = bucket.getKeyAsString();
  246. catalogVo.setCatalogId(Long.parseLong(keyAsString));
  247. //得到分类名
  248. ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
  249. String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
  250. catalogVo.setCatalogName(catalog_name);
  251. catalogVos.add(catalogVo);
  252. }
  253. result.setCatalogs(catalogVos);
  254. //===========以上从聚合信息获取到=============
  255. //5、分页信息-页码
  256. result.setPageNum(param.getPageNum());
  257. //6、分页信息-总记录数
  258. long total = hits.getTotalHits().value;
  259. result.setTotal(total);
  260. //7、分页信息-总页码-计算
  261. int totalPages = total%EsConstant.PRODUCT_PAGESIZE == 0 ?(int) total/EsConstant.PRODUCT_PAGESIZE:((int)total/EsConstant.PRODUCT_PAGESIZE+1);
  262. result.setTotalPages(totalPages);
  263. return result;
  264. }
  265. }

4. 页面效果

1) 基本数据渲染

将商品的基本属性渲染出来

  1. <div class="rig_tab">
  2. <!-- 遍历各个商品-->
  3. <div th:each="product : ${result.getProduct()}">
  4. <div class="ico">
  5. <i class="iconfont icon-weiguanzhu"></i>
  6. <a href="/static/search/#">关注</a>
  7. </div>
  8. <p class="da">
  9. <a th:href="|http://item.gulimall.com/${product.skuId}.html|" >
  10. <!--图片 -->
  11. <img class="dim" th:src="${product.skuImg}">
  12. </a>
  13. </p>
  14. <ul class="tab_im">
  15. <li><a href="/static/search/#" title="黑色">
  16. <img th:src="${product.skuImg}"></a></li>
  17. </ul>
  18. <p class="tab_R">
  19. <!-- 价格 -->
  20. <span th:text="'¥' + ${product.skuPrice}">¥5199.00</span>
  21. </p>
  22. <p class="tab_JE">
  23. <!-- 标题 -->
  24. <!-- 使用utext标签,使检索时高亮不会被转义-->
  25. <a href="/static/search/#" th:utext="${product.skuTitle}">
  26. Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
  27. </a>
  28. </p>
  29. <p class="tab_PI">已有<span>11万+</span>热门评价
  30. <a href="/static/search/#">二手有售</a>
  31. </p>
  32. <p class="tab_CP"><a href="/static/search/#" title="谷粒商城Apple产品专营店">谷粒商城Apple产品...</a>
  33. <a href='#' title="联系供应商进行咨询">
  34. <img src="/static/search/img/xcxc.png">
  35. </a>
  36. </p>
  37. <div class="tab_FO">
  38. <div class="FO_one">
  39. <p>自营
  40. <span>谷粒商城自营,品质保证</span>
  41. </p>
  42. <p>满赠
  43. <span>该商品参加满赠活动</span>
  44. </p>
  45. </div>
  46. </div>
  47. </div>
  48. </div>

2) 筛选条件渲染

将结果的品牌、分类、商品属性进行遍历显示,并且点击某个属性值时可以通过拼接url进行跳转

  1. <div class="JD_nav_logo">
  2. <!--品牌-->
  3. <div class="JD_nav_wrap">
  4. <div class="sl_key">
  5. <span>品牌:</span>
  6. </div>
  7. <div class="sl_value">
  8. <div class="sl_value_logo">
  9. <ul>
  10. <li th:each="brand: ${result.getBrands()}">
  11. <!--替换url-->
  12. <a href="#" th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}">
  13. <img src="/static/search/img/598033b4nd6055897.jpg" alt="" th:src="${brand.brandImg}">
  14. <div th:text="${brand.brandName}">
  15. 华为(HUAWEI)
  16. </div>
  17. </a>
  18. </li>
  19. </ul>
  20. </div>
  21. </div>
  22. <div class="sl_ext">
  23. <a href="#">
  24. 更多
  25. <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
  26. <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
  27. </a>
  28. <a href="#">
  29. 多选
  30. <i>+</i>
  31. <span>+</span>
  32. </a>
  33. </div>
  34. </div>
  35. <!--分类-->
  36. <div class="JD_pre" th:each="catalog: ${result.getCatalogs()}">
  37. <div class="sl_key">
  38. <span>分类:</span>
  39. </div>
  40. <div class="sl_value">
  41. <ul>
  42. <li><a href="#" th:text="${catalog.getCatalogName()}" th:href="${'javascript:searchProducts(&quot;catalogId&quot;,'+catalog.catalogId+')'}">0-安卓(Android)</a></li>
  43. </ul>
  44. </div>
  45. </div>
  46. <!--价格-->
  47. <div class="JD_pre">
  48. <div class="sl_key">
  49. <span>价格:</span>
  50. </div>
  51. <div class="sl_value">
  52. <ul>
  53. <li><a href="#">0-499</a></li>
  54. <li><a href="#">500-999</a></li>
  55. <li><a href="#">1000-1699</a></li>
  56. <li><a href="#">1700-2799</a></li>
  57. <li><a href="#">2800-4499</a></li>
  58. <li><a href="#">4500-11999</a></li>
  59. <li><a href="#">12000以上</a></li>
  60. <li class="sl_value_li">
  61. <input type="text">
  62. <p>-</p>
  63. <input type="text">
  64. <a href="#">确定</a>
  65. </li>
  66. </ul>
  67. </div>
  68. </div>
  69. <!--商品属性-->
  70. <div class="JD_pre" th:each="attr: ${result.getAttrs()}" >
  71. <div class="sl_key">
  72. <span th:text="${attr.getAttrName()}">系统:</span>
  73. </div>
  74. <div class="sl_value">
  75. <ul>
  76. <li th:each="val: ${attr.getAttrValue()}">
  77. <a href="#"
  78. th:text="${val}"
  79. th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}">0-安卓(Android)</a></li>
  80. </ul>
  81. </div>
  82. </div>
  83. </div>
  1. function searchProducts(name,value){
  2. //原来的页面
  3. var href = location.href + "";
  4. if (href.indexOf("?") != -1){
  5. location.href = location.href + "&" +name+ "=" + value;
  6. }else {
  7. location.href = location.href + "?" +name+ "=" + value;
  8. }
  9. }
  10. /**
  11. * @param url 目前的url
  12. * @param paramName 需要替换的参数属性名
  13. * @param replaceVal 需要替换的参数的新属性值
  14. * @param forceAdd 该参数是否可以重复查询(attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏)
  15. * @returns {string} 替换或添加后的url
  16. */
  17. function replaceParamVal(url, paramName, replaceVal,forceAdd) {
  18. var oUrl = url.toString();
  19. var nUrl;
  20. if (oUrl.indexOf(paramName) != -1) {
  21. if( forceAdd && oUrl.indexOf(paramName+"="+replaceVal)==-1) {
  22. if (oUrl.indexOf("?") != -1) {
  23. nUrl = oUrl + "&" + paramName + "=" + replaceVal;
  24. } else {
  25. nUrl = oUrl + "?" + paramName + "=" + replaceVal;
  26. }
  27. } else {
  28. var re = eval('/(' + paramName + '=)([^&]*)/gi');
  29. nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
  30. }
  31. } else {
  32. if (oUrl.indexOf("?") != -1) {
  33. nUrl = oUrl + "&" + paramName + "=" + replaceVal;
  34. } else {
  35. nUrl = oUrl + "?" + paramName + "=" + replaceVal;
  36. }
  37. }
  38. return nUrl;
  39. };

3) 分页数据渲染

将页码绑定至属性pn,当点击某页码时,通过获取pn值进行url拼接跳转页面

  1. <div class="filter_page">
  2. <div class="page_wrap">
  3. <span class="page_span1">
  4. <!-- 不是第一页时显示上一页 -->
  5. <a class="page_a" href="#" th:if="${result.pageNum>1}" th:attr="pn=${result.getPageNum()-1}">
  6. < 上一页
  7. </a>
  8. <!-- 将各个页码遍历显示,并将当前页码绑定至属性pn -->
  9. <a href="#" class="page_a"
  10. th:each="page: ${result.pageNavs}"
  11. th:text="${page}"
  12. th:style="${page==result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
  13. th:attr="pn=${page}"
  14. >1</a>
  15. <!-- 不是最后一页时显示下一页 -->
  16. <a href="#" class="page_a" th:if="${result.pageNum<result.totalPages}" th:attr="pn=${result.getPageNum()+1}">
  17. 下一页 >
  18. </a>
  19. </span>
  20. <span class="page_span2">
  21. <em><b th:text="${result.totalPages}">169</b>&nbsp;&nbsp;到第</em>
  22. <input type="number" value="1" class="page_input">
  23. <em></em>
  24. <a href="#">确定</a>
  25. </span>
  26. </div>
  27. </div>
  1. $(".page_a").click(function () {
  2. var pn=$(this).attr("pn");
  3. location.href=replaceParamVal(location.href,"pageNum",pn,false);
  4. console.log(replaceParamVal(location.href,"pageNum",pn,false))
  5. })

4) 页面排序和价格区间

页面排序功能需要保证,点击某个按钮时,样式会变红,并且其他的样式保持最初的样子;

点击某个排序时首先按升序显示,再次点击再变为降序,并且还会显示上升或下降箭头

页面排序跳转的思路是通过点击某个按钮时会向其class属性添加/去除desc,并根据属性值进行url拼接

  1. <div class="filter_top">
  2. <div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}">
  3. <!-- 通过判断当前class是否有desc来进行样式的渲染和箭头的显示-->
  4. <a sort="hotScore"
  5. th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
  6. th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')) ?
  7. 'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
  8. 综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
  9. #strings.endsWith(p,'desc')) ?'↓':'↑' }]]</a>
  10. <a sort="saleCount"
  11. th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
  12. th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount')) ?
  13. 'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
  14. 销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
  15. #strings.endsWith(p,'desc'))?'↓':'↑' }]]</a>
  16. <a sort="skuPrice"
  17. th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
  18. th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice')) ?
  19. 'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
  20. 价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
  21. #strings.endsWith(p,'desc'))?'↓':'↑' }]]</a>
  22. <a sort="hotScore" class="sort_a">评论分</a>
  23. <a sort="hotScore" class="sort_a">上架时间</a>
  24. <!--价格区间搜索-->
  25. <input id="skuPriceFrom" type="number"
  26. th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
  27. style="width: 100px; margin-left: 30px">
  28. -
  29. <input id="skuPriceTo" type="number"
  30. th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
  31. style="width: 100px">
  32. <button id="skuPriceSearchBtn">确定</button>
  33. </div>
  34. <div class="filter_top_right">
  35. <span class="fp-text">
  36. <b>1</b><em>/</em><i>169</i>
  37. </span>
  38. <a href="#" class="prev"><</a>
  39. <a href="#" class="next"> > </a>
  40. </div>
  41. </div>
  1. $(".sort_a").click(function () {
  2. //添加、剔除desc
  3. $(this).toggleClass("desc");
  4. //获取sort属性值并进行url跳转
  5. let sort = $(this).attr("sort");
  6. sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
  7. location.href = replaceParamVal(location.href, "sort", sort,false);
  8. return false;
  9. });

价格区间搜索函数

  1. $("#skuPriceSearchBtn").click(function () {
  2. var skuPriceFrom = $("#skuPriceFrom").val();
  3. var skuPriceTo = $("#skuPriceTo").val();
  4. location.href = replaceParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
  5. })

 5)是否有库存

  1. <li>
  2. <a href="#" th:with="check = ${param.hasStock}">
  3. <input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')}">
  4. 仅显示有货
  5. </a>
  6. </li>

6)关键字搜索修改

  1. function searchProducts(name, value) {
  2. //原来的页面
  3. // var href = location.href + "";
  4. // if (href.indexOf("?") != -1) {
  5. // location.href = location.href + "&" + name + "=" + value;
  6. // } else {
  7. // location.href = location.href + "?" + name + "=" + value;
  8. // }
  9. location.href = replaceParamVal(location.href,name,value,true);
  10. }
  1. $("#skuPriceSearchBtn").click(function () {
  2. //1、拼上价格区间的查询条件
  3. var skuPriceFrom = $("#skuPriceFrom").val();
  4. var skuPriceTo = $("#skuPriceTo").val();
  5. location.href = replaceParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
  6. });
  7. /*TODO 是否有库存路径有bug*/
  8. $("#showHasStock").change(function(){
  9. if ($(this).prop('checked')){
  10. location.href = replaceParamVal(location.href, "hasStock", 1, false);
  11. }else {
  12. //没选中
  13. var re = eval('/(hasStock=)([^&]*)/gi');
  14. location.href = (location.href+"").replace(re,'');
  15. }
  16. return false;
  17. })

5) 面包屑导航

修改gulimall-search的pom

定义springcloud的版本

  1. <properties>
  2. <java.version>1.8</java.version>
  3. <elasticsearch.version>7.4.2</elasticsearch.version>
  4. <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
  5. </properties>

 添加依赖管理

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>org.springframework.cloud</groupId>
  5. <artifactId>spring-cloud-dependencies</artifactId>
  6. <version>${spring-cloud.version}</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>
  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-openfeign</artifactId>
  4. </dependency>

主启动类添加 开启远程调用

 修改com.atguigu.gulimall.search.feign.ProducteFeignService类,代码如下

  1. package com.atguigu.gulimall.search.feign;
  2. import com.atguigu.common.utils.R;
  3. import org.springframework.cloud.openfeign.FeignClient;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.PathVariable;
  6. /**
  7. * @Description: ProducteFeignService
  8. * @Author: WangTianShun
  9. * @Date: 2020/11/15 21:27
  10. * @Version 1.0
  11. */
  12. @FeignClient("gulimall-product")
  13. public interface ProducteFeignService {
  14. @GetMapping("/product/attr/info/{attrId}")
  15. public R attrInfo(@PathVariable("attrId") Long attrId);
  16. }

修改com.atguigu.common.utils.R类,代码如下

  1. //利用fastJson进行逆转
  2. public <T> T getData(String key,TypeReference<T> typeReference) {
  3. Object data = get(key);
  4. String s = JSON.toJSONString(data);
  5. T t = JSON.parseObject(s, typeReference);
  6. return t;
  7. }

修改“com.atguigu.gulimall.search.feign.ProducteFeignService” 类,代码如下:

  1. package com.atguigu.gulimall.search.vo;
  2. import lombok.Data;
  3. /**
  4. * @Description: AttrResponseVo
  5. * @Author: WangTianShun
  6. * @Date: 2020/11/15 21:36
  7. * @Version 1.0
  8. */
  9. @Data
  10. public class AttrResponseVo {
  11. /**
  12. * 属性id
  13. */
  14. private Long attrId;
  15. /**
  16. * 属性名
  17. */
  18. private String attrName;
  19. /**
  20. * 是否需要检索[0-不需要,1-需要]
  21. */
  22. private Integer searchType;
  23. /**
  24. * 属性图标
  25. */
  26. private String icon;
  27. /**
  28. * 可选值列表[用逗号分隔]
  29. */
  30. private String valueSelect;
  31. /**
  32. * 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
  33. */
  34. private Integer attrType;
  35. /**
  36. * 启用状态[0 - 禁用,1 - 启用]
  37. */
  38. private Long enable;
  39. /**
  40. * 所属分类
  41. */
  42. private Long catelogId;
  43. /**
  44. * 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
  45. */
  46. private Integer showDesc;
  47. private Long attrGroupId;
  48. private String catelogName;
  49. private String groupName;
  50. private Long[] catelogPath;
  51. }

修改“com.atguigu.gulimall.search.vo.SearchResult” 类,代码如下:

  1. package com.atguigu.gulimall.search.vo;
  2. import com.atguigu.common.to.es.SkuEsModel;
  3. import lombok.Data;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. /**
  7. * @Description: SearchResponse
  8. * @Author: WangTianShun
  9. * @Date: 2020/11/12 16:49
  10. * @Version 1.0
  11. */
  12. @Data
  13. public class SearchResult {
  14. /**
  15. * 查询到的商品信息
  16. */
  17. private List<SkuEsModel> products;
  18. private Integer pageNum;//当前页码
  19. private Long total;//总记录数
  20. private Integer totalPages;//总页码
  21. private List<Integer> pageNavs;//导航页码
  22. private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
  23. private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
  24. private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
  25. //=====================以上是返给页面的信息==========================
  26. //面包屑导航数据
  27. private List<NavVo> navs = new ArrayList<>();
  28. @Data
  29. public static class NavVo{
  30. private String navName;
  31. private String navValue;
  32. private String link;
  33. }
  34. @Data
  35. public static class BrandVo{
  36. private Long brandId;
  37. private String brandName;
  38. private String brandImg;
  39. }
  40. @Data
  41. public static class CatalogVo{
  42. private Long catalogId;
  43. private String catalogName;
  44. private String brandImg;
  45. }
  46. @Data
  47. public static class AttrVo{
  48. private Long attrId;
  49. private String attrName;
  50. private List<String> attrValue;
  51. }
  52. }

修改“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl” 类,代码如下: 

在封装结果时,将查询的属性值进行封装

  1. //6、构建面包屑导航功能
  2. List<String> attrs = param.getAttrs();
  3. if (attrs != null && attrs.size() > 0) {
  4. List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
  5. String[] split = attr.split("_");
  6. SearchResult.NavVo navVo = new SearchResult.NavVo();
  7. //6.1 设置属性值
  8. navVo.setNavValue(split[1]);
  9. //6.2 查询并设置属性名
  10. try {
  11. R r = producteFeignService.attrInfo(Long.parseLong(split[0]));
  12. if (r.getCode() == 0) {
  13. AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
  14. });
  15. navVo.setNavName(attrResponseVo.getAttrName());
  16. }else {
  17. navVo.setNavName(split[0]);
  18. }
  19. } catch (Exception e) {
  20. }
  21. //6.3 设置面包屑跳转链接(当点击该链接时剔除点击属性)
  22. //取消了这个面包屑以后,我们就要跳转到那个地方,将请求地址的url里面的当前置空
  23. //拿到所有的查询条件,去掉当前
  24. String queryString = param.get_queryString();
  25. String encode = null;
  26. try {
  27. encode = URLEncoder.encode(attr, "UTF-8");
  28. encode = encode.replace("+","20%");//浏览器对空格编码和java不一样
  29. } catch (UnsupportedEncodingException e) {
  30. e.printStackTrace();
  31. }
  32. String replace = queryString.replace("&attrs=" + encode, "");
  33. navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
  34. return navVo;
  35. }).collect(Collectors.toList());
  36. result.setNavs(navVos);
  37. }
  38. return result;

 因为远程调用可以给他添加缓存

页面渲染

  1. <div class="JD_ipone_one c">
  2. <!-- 遍历面包屑功能 -->
  3. <a th:href="${nav.link}" th:each="nav:${result.navs}"><span th:text="${nav.navName}"></span><span th:text="${nav.navValue}"></span> x</a>
  4. </div>

7) 条件筛选联动

就是将品牌和分类也封装进面包屑数据中,并且在页面进行th:if的判断,当url有该属性的查询条件时就不进行显示了

修改“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl”类,代码如下:

  1. //6、构建面包屑导航功能
  2. List<String> attrs = param.getAttrs();
  3. if (attrs != null && attrs.size() > 0) {
  4. List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
  5. String[] split = attr.split("_");
  6. SearchResult.NavVo navVo = new SearchResult.NavVo();
  7. //6.1 设置属性值
  8. navVo.setNavValue(split[1]);
  9. //6.2 查询并设置属性名
  10. try {
  11. R r = producteFeignService.attrInfo(Long.parseLong(split[0]));
  12. result.getAttrIds().add(Long.valueOf(split[0]));
  13. if (r.getCode() == 0) {
  14. AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
  15. });
  16. navVo.setNavName(attrResponseVo.getAttrName());
  17. }else {
  18. navVo.setNavName(split[0]);
  19. }
  20. } catch (Exception e) {
  21. }
  22. //6.3 设置面包屑跳转链接(当点击该链接时剔除点击属性)
  23. //取消了这个面包屑以后,我们就要跳转到那个地方,将请求地址的url里面的当前置空
  24. //拿到所有的查询条件,去掉当前
  25. String replace = replaceQueryString(param, attr,"attrs");
  26. navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
  27. return navVo;
  28. }).collect(Collectors.toList());
  29. result.setNavs(navVos);
  30. }
  31. //品牌、分类
  32. if (null != param.getBrandId() && param.getBrandId().size() > 0){
  33. List<SearchResult.NavVo> navs = result.getNavs();
  34. SearchResult.NavVo navVo = new SearchResult.NavVo();
  35. navVo.setNavName("品牌");
  36. //TODO 远程查询所有品牌
  37. R r = producteFeignService.brandInfo(param.getBrandId());
  38. if (0 == r.getCode()){
  39. List<BrandVo> brands = r.getData("brands", new TypeReference<List<BrandVo>>() {
  40. });
  41. StringBuffer buffer = new StringBuffer();
  42. String replace = "";
  43. for (BrandVo brand : brands) {
  44. buffer.append(brand.getBrandName()+";");
  45. replace = replaceQueryString(param, brand.getBrandId()+"","attrs");
  46. }
  47. navVo.setNavValue(buffer.toString());
  48. navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
  49. }
  50. navs.add(navVo);
  51. }
  52. //TODO 分类,不需要导航取消
  53. return result;
  54. }
  55. private String replaceQueryString(SearchParam param, String value,String key) {
  56. String queryString = param.get_queryString();
  57. String encode = null;
  58. try {
  59. encode = URLEncoder.encode(value, "UTF-8");
  60. encode = encode.replace("+","20%");//浏览器对空格编码和java不一样
  61. } catch (UnsupportedEncodingException e) {
  62. e.printStackTrace();
  63. }
  64. return queryString.replace("&"+key+"=" + encode, "");
  65. }

 attrIds作为记录url已经有的属性id

远程调用获取品牌信息

修改“com.atguigu.gulimall.search.feign.ProducteFeignService”类,代码如下:

  1. @RequestMapping("/product/brand/infos")
  2. public R brandInfo(@RequestParam("brandIds") List<Long> brandIds);

修改“com.atguigu.gulimall.search.vo.BrandVo”类,代码如下:

  1. package com.atguigu.gulimall.search.vo;
  2. import lombok.Data;
  3. /**
  4. * @author WangTianShun
  5. * @date 2020/10/15 10:00
  6. */
  7. @Data
  8. public class BrandVo {
  9. private Long brandId;
  10. private String brandName;
  11. }

修改“com.atguigu.gulimall.product.app.BrandController”类,代码如下:

  1. /**
  2. * 信息
  3. */
  4. @RequestMapping("/infos")
  5. public R info(@RequestParam("brandIds") List<Long> brandIds){
  6. List<BrandEntity> brands = brandService.getBrandsById(brandIds);
  7. return R.ok().put("brands", brands);
  8. }

修改“com.atguigu.gulimall.product.service.BrandService”类,代码如下:

List<BrandEntity> getBrandsById(List<Long> brandIds);

修改“com.atguigu.gulimall.product.service.impl.BrandServiceImpl”类,代码如下:

  1. @Override
  2. public List<BrandEntity> getBrandsById(List<Long> brandIds) {
  3. List<BrandEntity> brandId = baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id", brandIds));
  4. return brandId;
  5. }

  1. <!--品牌-->
  2. <div th:if="${#strings.isEmpty(brandid)}" class="JD_nav_wrap">
  3. <div class="sl_key">
  4. <span><b>品牌:</b></span>
  5. </div>
  6. <div class="sl_value">
  7. <div class="sl_value_logo">
  8. <ul>
  9. <li th:each="brand:${result.brands}">
  10. <a href="/static/search/#"
  11. th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}">
  12. <img th:src="${brand.brandImg}" alt="">
  13. <div th:text="${brand.brandName}">
  14. 华为(HUAWEI)
  15. </div>
  16. </a>
  17. </li>
  18. </ul>
  19. </div>
  20. </div>
  21. <div class="sl_ext">
  22. <a href="/static/search/#">
  23. 更多
  24. <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
  25. <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
  26. </a>
  27. <a href="/static/search/#">
  28. 多选
  29. <i>+</i>
  30. <span>+</span>
  31. </a>
  32. </div>
  33. </div>
  34. <!--分类-->
  35. <div class="JD_nav_wrap">
  36. <div class="sl_key">
  37. <span><b>分类:</b></span>
  38. </div>
  39. <div class="sl_value">
  40. <ul>
  41. <li th:each="catalog:${result.catalogs}">
  42. <a href="/static/search/#"
  43. th:href="${'javascript:searchProducts(&quot;catalog3&quot;,'+catalog.catalogId+')'}"
  44. th:text="${catalog.catalogName}">5.56英寸及以上</a>
  45. </li>
  46. </ul>
  47. </div>
  48. <div class="sl_ext">
  49. <a href="/static/search/#">
  50. 更多
  51. <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
  52. <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
  53. </a>
  54. <a href="/static/search/#">
  55. 多选
  56. <i>+</i>
  57. <span>+</span>
  58. </a>
  59. </div>
  60. </div>
  61. <!--其他所有需要展示的属性-->
  62. <div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">
  63. <div class="sl_key">
  64. <span th:text="${attr.attrName}">屏幕尺寸:</span>
  65. </div>
  66. <div class="sl_value">
  67. <ul>
  68. <li th:each="val:${attr.attrValue}">
  69. <a href="/static/search/#"
  70. th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
  71. th:text="${val}">5.56英寸及以上</a></li>
  72. </ul>
  73. </div>
  74. </div>

五、异步

1、继承Thread

2、实现Runnable

3、实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)

4、线程池

  1. package com.atguigu.gulimall.search.thread;
  2. import java.util.concurrent.*;
  3. /**
  4. * @Description: ThreadTest
  5. * @Author: WangTianShun
  6. * @Date: 2020/11/16 8:50
  7. * @Version 1.0
  8. */
  9. public class ThreadTest {
  10. public static ExecutorService service = Executors.newFixedThreadPool(10);
  11. public static void main(String[] args) throws ExecutionException, InterruptedException {
  12. System.out.println("main.............start.......");
  13. /**
  14. * 1、继承Thread
  15. *
  16. * 2、实现Runnable
  17. *
  18. * 3、实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
  19. *
  20. * 4、线程池
  21. * 给线程池直接提交任务
  22. * service.execute(new Runnable01());
  23. * 创建
  24. * 1)、Executors
  25. *
  26. *总结:
  27. * 我们以后再业务代码里面,以上三种启动线程的方式都不用。将所有的多线程异步任务都交给线程池执行
  28. *
  29. * 区别:
  30. * 1、2不能得到返回值。3可以获取返回值
  31. * 1、2、3都不能控制资源
  32. * 4可以控制资源,性能稳定。
  33. */
  34. //当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
  35. // 1、继承Thread
  36. // Thread01 thread01 = new Thread01();
  37. // thread01.start();//启动线程
  38. // 2、实现Runnable
  39. // Runnable01 runnable01 = new Runnable01();
  40. // new Thread(runnable01).start();
  41. // 3、实现Callable接口+FutureTask
  42. FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
  43. new Thread(futureTask).start();
  44. //阻塞等待整个线程执行完成,获取返回结果
  45. Integer integer = futureTask.get();
  46. // 4、线程池
  47. service.execute(new Runnable01());
  48. System.out.println("main.............end......."+integer);
  49. }
  50. public static class Thread01 extends Thread{
  51. @Override
  52. public void run() {
  53. System.out.println("当前线程:"+Thread.currentThread().getId());
  54. int i = 10/2;
  55. System.out.println("当前运行结果:" + i);
  56. }
  57. }
  58. public static class Runnable01 implements Runnable{
  59. @Override
  60. public void run() {
  61. System.out.println("当前线程:"+Thread.currentThread().getId());
  62. int i = 10/2;
  63. System.out.println("当前运行结果:" + i);
  64. }
  65. }
  66. public static class Callable01 implements Callable<Integer>{
  67. @Override
  68. public Integer call() throws Exception {
  69. System.out.println("当前线程:"+Thread.currentThread().getId());
  70. int i = 10/2;
  71. System.out.println("当前运行结果:" + i);
  72. return i;
  73. }
  74. }
  75. }

1、线程池

1)、七大参数

(1)、corePoolSize[5],核心线程数[一直存在,除非(allowCoreThreadTimeOut)];线程池,创建好以后就准备就绪的线程数量,就等待接收异步任务去执行

                       5个 Thread thread = new Thread(); thread.start();

(2)、maximumPoolSize[200]:最大线程数量;控制资源

(3)、keepAliveTime:存活时间。如果当前正在运行的线程数量大于core数量

                    释放空闲的线程(maximumPoolSize-corePoolSize)。只要线程空闲大于指定的keepAliveTime;

(4)、unit:时间单位

(5)、BlockingQueue<Runnable> workQueue:阻塞队列。如果任务有很多。就会将多的任务放在队列里面

                   只要有线程空闲,就会去队列里面取出新的任务继续执行

(6)、ThreadFactory:线程创建的工厂

(7)、RejectedExecutionHandler handler:如果队列满了,按照我们指定的拒绝策略,拒绝执行

2)、工作顺序

1)、线程池创建,准备好core数量的核心线程,准备接受任务

        1.1、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行

       1.2、阻塞队垒满了,就直接开新线程执行,最大只能开到max指定的数量

       1.3、max满了就用RejectedExecutionHandler拒绝任务

       1.4、max都执行完成,有很多空闲,在指定的时间keepAliveTime以后,释放空闲的线程(max-core)。

               new LinkedBlockingQueue<>(),默认是Integer的最大值。内存不够

一个线程池 core 7, max 20, queue 50, 100并发进来怎么分配的

       7个会立即执行。50个进入队列,在开13个进行执行。剩下30个就是用拒绝策略

如果不想抛弃还要执行,CallerRunsPolicy

  1. ThreadPoolExecutor executor = new ThreadPoolExecutor(
  2. 5,
  3. 200,
  4. 10,
  5. TimeUnit.SECONDS,
  6. new LinkedBlockingQueue<>(100000),
  7. Executors.defaultThreadFactory(),
  8. new ThreadPoolExecutor.AbortPolicy());

3)、常见的4种线程池

newCachedThreadPool

创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在对垒中等待。

newScheduledThreadPool

创建一个定长线程池。支持定势及周期性人去执行

newSingleThreadExecutor

创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务。

4)、使用线程池的好处

2、CompletableFuture组合式异步编程

1)、创建异步对象

(1) runAsync 和 supplyAsync方法

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

  1. public static CompletableFuture<Void> runAsync(Runnable runnable)
  2. public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
  3. public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
  4. public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。

  • runAsync方法不支持返回值。
  • supplyAsync可以支持返回值。
  1. public static void main(String[] args) throws ExecutionException, InterruptedException {
  2. System.out.println("main.............start.......");
  3. // CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
  4. // System.out.println("当前线程:" + Thread.currentThread().getId());
  5. // int i = 10 / 2;
  6. // System.out.println("当前运行结果:" + i);
  7. // }, executor);
  8. CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
  9. System.out.println("当前线程:" + Thread.currentThread().getId());
  10. int i = 10 / 2;
  11. System.out.println("当前运行结果:" + i);
  12. return i;
  13. }, executor);
  14. Integer integer = future.get();
  15. System.out.println("main.............end......."+integer);
  16. }

2)、计算结果完成时的回调方法

当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:

  1. //可以处理异常,无返回值
  2. public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
  3. public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
  4. public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
  5. //可以处理异常,有返回值
  6. public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

可以看到Action的类型是BiConsumer<? super T,? super Throwable>它可以处理正常的计算结果,或者异常情况。

  1. public class ThreadTest {
  2. public static ExecutorService executor = Executors.newFixedThreadPool(10);
  3. public static void main(String[] args) throws ExecutionException, InterruptedException {
  4. System.out.println("main.............start.......");
  5. // CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
  6. // System.out.println("当前线程:" + Thread.currentThread().getId());
  7. // int i = 10 / 2;
  8. // System.out.println("当前运行结果:" + i);
  9. // }, executor);
  10. CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
  11. System.out.println("当前线程:" + Thread.currentThread().getId());
  12. int i = 10 / 0;
  13. System.out.println("当前运行结果:" + i);
  14. return i;
  15. }, executor).whenComplete((result,exception)->{
  16. //虽然能得到异常信息,但是没法修改返回数据
  17. System.out.println("异步任务完成了...结果是"+result+";异常是"+exception);
  18. //可以感知异常,同时返回默认值
  19. }).exceptionally(throwable -> {
  20. return 10;
  21. });
  22. Integer integer = future.get();
  23. System.out.println("main.............end......."+integer);
  24. }

3)、handle 方法

handle 是执行任务完成时对结果的处理。 handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。

  1. public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
  2. public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
  3. public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Execut
  1. /**
  2. * 方法执行完成后的处理
  3. */
  4. CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
  5. System.out.println("当前线程:" + Thread.currentThread().getId());
  6. int i = 10 / 5;
  7. System.out.println("当前运行结果:" + i);
  8. return i;
  9. }, executor).handle((result,exception)->{
  10. if (result != null){
  11. return result*2;
  12. }
  13. if (exception != null){
  14. return 0;
  15. }
  16. return 0;
  17. });
  18. Integer integer = future.get();
  19. System.out.println("main.............end......."+integer);
  20. }

4)、线程串行化

  • thenRun:不能获取上一步的执行结果
  • thenAcceptAsync:能接受上一步结果,但是无返回值
  • thenApplyAsync:能接受上一步结果,有返回值
  1. /**
  2. * 线程串行化
  3. * 1)、thenRun 不能获取得到上一步的执行结果
  4. */
  5. // CompletableFuture<Void> thenRunAsync = CompletableFuture.supplyAsync(() -> {
  6. // System.out.println("当前线程:" + Thread.currentThread().getId());
  7. // int i = 10 / 5;
  8. // System.out.println("当前运行结果:" + i);
  9. // return i;
  10. // }, executor).thenRunAsync(() -> {
  11. // System.out.println("任务2启动了。。。。");
  12. // }, executor);
  13. /**
  14. * 线程串行化
  15. * 1)、thenRun 不能获取得到上一步的执行结果,无返回值
  16. * 2)、thenAcceptAsync能接收上一步返回结果,但无返回值
  17. */
  18. // CompletableFuture<Void> thenRunAsync = CompletableFuture.supplyAsync(() -> {
  19. // System.out.println("当前线程:" + Thread.currentThread().getId());
  20. // int i = 10 / 5;
  21. // System.out.println("当前运行结果:" + i);
  22. // return i;
  23. // }, executor).thenAcceptAsync(res -> {
  24. // System.out.println("任务2启动了。。。。"+res);
  25. // }, executor);
  26. /**
  27. * 线程串行化
  28. * 1)、thenRun 不能获取得到上一步的执行结果,无返回值
  29. * 2)、thenAcceptAsync能接收上一步返回结果,但无返回值
  30. * 3)、thenApplyAsync能接收上一步的返回结果,也有返回值
  31. */
  32. CompletableFuture<String> thenApplyAsync = CompletableFuture.supplyAsync(() -> {
  33. System.out.println("当前线程:" + Thread.currentThread().getId());
  34. int i = 10 / 5;
  35. System.out.println("当前运行结果:" + i);
  36. return i;
  37. }, executor).thenApplyAsync(res -> {
  38. System.out.println("任务2启动了。。。。" + res);
  39. return "hello" + res;
  40. }, executor);
  41. System.out.println("main.............end......."+ thenApplyAsync.get());
  42. }

5)、两任务组合-都要完成

thenCombine 会把两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。

  • runAfterBoth

两个CompletionStage,都完成了计算才会执行下一步的操作(Runnable)

  1. public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable action);
  2. public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action);
  3. public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor
  • thenAcceptBoth

当两个CompletionStage都执行完成后,把结果一块交给thenAcceptBoth来进行消耗

  1. public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
  2. public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
  3. public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action, Executor executor);
  • thenCombine

thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。

  1. public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
  2. public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
  3. public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
  1. /**
  2. * 不能得到两个任务的参数,也无返回结果
  3. */
  4. // future01.runAfterBothAsync(future02,()->{
  5. // System.out.println("任务三开始。。。");
  6. // },executor);
  7. /**
  8. * 能得到两个任务的参数,无返回结果
  9. */
  10. // future01.thenAcceptBothAsync(future02,(f1,f2)->{
  11. // System.out.println("任务三开始。。。之前的结果"+f1+":"+f2);
  12. // },executor);
  13. /**
  14. * 能得到两个任务的参数,无返回结果
  15. */
  16. CompletableFuture<String> thenCombineAsync = future01.thenCombineAsync(future02, (f1, f2) -> {
  17. System.out.println("任务三开始。。。之前的结果" + f1 + ":" + f2);
  18. return f1 + ":" + f2 + "->haha";
  19. }, executor);
  20. System.out.println("main.............end......." + thenCombineAsync.get());
  21. }

6)、两任务组合-只要有一个任务完成就执行第三个

  • runAfterEither 方法

两个CompletionStage,任何一个完成了都会执行下一步的操作(Runnable)

  1. public CompletionStage<Void> runAfterEither(CompletionStage<?> other,Runnable action);
  2. public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action);
  3. public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);
  • acceptEither 方法

两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的消耗操作。

  1. public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action);
  2. public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action);
  3. public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? supe
  • applyToEither 方法

两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的转化操作。

  1. public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn);
  2. public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn);
  3. public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? sup
  1. CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
  2. System.out.println("任务1线程:" + Thread.currentThread().getId());
  3. int i = 10 / 5;
  4. System.out.println("任务1线程结束");
  5. return i;
  6. }, executor);
  7. CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
  8. System.out.println("任务2线程:" + Thread.currentThread().getId());
  9. try {
  10. Thread.sleep(3000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. System.out.println("任务2线程结束");
  15. return "hello";
  16. }, executor);
  • thenCompose 方法

thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。

  1. public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
  2. public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
  3. public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage

7)、多任务组合

  • allOf
  1. CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
  2. System.out.println("查询商品的属性");
  3. return "黑色+256g";
  4. },executor);
  5. CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
  6. try {
  7. Thread.sleep(3000);
  8. System.out.println("查询商品的图片信息");
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. return "hello.jpg";
  13. },executor);
  14. CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
  15. System.out.println("查询商品的介绍");
  16. return "华为";
  17. },executor);
  18. CompletableFuture<Void> allOf = CompletableFuture.allOf(futureAttr, futureImg, futureDesc);
  19. allOf.get();//等待所有线程执行完
  20. System.out.println("main.............end......."+futureAttr.get()+"=>"+futureImg.get()+"=>"+futureDesc.get() );
  21. }

  • anyOf
  1. CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
  2. System.out.println("查询商品的属性");
  3. return "黑色+256g";
  4. },executor);
  5. CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
  6. try {
  7. Thread.sleep(3000);
  8. System.out.println("查询商品的图片信息");
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. return "hello.jpg";
  13. },executor);
  14. CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
  15. System.out.println("查询商品的介绍");
  16. return "华为";
  17. },executor);
  18. // CompletableFuture<Void> allOf = CompletableFuture.allOf(futureAttr, futureImg, futureDesc);
  19. // allOf.get();//等待所有线程执行完
  20. CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureAttr, futureImg, futureDesc);
  21. System.out.println("main.............end......."+anyOf.get() );

六、商品详情


1、搭建好域名跳转环境

1、用记事本打开C:\Windows\System32\drivers\etc\host(记得先把属性的只读模式去掉)

2、配置Nginx

之前配置了*.gulimall.com可以匹配上item.gulimall.com

3、改网关

4、把详情页的html导入gulimall-product项目里

5、给虚拟机的nginx传入详情页的静态资源(static里再创建一个文件夹item)

 6、把item.html的路径改了(ctrl+r)

添加“com.atguigu.gulimall.product.web.ItemController”类,代码如下:

  1. package com.atguigu.gulimall.product.web;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.PathVariable;
  5. /**
  6. * @Description: ItemController
  7. * @Author: WangTianShun
  8. * @Date: 2020/11/16 15:31
  9. * @Version 1.0
  10. */
  11. @Controller
  12. public class ItemController {
  13. /**
  14. * 展示当前sku的详情
  15. * @param skuId
  16. * @return
  17. */
  18. @GetMapping("/{skuId}.html")
  19. public String skuItem(@PathVariable("skuId") Long skuId){
  20. System.out.println("准备查询"+skuId+"的详情");
  21. return "item";
  22. }
  23. }

修改gulimall-search的list.html

  1. <p class="da">
  2. <a th:href="|http://item.gulimall.com/${product.skuId}.html|">
  3. <img th:src="${product.skuImg}" class="dim">
  4. </a>
  5. </p>

测试效果

2、模型抽取

模仿京东商品详情页,如下图所示,包括sku基本信息,图片信息,销售属性,图片介绍和规格参数

因此建立以下vo

添加“com.atguigu.gulimall.product.vo.SkuItemVo”类,代码如下:

  1. package com.atguigu.gulimall.product.vo;
  2. import com.atguigu.gulimall.product.entity.SkuImagesEntity;
  3. import com.atguigu.gulimall.product.entity.SkuInfoEntity;
  4. import com.atguigu.gulimall.product.entity.SpuInfoDescEntity;
  5. import lombok.Data;
  6. import java.util.List;
  7. /**
  8. * @Description: ItemVo
  9. * @Author: WangTianShun
  10. * @Date: 2020/11/16 16:20
  11. * @Version 1.0
  12. */
  13. @Data
  14. public class SkuItemVo {
  15. //1、sku基本信息获取 pms_sku_info
  16. SkuInfoEntity info;
  17. //2、sku的图片信息 pms_sku_images
  18. List<SkuImagesEntity> images;
  19. //3、获取spu的销售属性组合
  20. List<SkuItemSaleAttrVo> saleAttr;
  21. //4、获取spu的介绍
  22. SpuInfoDescEntity desc;
  23. //5、获取spu的规格参数信息
  24. List<SpuItemAttrGroupVo> groupAttrs;
  25. }

 添加“com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo”类,代码如下:

  1. package com.atguigu.gulimall.product.vo;
  2. import lombok.Data;
  3. import java.util.List;
  4. /**
  5. * @Description: SkuItemSaleAttrVo
  6. * @Author: WangTianShun
  7. * @Date: 2020/11/16 21:05
  8. * @Version 1.0
  9. */
  10. @Data
  11. public class SkuItemSaleAttrVo {
  12. private Long attrId;
  13. private String attrName;
  14. private String attrValues;
  15. }

 添加“com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo”类,代码如下:

  1. package com.atguigu.gulimall.product.vo;
  2. import lombok.Data;
  3. import lombok.ToString;
  4. import java.util.List;
  5. /**
  6. * @Description: SpuItemAttrGroupVo
  7. * @Author: WangTianShun
  8. * @Date: 2020/11/16 21:02
  9. * @Version 1.0
  10. */
  11. @ToString
  12. @Data
  13. public class SpuItemAttrGroupVo {
  14. private String groupName;
  15. private List<Attr> attrs;
  16. }

3、 封装商品属性

(1) 总体思路

修改“com.atguigu.gulimall.product.web.ItemController”类,代码如下:

  1. @Controller
  2. public class ItemController {
  3. @Autowired
  4. SkuInfoService skuInfoService;
  5. /**
  6. * 展示当前sku的详情
  7. * @param skuId
  8. * @return
  9. */
  10. @GetMapping("/{skuId}.html")
  11. public String skuItem(@PathVariable("skuId") Long skuId, Model model) {
  12. SkuItemVo skuItemVo = skuInfoService.item(skuId);
  13. model.addAttribute("item", skuItemVo);
  14. return "item";
  15. }

 修改“com.atguigu.gulimall.product.service.SkuInfoService”类,代码如下:

SkuItemVo item(Long skuId);

 修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

  1. @Override
  2. public SkuItemVo item(Long skuId) {
  3. SkuItemVo skuItemVo = new SkuItemVo();
  4. //1、sku基本信息获取 pms_sku_info
  5. SkuInfoEntity info = getById(skuId);
  6. skuItemVo.setInfo(info);
  7. Long catalogId = info.getCatalogId();
  8. Long spuId = info.getSpuId();
  9. //2、sku的图片信息 pms_sku_images
  10. List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
  11. skuItemVo.setImages(images);
  12. //3、获取spu的销售属性组合
  13. List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
  14. skuItemVo.setSaleAttr(saleAttrVos);
  15. //4、获取spu的介绍 pms_spu_info_desc
  16. SpuInfoDescEntity desc = spuInfoDescService.getById(spuId);
  17. skuItemVo.setDesc(desc);
  18. //5、获取spu的规格参数信息
  19. List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
  20. skuItemVo.setGroupAttrs(attrGroupVos);
  21. return skuItemVo;
  22. }

(2) 获取spu的图片信息

 修改“com.atguigu.gulimall.product.service.SkuImagesService”类,代码如下:

List<SkuImagesEntity> getImagesBySkuId(Long skuId);

 修改“com.atguigu.gulimall.product.service.impl.SkuImagesServiceImpl”类,代码如下:

  1. @Override
  2. public List<SkuImagesEntity> getImagesBySkuId(Long skuId) {
  3. List<SkuImagesEntity> imagesEntities = this.baseMapper.selectList(new QueryWrapper<SkuImagesEntity>().eq("sku_id", skuId));
  4. return imagesEntities;
  5. }

(3) 获取spu的销售属性

由于我们需要获取该spu下所有sku的销售属性,因此我们需要先从pms_sku_info查出该spuId对应的skuId

pms_sku_sale_attr_value表中查出上述skuId对应的属性

 ​

因此我们需要使用连表查询,并且通过分组将单个属性值对应的多个spuId组成集合,效果如下

 修改“com.atguigu.gulimall.product.service.SkuSaleAttrValueService”类,代码如下:

List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId);

  修改“com.atguigu.gulimall.product.service.impl.SkuSaleAttrValueServiceImpl”类,代码如下:

  1. @Override
  2. public List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {
  3. SkuSaleAttrValueDao dao = this.baseMapper;
  4. List<SkuItemSaleAttrVo> saleAttrVos = dao.getSaleAttrsBySpuId(spuId);
  5. return saleAttrVos;
  6. }

  修改“com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao”类,代码如下:

List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);

修改SkuSaleAttrValueDao.xml

  1. <select id="getSaleAttrsBySpuId" resultType="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
  2. SELECT ssav.attr_id attr_id, ssav.attr_name attr_name, GROUP_CONCAT(DISTINCT ssav.attr_value) attr_values FROM pms_sku_info info
  3. LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
  4. WHERE info.spu_id=#{spuId}
  5. GROUP BY ssav.attr_id,ssav.attr_name
  6. </select>

(5) 获取spu的规格参数信息

由于需要通过spuIdcatalogId查询对应规格参数,所以我们需要通过pms_attr_group表获得catalogIdattrGroupName

 ​

然后通过 pms_attr_attrgroup_relation获取分组对应属性id

再到 pms_product_attr_value查询spuId对应的属性

 ​

最终sql效果,联表含有需要的所有属性

  修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:

List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId);

   修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:

  1. @Override
  2. public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {
  3. //1、查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
  4. //1.1、
  5. AttrGroupDao baseMapper = this.getBaseMapper();
  6. List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
  7. return vos;
  8. }

    修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:

  1. @Mapper
  2. public interface AttrGroupDao extends BaseMapper<AttrGroupEntity> {
  3. List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
  4. }

修改AttrGroupDao.xml

  1. <!--resultType 返回集合里面元素的类型,只要有嵌套属性就要封装自定义结果集-->
  2. <resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo">
  3. <result property="groupName" column="attr_group_name"/>
  4. <collection property="attrs" ofType="com.atguigu.gulimall.product.vo.Attr">
  5. <result property="attrName" column="attr_name"/>
  6. <result property="attrValue" column="attr_value"/>
  7. </collection>
  8. </resultMap>
  9. <select id="getAttrGroupWithAttrsBySpuId" resultMap="spuItemAttrGroupVo">
  10. SELECT pav.spu_id, ag.attr_group_name, ag.attr_group_id, aar.attr_id, attr.attr_name, pav.attr_value
  11. FROM pms_attr_group ag
  12. LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
  13. LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
  14. LEFT JOIN pms_product_attr_value pav ON pav.attr_id = attr.attr_id
  15. WHERE ag.catelog_id = 225 AND pav.spu_id = 2
  16. </select>

3、页面渲染

1)、添加thymeleaf的名称空间

<html lang="en" xmlns:th="http://www.thymeleaf.org">
  1. <div class="box-yuyue">
  2. <div class="yuyue-one">
  3. <img src="/static/item/img/7270ffc3baecdd448958f9f5e69cf60f.png" alt="" /> 预约抢购
  4. </div>
  5. <div class="yuyue-two">
  6. <ul>
  7. <li>
  8. <img src="/static/item/img/f64963b63d6e5849977ddd6afddc1db5.png" />
  9. <span>190103</span> 人预约
  10. </li>
  11. <li>
  12. <img src="/static/item/img/36860afb69afa241beeb33ae86678093.png" /> 预约剩余
  13. <span id="timer">
  14. </span>
  15. </li>
  16. </ul>
  17. </div>
  18. </div>
  19. <div class="box-summary clear">
  20. <ul>
  21. <li>京东价</li>
  22. <li>
  23. <span></span>
  24. <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
  25. </li>
  26. <li>
  27. 预约享资格
  28. </li>
  29. <li>
  30. <a href="/static/item/">
  31. 预约说明
  32. </a>
  33. </li>
  34. </ul>
  35. </div>

  1. <li>
  2. <span th:text="${item.hasStock ? '有货' : '无货'}">无货</span>, 此商品暂时售完
  3. </li>
  1. <div class="imgbox">
  2. <div class="probox">
  3. <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
  4. <div class="hoverbox"></div>
  5. </div>
  6. <div class="showbox">
  7. <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
  8. </div>
  9. </div>
  10. <div class="box-lh">
  11. <div class="box-lh-one">
  12. <ul>
  13. <li th:each="img:${item.images}"th:if="${!#strings.isEmpty(img.imgUrl)}"><img th:src="${img.imgUrl}" /></li>
  14. </ul>
  15. </div>
  16. <div id="left">
  17. < </div>
  18. <div id="right">
  19. >
  20. </div>
  21. </div>
  22. <div class="boxx-one">
  23. <ul>
  24. <li>
  25. <span>
  26. <img src="/static/item/img/b769782fe4ecca40913ad375a71cb92d.png" alt="" />关注
  27. </span>
  28. <span>
  29. <img src="/static/item/img/9224fcea62bfff479a6712ba3a6b47cc.png" alt="" />
  30. 对比
  31. </span>
  32. </li>
  33. <li>
  34. </li>
  35. </ul>
  36. </div>
  37. </div>
  1. <div class="box-attr clear" th:each="attr:${item.saleAttr}">
  2. <dl>
  3. <dt>选择[[${attr.attrName}]]</dt>
  4. <dd th:each="val:${#strings.listSplit(attr.attrValues,',')}">
  5. <a href="/static/item/#">
  6. [[${val}]]
  7. <!-- <img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
  8. </a>
  9. </dd>
  10. </dl>
  11. </div>
<img class="xiaoguo" th:src="${descp}" th:each="descp:${#strings.listSplit(item.desc.decript,',')}"/>
  1. <!--商品介绍-->
  2. <div class="allquanbushop">
  3. <ul class="shopjieshao">
  4. <li class="jieshoa" style="background: #e4393c;">
  5. <a style="color: white;">商品介绍</a>
  6. </li>
  7. <li class="baozhuang">
  8. <a>规格与包装</a>
  9. </li>
  10. <li class="baozhang">
  11. <a href="/static/item/##">售后保障</a>
  12. </li>
  13. <li class="pingjia">
  14. <a href="/static/item/##">商品评价(4万+)</a>
  15. </li>
  16. <li class="shuoming">
  17. <a href="/static/item/##">预约说明</a>
  18. </li>
  19. </ul>
  1. <li class="baozhuang actives" id="li2">
  2. <div class="guiGebox">
  3. <div class="guiGe" th:each="group:${item.groupAttrs}">
  4. <h3 th:text="${group.groupName}">主体</h3>
  5. <dl >
  6. <div th:each="attr:${group.attrs}">
  7. <dt th:text="${attr.attrName}">品牌</dt>
  8. <dd th:text="${attr.attrValue}">华为(HUAWEI)</dd>
  9. </div>
  10. </dl>
  11. </div>
  12. <div class="package-list">
  13. <h3>包装清单</h3>
  14. <p>手机(含内置电池) X 1、5A大电流华为SuperCharge充电器X 1、5A USB数据线 X 1、半入耳式线控耳机 X 1、快速指南X 1、三包凭证 X 1、取卡针 X 1、保护壳 X 1</p>
  15. </div>
  16. </div>
  17. </li>

修改:获取spu的销售属性的逻辑

由于我们需要获取该spu下所有sku的销售属性,因此我们需要先从pms_sku_info查出该spuId对应的skuId

pms_sku_sale_attr_value表中查出上述skuId对应的属性

因此我们需要使用连表查询,并且通过分组将单个属性值对应的多个spuId组成集合,效果如下

为什么要设计成这种模式呢?

因为这样可以在页面显示切换属性时,快速得到对应skuId的值,比如白色对应的sku_ids为30,29,8+128GB对应的sku_ids为29,31,27,那么销售属性为白色、8+128GB的商品的skuId则为二者的交集29

添加“com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo”类,代码如下:

  1. @Data
  2. public class AttrValueWithSkuIdVo {
  3. private String attrValue;
  4. private String skuIds;
  5. }

修改“com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo”类,代码如下:

  1. @Data
  2. public class SkuItemSaleAttrVo {
  3. private Long attrId;
  4. private String attrName;
  5. private List<AttrValueWithSkuIdVo> attrValues;
  6. }

 修改SkuItemSaleAttrVo.xml

  1. <resultMap id="skuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
  2. <result property="attrId" column="attr_id"/>
  3. <result property="attrName" column="attr_name"/>
  4. <collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo">
  5. <result property="attrValue" column="attr_value"/>
  6. <result property="skuIds" column="sku_ids"/>
  7. </collection>
  8. </resultMap>
  9. <select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
  10. SELECT ssav.attr_id attr_id, ssav.attr_name attr_name, ssav.attr_value attr_value, GROUP_CONCAT(DISTINCT info.sku_id) sku_ids FROM pms_sku_info info
  11. LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
  12. WHERE info.spu_id = #{spuId}
  13. GROUP BY ssav.attr_id,ssav.attr_name, ssav.attr_value
  14. </select>

效果如下:

4、页面的sku切换

  1. <script>
  2. $(".sku_attr_value").click(function(){
  3. //1、点击的元素添加上自定义的属性,为了识别我们是刚才被点击的、
  4. var skus = new Array();
  5. $(this).addClass("clicked");
  6. var curr = $(this).attr("skus").split(",");
  7. //当前被点击的所有sku组合数组放进去
  8. skus.push(curr);
  9. //去掉同一行的所有的checked
  10. $(this).parent().parent().find(".sku_attr_value").removeClass("checked");
  11. $("a[class='sku_attr_value checked']").each(function(){
  12. skus.push($(this).attr("skus").split(","));
  13. });
  14. console.log(skus);
  15. //2、取出他们的交集,得到skuId
  16. // console.log($(skus[0]).filter(skus[1])[0]);
  17. var filterEle = skus[0];
  18. for (var i = 1; i < skus.length; i++){
  19. filterEle = $(filterEle).filter(skus[i])
  20. }
  21. console.log(filterEle[0]);
  22. //3、跳转
  23. location.href = "http://item.gulimall.com/"+filterEle[0]+".html";
  24. });
  25. $(function(){
  26. $(".sku_attr_value").parent().css({"border":"solid 1px #ccc"});
  27. $("a[class='sku_attr_value checked']").parent().css({"border":"solid 1px red"});
  28. //方法二
  29. // $(".sku_attr_value").parent().css({"border":"solid 1px #ccc"});
  30. // $(".sku_attr_value.checked").parent().css({"border":"solid 1px red"});
  31. })
  32. </script>

通过控制class中是否包换checked属性来控制显示样式,因此要根据skuId判断

  1. <div class="box-attr-3">
  2. <div class="box-attr clear" th:each="attr:${item.saleAttr}">
  3. <dl>
  4. <dt>选择[[${attr.attrName}]]</dt>
  5. <dd th:each="vals:${attr.attrValues}">
  6. <a
  7. th:attr="skus=${vals.skuIds},class=${#lists.contains(#strings.listSplit(vals.skuIds,','),item.info.skuId.toString())?'sku_attr_value checked' : 'sku_attr_value'}">
  8. [[${vals.attrValue}]]
  9. <!-- <img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
  10. </a>
  11. </dd>
  12. </dl>
  13. </div>

5、使用异步编排

添加线程池属性配置类,并注入到容器中

  1. package com.atguigu.gulimall.product.config;
  2. import lombok.Data;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.stereotype.Component;
  5. /**
  6. * @Description: ThreadPoolConfigProperties
  7. * @Author: WangTianShun
  8. * @Date: 2020/11/17 13:40
  9. * @Version 1.0
  10. */
  11. @ConfigurationProperties(prefix = "gulimall.thread")
  12. @Component
  13. @Data
  14. public class ThreadPoolConfigProperties {
  15. private Integer core;
  16. private Integer maxSize;
  17. private Integer keepAliveTime;
  18. }

添加application.yml

  1. #线程池属性的配置
  2. gulimall:
  3. thread:
  4. core: 20
  5. max-size: 200
  6. keep-alive-time: 10

线程池配置,获取线程池的属性值这里直接调用与配置文件相对应的属性配置类

  1. package com.atguigu.gulimall.product.config;
  2. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.LinkedBlockingQueue;
  7. import java.util.concurrent.ThreadPoolExecutor;
  8. import java.util.concurrent.TimeUnit;
  9. /**
  10. * @Description: MyThreadConfig
  11. * @Author: WangTianShun
  12. * @Date: 2020/11/17 13:31
  13. * @Version 1.0
  14. */
  15. //如果ThreadPoolConfigProperties.class类没有加上@Component注解,那么我们在需要的配置类里开启属性配置的类加到容器中
  16. //@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
  17. @Configuration
  18. public class MyThreadConfig {
  19. @Bean
  20. public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
  21. return new ThreadPoolExecutor(pool.getCore(),
  22. pool.getMaxSize(),
  23. pool.getKeepAliveTime(),
  24. TimeUnit.SECONDS,
  25. new LinkedBlockingQueue<>(100000),
  26. Executors.defaultThreadFactory(),
  27. new ThreadPoolExecutor.AbortPolicy());
  28. }
  29. }

 为了使我们的任务进行的更快,我们可以让查询的各个子任务多线程执行,但是由于各个任务之间可能有相互依赖的关系,因此就涉及到了异步编排。

在这次查询中spu的销售属性、介绍、规格参数信息都需要spuId,因此依赖sku基本信息的获取,所以我们要让这些任务在1之后运行。因为我们需要1运行的结果,因此调用thenAcceptAsync()可以接受上一步的结果且没有返回值。

最后时,我们需要调用get()方法使得所有方法都已经执行完成

  1. @Autowired
  2. ThreadPoolExecutor executor;
  1. @Override
  2. public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
  3. SkuItemVo skuItemVo = new SkuItemVo();
  4. // 使用异步编排
  5. CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
  6. //1、sku基本信息获取 pms_sku_info
  7. SkuInfoEntity info = getById(skuId);
  8. skuItemVo.setInfo(info);
  9. return info;
  10. }, executor);
  11. CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
  12. //3、获取spu的销售属性组合
  13. List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
  14. skuItemVo.setSaleAttr(saleAttrVos);
  15. }, executor);
  16. CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
  17. //4、获取spu的介绍 pms_spu_info_desc
  18. SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
  19. skuItemVo.setDesc(desc);
  20. }, executor);
  21. CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
  22. //5、获取spu的规格参数信息
  23. List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
  24. skuItemVo.setGroupAttrs(attrGroupVos);
  25. }, executor);
  26. CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
  27. //2、sku的图片信息 pms_sku_images
  28. List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
  29. skuItemVo.setImages(images);
  30. }, executor);
  31. //等待所有任务都完成
  32. CompletableFuture.allOf(saleAttrFuture, descFuture, baseAttrFuture, imageFuture).get();
  33. return skuItemVo;
  34. }

未完,请看下一篇

谷粒商城-个人笔记(高级篇三):https://blog.csdn.net/wts563540/article/details/109713677

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/462429
推荐阅读
相关标签
  

闽ICP备14008679号