赞
踩
目录
1)、在“gulimall-product”项目中导入前端代码:
1)、商城业务-nginx-搭建域名访问环境一(反向代理配置)
3)、Redisson-lock看门狗原理-redisson如何解决死锁
3)、SpringCache-整合&体验@Cacheable
项目在发布的时候,将静态资源放到nginx中,实现动静分离
引入"thymeleaf"依赖:
前端使用了thymeleaf开发,因此要导入该依赖,并且为了改动页面实时生效导入devtools
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
将静态资源放入到static文件夹下,而将index.html放入到templates文件夹下:
在“application.yml”文件中设置thymeleaf,关闭thymeleaf缓存,路径为:spring.thymeleaf
- spring:
- thymeleaf:
- cache: false
同时将“controller”修改为app,以后它都是被移动APP所访问的地方。
创建web文件夹:
启动“gulimall-product”服务,根据路径就可以直接访问静态资源
在静态资源目录static下的资源,可以直接访问,如:http://localhost:10000/index/css/GL.css
SpringBoot在访问项目的时候,会默认找到index文件夹下的文件。
这些规则是配置在“ResourceProperties”文件中指定的:
- private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
-
- "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
-
- 关于欢迎页,它是在静态文件夹中,寻找index.html页面的:
-
- org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
- private Optional<Resource> getWelcomePage() {
-
- String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
-
- return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
-
- }
-
- private Resource getIndexHtml(String location) {
-
- return this.resourceLoader.getResource(location + "index.html");
-
- }
现在想要实现的效果是访问http://localhost:10000/index.html能访问,另外当
在thymeleaf中,默认的访问的前缀和后缀
- //前缀
- public static final String DEFAULT_PREFIX = "classpath:/templates/";
- //后缀
- public static final String DEFAULT_SUFFIX = ".html";
当controller中返回的是一个视图地址,它就会使视图解析器进行拼串。
查询出所有的一级分类:
2.1)、渲染一级分类菜单
由于访问首页时就要加载一级目录,所以我们需要在加载首页时获取该数据
修改“com.atguigu.gulimall.product.web.IndexController”类,修改如下:
- @GetMapping({"/", "index.html"})
- public String getIndex(Model model) {
- //获取所有的一级分类
- List<CategoryEntity> catagories = categoryService.getLevel1Catagories();
- model.addAttribute("catagories", catagories);
- return "index";
- }
修改“com.atguigu.gulimall.product.service.CategoryService”类,修改如下:
List<CategoryEntity> getLevel1Category();
修改“com.atguigu.gulimall.product.service.impl.CategoryServiceImpl”类,修改如下:
- @Override
- public List<CategoryEntity> getLevel1Category() {
- List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
- return categoryEntities;
- }
2.2)、dev-tools实现不重启服务实时生效
1、添加devtools依赖
- <dependency>
-
- <groupId>org.springframework.boot</groupId>
-
- <artifactId>spring-boot-devtools</artifactId>
-
- <optional>true</optional>
-
- </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">
页面遍历菜单数据
- <div class="header_main_left">
- <ul>
- <li th:each="category : ${categorys}">
- <a href="#" class="header_main_left_a" ctg-data="3" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器111</b></a>
- </li>
- </ul>
- </div>
首页加载的数据默认来自于静态资源的“index/catalog.json”,现在需要让它重数据库实时读取
添加“com.atguigu.gulimall.product.web.IndexController”类,代码如下:
- @ResponseBody
- @GetMapping("/index/catelog.json")
- public Map<String, List<Catelog2Vo>> getCatelogJson(){
- Map<String, List<Catelog2Vo>> catelogJson = categoryService.getCatelogJson();
- return catelogJson;
- }
添加“com.atguigu.gulimall.product.vo.Catelog2Vo”类,代码如下:
- //二级分类
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class Catelog2Vo {
- private String catelog1Id; //一级父分类id
- private List<Catalog3Vo> catalog3List; //三级子分类
- private String id;
- private String name;
-
- //三级分类
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public static class Catalog3Vo{
- private String catelog2Id; //父分类,2级分类id
- private String id;
- private String name;
- }
- }
修改“com.atguigu.gulimall.product.service.impl.CategoryServiceImpl”类,代码如下:
- /**
- * 逻辑是
- * (1)根据一级分类,找到对应的二级分类
- * (2)将得到的二级分类,封装到Catelog2Vo中
- * (3)根据二级分类,得到对应的三级分类
- * (3)将三级分类封装到Catalog3List
- * @return
- */
- @Override
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
- //查出所有1级分类
- List<CategoryEntity> level1Category = getLevel1Category();
-
- //2、封装数据
- Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
- //1、每一个的一级分类,查到这个一级分类的二级分类
- List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
- //2、封装上面的结果
- List<Catelog2Vo> catelog2Vos = null;
- if (categoryEntities != null) {
- catelog2Vos = categoryEntities.stream().map(l2 -> {
- Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
- //1、找当前二级分类的三级分类封装vo
- List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
- if (level3Catelog != null){
- List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
- //2、封装成指定格式
- Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
- return catelog3Vo;
- }).collect(Collectors.toList());
- catelog2Vo.setCatelog3List(Collections.singletonList(collect));
- }
- return catelog2Vo;
- }).collect(Collectors.toList());
- }
- return catelog2Vos;
- }));
-
- return parent_cid;
- }
Nginx+windows搭建域名访问环境
正向代理和反向代理
修改本地的C:\Windows\System32\drivers\etc\hosts文件,添加域名映射规则:
先把只读模式去掉,才可以编辑
192.168.43.125 gulimall.com
192.168.43.125 为Nginx所在的设备
测试Nginx的访问:http://gulimall.com/
关于Nginx的配置文件:
- user nginx;
- worker_processes 1;
- error_log /var/log/nginx/error.log warn;
- pid /var/run/nginx.pid;
- events {
- worker_connections 1024;
- }
- http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
- access_log /var/log/nginx/access.log main;
- sendfile on;
- #tcp_nopush on;
- keepalive_timeout 65;
- #gzip on;
- include /etc/nginx/conf.d/*.conf;
- }
注意这里的“include /etc/nginx/conf.d/*.conf;”,它是将“/etc/nginx/conf.d/*.conf”目录下的所有配置文件包含到nginx.conf文件中。下面是该文件的内容:
- server {
- listen 80;
- server_name localhost;
- #charset koi8-r;
- #access_log /var/log/nginx/host.access.log main;
- location / {
- root /usr/share/nginx/html;
- index index.html index.htm;
- }
- #error_page 404 /404.html;
- # redirect server error pages to the static page /50x.html
- #
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- # proxy the PHP scripts to Apache listening on 127.0.0.1:80
- #
- #location ~ \.php$ {
- # proxy_pass http://127.0.0.1;
- #}
- # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
- #
- #location ~ \.php$ {
- # root html;
- # fastcgi_pass 127.0.0.1:9000;
- # fastcgi_index index.php;
- # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
- # include fastcgi_params;
- #}
- # deny access to .htaccess files, if Apache's document root
- # concurs with nginx's one
- #
- #location ~ /\.ht {
- # deny all;
- #}
- }
在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。
整个的数据流是这样的:浏览器请求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.1)关于Nginx的负载均衡
使用Nginx作为Http负载均衡器
http://nginx.org/en/docs/http/load_balancing.html
默认的负载均衡配置:
- http {
- upstream myapp1 {
- server srv1.example.com;
- server srv2.example.com;
- server srv3.example.com;
- }
- server {
- listen 80;
- location / {
- proxy_pass http://myapp1;
- }
- }
- }
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_pass, uwsgi_pass, scgi_pass, memcached_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”添加路由规则:
- - id: gulimall_host_route
- uri: lb://gulimall-product
- predicates:
- - 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
jmeter压力测试工具
查看响应结果
汇总图:
JMeter在windows下地址占用bug解决
堆内存与垃圾回收
https://visualvm.github.io/uc/8u131/updates.xml.gz
实例:如下面我们想要可视化GC的过程,可以安装这个插件
安装完成后,重启jvisualvm
然后连接我们的gulimall-product服务进程
在GC选项卡能够看到GC发生过程:
(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关闭发送失败
查看汇总报告
查看聚合报告
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)测试效果
再次进行压测:
执行压测
查看测试报告
同时通过jvisualvm查看GC过程
能够看到在老年代中存在耗时的GC过程,并且随着并发量增加,已经开始出现OutOfMemoryError异常了
并且服务也会down掉:
现在我们可以通过增加服务占用的内存大小,来控制减少Full GC发生的频率
来看我们之前编写的获取三级分类的业务逻辑:
- @Override
-
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
-
- //1.查出所有一级分类
-
- List<CategoryEntity> level1Categories = getLevel1Categories();
-
-
- Map<String, List<Catelog2Vo>> parent_cid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
-
- //2. 根据一级分类的id查找到对应的二级分类
-
- List<CategoryEntity> level2Categories = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", level1.getCatId()));
-
-
- //3. 根据二级分类,查找到对应的三级分类
-
- List<Catelog2Vo> catelog2Vos =null;
-
-
- if(null != level2Categories || level2Categories.size() > 0){
-
- catelog2Vos = level2Categories.stream().map(level2 -> {
-
- //得到对应的三级分类
-
- List<CategoryEntity> level3Categories = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", level2.getCatId()));
-
- //封装到Catalog3List
-
- List<Catalog3List> catalog3Lists = null;
-
- if (null != level3Categories) {
-
- catalog3Lists = level3Categories.stream().map(level3 -> {
-
- Catalog3List catalog3List = new Catalog3List(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
-
- return catalog3List;
-
- }).collect(Collectors.toList());
-
- }
-
- return new Catelog2Vo(level1.getCatId().toString(), catalog3Lists, level2.getCatId().toString(), level2.getName());
-
- }).collect(Collectors.toList());
-
- }
-
-
- return catelog2Vos;
-
- }));
-
- return parent_cid;
-
- }
(1)先从数据库中查询所有的一级分类
(2)根据一级分类的ID到数据库中找到对应的二级分类
(3)根据二级分类的ID,到数据库中寻找到对应的三级分类
在这个逻辑实现中,每一个一级分类的ID,至少要经过3次数据库查询才能得到对应的三级分类,所以在大数据量的情况下,频繁的操作数据库,性能比较低。
我们可以考虑将这些分类数据一次性的load到内存中,在内存中来操作这些数据,而不是频繁的进行数据库交互操作,下面是优化后的查询
- @Override
-
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
-
- //一次性查询出所有的分类数据,减少对于数据库的访问次数,后面的数据操作并不是到数据库中查询,而是直接从这个集合中获取,
-
- // 由于分类信息的数据量并不大,所以这种方式是可行的
-
- List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
-
-
- //1.查出所有一级分类
-
- List<CategoryEntity> level1Categories = getParentCid(categoryEntities,0L);
-
-
- Map<String, List<Catelog2Vo>> parent_cid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
-
- //2. 根据一级分类的id查找到对应的二级分类
-
- List<CategoryEntity> level2Categories = getParentCid(categoryEntities,level1.getCatId());
-
-
- //3. 根据二级分类,查找到对应的三级分类
-
- List<Catelog2Vo> catelog2Vos =null;
-
-
- if(null != level2Categories || level2Categories.size() > 0){
-
- catelog2Vos = level2Categories.stream().map(level2 -> {
-
- //得到对应的三级分类
-
- List<CategoryEntity> level3Categories = getParentCid(categoryEntities,level2.getCatId());
-
- //封装到Catalog3List
-
- List<Catalog3List> catalog3Lists = null;
-
- if (null != level3Categories) {
-
- catalog3Lists = level3Categories.stream().map(level3 -> {
-
- Catalog3List catalog3List = new Catalog3List(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
-
- return catalog3List;
-
- }).collect(Collectors.toList());
-
- }
-
- return new Catelog2Vo(level1.getCatId().toString(), catalog3Lists, level2.getCatId().toString(), level2.getName());
-
- }).collect(Collectors.toList());
-
- }
-
-
- return catelog2Vos;
-
- }));
-
- return parent_cid;
-
- }
-
-
- /**
- * 在selectList中找到parentId等于传入的parentCid的所有分类数据
- * @param selectList
- * @param parentCid
- * @return
- */
-
- private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList,Long parentCid) {
-
- List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
-
- return collect;
-
- }
-
整体的逻辑就是每次根据分类ID,找到所有子分类数据的时候,不再从数据库中查找,而是在内存中查询。
我们可以通过Jmeter来测试一下优化后的查询效率
请求参数设置如下:
下面是测试的比对:
对于复杂的业务,已经不能够通过代码层面的优化和数据库层面的优化,来达到增加吞吐量的目的。这就想要使用到缓存。
本地缓存
分布式本地缓存
分布式缓存-本地模式在分布式下的问题
这种情况下,每个服务维持一个缓存,所带来的问题:
(1)缓存不共享
在这种情况下,每个服务都有一个缓存,但是这个缓存并不共享,水平上当调度到另外一个台设备上的时候,可能它的服务中并不存在这个缓存,因此需要重新查询。
(2)缓存一致性问题
在一台设备上的缓存更新后,其他设备上的缓存可能还未更新,这样当从其他设备上获取数据的时候,得到的可能就是未给更新的数据。
在这种下,一个服务的不同副本共享同一个缓存空间,缓存放置到缓存中间件中,这个缓存中间件可以是redis等,而且缓存中间件也是可以水平或纵向扩展的,如Redis可以使用redis集群。它打破了缓存容量的限制,能够做到高可用,高性能。
在“gulimall-product”项目中引入redis
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
Reids是通过“RedisAutoConfiguration”来完成的,它将所有的配置信息,都放置到了“RedisProperties”中。
配置redis主机地址
- spring:
- redis:
- host: 172.20.10.3
- port: 6379
这里我们的Redis服务器为172.20.10.3,部署的是Redis容器。
在“”类中,提供了两种操作Redis的方式:
- @Configuration(proxyBeanMethods = false)
- @ConditionalOnClass(RedisOperations.class)
- @EnableConfigurationProperties(RedisProperties.class)
- @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
- public class RedisAutoConfiguration {
-
- @Bean
- @ConditionalOnMissingBean(name = "redisTemplate")
- //将保存进入Redis的键值都是Object
- public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
- throws UnknownHostException {
- RedisTemplate<Object, Object> template = new RedisTemplate<>();
- template.setConnectionFactory(redisConnectionFactory);
- return template;
- }
-
- @Bean
- @ConditionalOnMissingBean
- //保存进Redis的数据,键值是(String,String)
- public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
- throws UnknownHostException {
- StringRedisTemplate template = new StringRedisTemplate();
- template.setConnectionFactory(redisConnectionFactory);
- return template;
- }
-
- }
关于“StringRedisTemplate”
- public class StringRedisTemplate extends RedisTemplate<String, String> {
-
- /**
- * Constructs a new <code>StringRedisTemplate</code> instance. {@link #setConnectionFactory(RedisConnectionFactory)}
- * and {@link #afterPropertiesSet()} still need to be called.
- */
- public StringRedisTemplate() {
- setKeySerializer(RedisSerializer.string());//键序列化为String
- setValueSerializer(RedisSerializer.string());//key序列化为String
- setHashKeySerializer(RedisSerializer.string());
- setHashValueSerializer(RedisSerializer.string());
- }
综上:SpringBoot整合Redis的方式
(1)引入“spring-boot-starter-data-redis”
(2)简单配置Redis的host等信息
(3)使用SpringBoot自动配置好的"StringRedisTemplate"来操作redis。
测试:
引入StringRedisTemplate
- @Autowired
- StringRedisTemplate redisTemplate;
- @Test
- public void testStringRedisTemplate(){
- ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
- //保存
- ops.set("hello","world_"+ UUID.randomUUID().toString());
- //查询
- String hello = ops.get("hello");
- System.out.println("之前保存的数据是:"+hello);
- }
在Redis上查看结果
先从缓存中获取分类三级分类数据,如果没有再从数据库中查询,并且将查询结果以JSON字符串的形式存放到Reids中的。
- @Override
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
- //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
- //1、加入缓存逻辑
- //JSON好处是跨语言,跨平台兼容。
- String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
- if (StringUtils.isEmpty(catalogJSON)){
- //2、缓存中没有,查询数据库
- Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
- //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
- String jsonString = JSON.toJSONString(catelogJsonFromDB);
- redisTemplate.opsForValue().set("catalogJSON",jsonString);
- return catelogJsonFromDB;
- }
-
- //转为我们指定的对象。
- Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
- return result;
- }
- private Map<String, List<Catelog2Vo>> getDataFromDB() {
- String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
- if (!StringUtils.isEmpty(catalogJSON)) {
- //如果缓存不为null直接缓存
- Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
- });
- return result;
- }
- System.out.println("查询了数据库。。。。。");
-
- List<CategoryEntity> selectList = baseMapper.selectList(null);
- //查出所有一级分类
- List<CategoryEntity> level1Category = getParent_cid(selectList, 0L);
-
- //2、封装数据
- Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
- //1、每一个的一级分类,查到这个一级分类的二级分类
- List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
- //2、封装上面的结果
- List<Catelog2Vo> catelog2Vos = null;
- if (categoryEntities != null) {
- catelog2Vos = categoryEntities.stream().map(l2 -> {
- Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
- //1、找当前二级分类的三级分类封装vo
- List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
- if (level3Catelog != null) {
- List<Catelog2Vo.Catalog3Vo> collect = level3Catelog.stream().map(l3 -> {
- //2、封装成指定格式
- Catelog2Vo.Catalog3Vo catelog3Vo = new Catelog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
- return catelog3Vo;
- }).collect(Collectors.toList());
- catelog2Vo.setCatalog3List(collect);
- }
- return catelog2Vo;
- }).collect(Collectors.toList());
- }
- return catelog2Vos;
- }));
- //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
- String jsonString = JSON.toJSONString(parent_cid);
- redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
- return parent_cid;
- }
启动“gulimall-product”和“gulimall-gateway”
执行压测:
测试报告:
再次访问页面出现了异常:
堆外内存溢出的原因,
解决方法:
修改“gulimall-product”的“pom.xml”文件,更换为Jedis
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- <exclusions>
- <exclusion>
- <groupId>io.lettuce</groupId>
- <artifactId>lettuce-core</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>redis.clients</groupId>
- <artifactId>jedis</artifactId>
- </dependency>
再次执行压测,没有出现Error:
使用Redis作为缓存后,吞吐量得到了很大的提升,响应时间也缩短了很多:
前面我们将查询三级分类数据的查询进行了优化,将查询结果放入到Redis中,当再次获取到相同数据的时候,直接从缓存中读取,没有则到数据库中查询,并将查询结果放入到Redis缓存中
但是在分布式系统中,这样还是会存在问题。
缓存穿透
缓存雪崩
缓存击穿
简单来说:缓存穿透是指查询一个永不存在的数据;缓存雪崩是值大面积key同时失效问题;缓存击穿是指高频key失效问题;
将查询db的方法加锁,这样在同一时间只有一个方法能查询数据库,就能解决缓存击穿的问题了
现在针对于单体应用上的加锁,我们来测试一下它是否能够正常工作。
(1)删除“redis”中的“catelogJson”
(2)修改三级分类的代码
- @Override
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
- //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
- /**
- * 1、空结果缓存,解决缓存穿透
- * 2、设置过期时间(随机加值);解决缓存雪崩
- * 3、加锁,解决缓存击穿
- */
- //1、加入缓存逻辑
- //JSON好处是跨语言,跨平台兼容。
- String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
- if (StringUtils.isEmpty(catalogJSON)){
- //2、缓存中没有,查询数据库
- System.out.println("缓存不命中.....将要查询数据库...");
- Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
- //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
- String jsonString = JSON.toJSONString(catelogJsonFromDB);
- redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
- return catelogJsonFromDB;
- }
- System.out.println("缓存命中...直接返回...");
- //转为我们指定的对象。
- Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
- return result;
- }
-
- //从数据库查询并封装分类数据
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
- //只要同一把锁就能锁住需要这个锁的所有线程
- //1、synchronized(this):SpringBoot所有的组件在容器中都是单例的
- // TODO 本地锁:synchronized,JUC(lock)。在分布式情况下想要锁住所有,必须使用分布式锁
- //使用DCL(双端检锁机制)来完成对于数据库的访问
- synchronized (this){
- //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
- String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
- if (!StringUtils.isEmpty(catalogJSON)){
- //如果缓存不为null直接缓存
- Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
- return result;
- }
- System.out.println("查询了数据库。。。。。");
- /**
- * 1、将数据库的多次查询变为1次
- */
- List<CategoryEntity> selectList = baseMapper.selectList(null);
- //查出所有一级分类
- List<CategoryEntity> level1Category = getParent_cid(selectList,0L);
-
- //2、封装数据
- Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
- //1、每一个的一级分类,查到这个一级分类的二级分类
- List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
- //2、封装上面的结果
- List<Catelog2Vo> catelog2Vos = null;
- if (categoryEntities != null) {
- catelog2Vos = categoryEntities.stream().map(l2 -> {
- Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
- //1、找当前二级分类的三级分类封装vo
- List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
- if (level3Catelog != null){
- List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
- //2、封装成指定格式
- Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
- return catelog3Vo;
- }).collect(Collectors.toList());
- catelog2Vo.setCatelog3List(Collections.singletonList(collect));
- }
- return catelog2Vo;
- }).collect(Collectors.toList());
- }
- return catelog2Vos;
- }));
-
- return parent_cid;
- }
-
- }
在上述方法中,我们将业务逻辑中的确认缓存没有
和查数据库
放到了锁里,但是最终控制台却打印了两次查询了数据库。这是因为在将结果放入缓存的这段时间里,有其他线程确认缓存没有,又再次查询了数据库,因此我们要将结果放入缓存
也进行加锁
(1)删除“redis”中的“catelogJson”
优化代码逻辑后
- @Override
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
- //给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
- /**
- * 1、空结果缓存,解决缓存穿透
- * 2、设置过期时间(随机加值);解决缓存雪崩
- * 3、加锁,解决缓存击穿
- */
- //1、加入缓存逻辑
- //JSON好处是跨语言,跨平台兼容。
- String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
- if (StringUtils.isEmpty(catalogJSON)){
- //2、缓存中没有,查询数据库
- System.out.println("缓存不命中.....将要查询数据库...");
- Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
-
- }
- System.out.println("缓存命中...直接返回...");
- //转为我们指定的对象。
- Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
- return result;
- }
-
- //从数据库查询并封装分类数据
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
- //只要同一把锁就能锁住需要这个锁的所有线程
- //1、synchronized(this):SpringBoot所有的组件在容器中都是单例的
- // TODO 本地锁:synchronized,JUC(lock)。在分布式情况下想要锁住所有,必须使用分布式锁
- //使用DCL(双端检锁机制)来完成对于数据库的访问
- synchronized (this){
- //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
- String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
- if (!StringUtils.isEmpty(catalogJSON)){
- //如果缓存不为null直接缓存
- Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
- return result;
- }
- System.out.println("查询了数据库。。。。。");
- /**
- * 1、将数据库的多次查询变为1次
- */
- List<CategoryEntity> selectList = baseMapper.selectList(null);
- //查出所有一级分类
- List<CategoryEntity> level1Category = getParent_cid(selectList,0L);
-
- //2、封装数据
- Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
- //1、每一个的一级分类,查到这个一级分类的二级分类
- List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
- //2、封装上面的结果
- List<Catelog2Vo> catelog2Vos = null;
- if (categoryEntities != null) {
- catelog2Vos = categoryEntities.stream().map(l2 -> {
- Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
- //1、找当前二级分类的三级分类封装vo
- List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
- if (level3Catelog != null){
- List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
- //2、封装成指定格式
- Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
- return catelog3Vo;
- }).collect(Collectors.toList());
- catelog2Vo.setCatelog3List(Collections.singletonList(collect));
- }
- return catelog2Vo;
- }).collect(Collectors.toList());
- }
- return catelog2Vos;
- }));
- //3、将查到的数据再放入缓存,将对象转为JSON在缓存中
- String jsonString = JSON.toJSONString(parent_cid);
- redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
- return parent_cid;
- }
-
- }
这里我们使用了双端检锁机制来控制线程的并发访问数据库。一个线程进入到临界区之前,进行缓存中是否有数据,进入到临界区后,再次判断缓存中是否有数据,这样做的目的是避免阻塞在临界区的多个线程,在其他线程释放锁后,重复进行数据库的查询和放缓存操作。
注:关于双端检锁机制的简单了解,可以参照:https://www.cnblogs.com/cosmos-wong/p/11914878.html
(3)后执行压测,使用100个线程来回发送请求:
优化后多线程访问时仅查询一次数据库
5)结论
通过观察日志,能够发现只有一个线程查询了数据库,其他线程都是直接从缓存中获取到数据的。所以在单体应用上实现了多线程的并发访问。
由于这里我们的“gulimall-product”就部署了一台,所以看上去一切祥和,但是在如果部署了多台,问题就出现了,主要问题就集中在我们所使用的锁上。我们锁使用的是“synchronized ”,这是一种本地锁,它只是在一台设备上有效,无法实现分布式情况下,锁住其他设备的相同操作。
我们现在的操作模型,表现为如下的形式:
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锁未能实现限制其他服务实例进入临界区,也就印证了在分布式情况下,本地锁只能针对于当前的服务生效。
当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题
所有的服务都到同一个redis进行获取数据,就可以避免这个问题
当分布式项目在高并发下也需要加锁,但本地锁只能锁住当前服务,这个时候就需要分布式锁
基本原理
下面使用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”观察四个窗口的输出
将命令批量发送到四个窗口的的方式:
阶段一
- public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
- //阶段一
- Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
- //获取到锁,执行业务
- if (lock) {
- //加锁成功。。。执行业务
- //2、设置过期时间,必须和加锁是同步的,原子的
- Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
- redisTemplate.delete("lock");//删除锁
- return dataFromDB;
- }else {
- //没获取到锁,等待100ms重试
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return getCatalogJsonDBWithRedisLock();
- }
- }
问题: 1、setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除
阶段二
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
- //1、占分布式锁。去redis占坑
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
- if (lock){
- //加锁成功。。。执行业务
- //2、设置过期时间
- redisTemplate.expire("lock",30,TimeUnit.SECONDS);
- Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
- redisTemplate.delete("lock");//删除锁
- return dataFromDB ;
- }else {
- //加锁失败。。。重试。 synchronized()
- //没获取到锁,等待100ms重试
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return getCatelogJsonFromDBWithRedisLock();//自旋的方式
- }
- }
问题: 1、setnx设置好,正要去设置过期时间,宕机。又死锁了。 解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令
阶段三
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
- //1、占分布式锁。去redis占坑
- String uuid = UUID.randomUUID().toString();
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
-
- if (lock){
- //加锁成功。。。执行业务
- //2、设置过期时间,必须和加锁是同步的,原子的
- Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
- redisTemplate.delete("lock");//删除锁
- return dataFromDB;
- }else {
- //加锁失败。。。重试。 synchronized()
- //没获取到锁,等待100ms重试
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return getCatelogJsonFromDBWithRedisLock();//自旋的方式
- }
- }
问题: 1、删除锁直接删除??? 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。 解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
阶段四
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
- //1、占分布式锁。去redis占坑
- String uuid = UUID.randomUUID().toString();
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
-
- if (lock){
- //加锁成功。。。执行业务
- //2、设置过期时间,必须和加锁是同步的,原子的
- Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
- String lockValue = redisTemplate.opsForValue().get("lock");
- if (uuid.equals(lockValue)) {
- //删除我自己的锁
- redisTemplate.delete("lock");//删除锁
- }
- return dataFromDB;
- }else {
- //加锁失败。。。重试。 synchronized()
- //没获取到锁,等待100ms重试
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return getCatelogJsonFromDBWithRedisLock();//自旋的方式
- }
- }
问题: 1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁 解决: 删除锁必须保证原子性。使用redis+Lua脚本完成
阶段五-最终形态
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
- //1、占分布式锁。去redis占坑
- String uuid = UUID.randomUUID().toString();
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
-
- if (lock){
- //加锁成功。。。执行业务
- //2、设置过期时间,必须和加锁是同步的,原子的
- Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
- //获取值对比+对比成功删除=原子操作 Lua脚本解锁
- // String lockValue = redisTemplate.opsForValue().get("lock");
- // if (uuid.equals(lockValue)) {
- // //删除我自己的锁
- // redisTemplate.delete("lock");//删除锁
- // }
- String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
- " return redis.call(\"del\",KEYS[1])\n" +
- "else\n" +
- " return 0\n" +
- "end";
- //删除锁
- Integer lock1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);
- return dataFromDB;
- }else {
- //加锁失败。。。重试。 synchronized()
- //没获取到锁,等待100ms重试
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return getCatelogJsonFromDBWithRedisLock();//自旋的方式
- }
- }
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期
Redison使用手册:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
导入依赖:以后使用Redisson作为分布式锁,分布式对象等功能框架
- <dependency>
-
- <groupId>org.redisson</groupId>
-
- <artifactId>redisson</artifactId>
-
- <version>3.12.5</version>
-
- </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 ” 配置类:
- @Configuration
-
- public class MyRedisConfig {
-
-
- @Bean(destroyMethod="shutdown")
-
- public RedissonClient redisson() throws IOException {
-
- Config config = new Config();
-
- config.useSingleServer().setAddress("redis://192.168.137.14:6379");
-
- RedissonClient redisson = Redisson.create(config);
-
- return redisson;
-
- }
-
-
- }
测试是否注入了RedissonClient,
- @Autowired
-
- RedissonClient redissonClient;
-
-
- @Test
-
- public void testRedison(){
-
- System.out.println(redissonClient);
-
- }
-
在Redison中分布式锁的使用,和java.util.concurrent包中的所提供的锁的使用方法基本相同。
测试Redisson的Lock锁的使用:
- @GetMapping("/hello")
- @ResponseBody
- public String hello(){
- //1.获取一把锁,只要名字一样,就是同一把锁
- RLock lock = redisson.getLock("my-lock");
- //2.加锁和解锁
-
-
- try {
- lock.lock();
- System.out.println("加锁成功,执行业务方法..."+Thread.currentThread().getId());
- Thread.sleep(30000);
- } catch (Exception e){
-
- }finally {
- lock.unlock();
- System.out.println("释放锁..."+Thread.currentThread().getId());
- }
- return "hello";
- }
同时发送两个请求到:http://localhost:10000/hello
能够看到在加锁期间另外一个请求一直都是出于挂起状态,需要等待上一个请求处理完毕后,它才能接着执行。
查看Redis:
设想一种情况,一个请求线程在执行业务方法的时候,突然发生了中断,此时没有来得及执行释放锁操作,那么同时等待的另外一个线程是否会发生死锁。为了模拟这种情形,我们同时启动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) :
- @Override
- public void lock(long leaseTime, TimeUnit unit) {
- try {
- lock(leaseTime, unit, false);
- } catch (InterruptedException e) {
- throw new IllegalStateException();
- }
- }
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)存在到期后自动删除的问题,但是我们对于它的使用还是比较多的,通常都会评估一下业务的最大执行用时,在这个时间内,如果仍然未能执行完成,则认为出现了问题,则释放锁执行其他逻辑。
保证一定能够读取到最新的数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁,写锁没释放读就必须等待。
(1)在Redis增加一个新的key“writeValue”,值为11111
(2)增加write和read的controller方法
- @GetMapping("/write")
- @ResponseBody
- public String writeValue(){
- RReadWriteLock writeLock=redisson.getReadWriteLock("rw-loc");
- String uuid = null;
- RLock lock = writeLock.writeLock();
- lock.lock();
- try {
- uuid = UUID.randomUUID().toString();
- redisTemplate.opsForValue().set("writeValue",uuid);
- Thread.sleep(30-000);
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- }
- return uuid;
- }
-
- @GetMapping("/read")
- @ResponseBody
- public String redValue(){
- String uuid = null;
- RReadWriteLock readLock=redisson.getReadWriteLock("rw-loc");
- RLock lock = readLock.readLock();
- lock.lock();
- try {
- uuid = redisTemplate.opsForValue().get("writeValue");
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- }
- return uuid;
- }
(3)启动gulimall-product
(4)分别访问“http://localhost:10000/read”和“http://localhost:10000/write”,观察现象。
读写锁补充
(1)修改“read”和“write”的controller方法
- @GetMapping("/write")
- @ResponseBody
- public String writeValue(){
- RReadWriteLock writeLock=redisson.getReadWriteLock("rw-loc");
- String uuid = null;
- RLock lock = writeLock.writeLock();
- lock.lock();
- try {
- log.info("写锁加锁成功");
- uuid = UUID.randomUUID().toString();
- redisTemplate.opsForValue().set("writeValue",uuid);
- Thread.sleep(30000);
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- log.info("写锁释放");
-
- }
- return uuid;
- }
-
- @GetMapping("/read")
- @ResponseBody
- public String redValue(){
- String uuid = null;
- RReadWriteLock readLock=redisson.getReadWriteLock("rw-loc");
- RLock lock = readLock.readLock();
- lock.lock();
- try {
- log.info("读锁加锁成功");
- uuid = redisTemplate.opsForValue().get("writeValue");
- Thread.sleep(30000);
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- log.info("读锁释放");
- }
- return uuid;
- }
- }
(2)先发送一个写请求,然后同时发送四个读请求
(3)观察现象
另外在先执行读操作时,写操作被阻塞。
小结:
所以只要存在写操作,不论前面是或后面执行的是读或写操作,都会阻塞。
5)、闭锁测试
先在redis中设置park的值为3
信号量作为分布式限流:
缓存一致性是为了解决数据库和缓存的数据不同步问题的。
1)缓存一致性——双写模式
2)缓存一致性——失效模式
3)缓存一致性——解决方案
4)缓存一致性——解决canal
使用Redisson的锁机制优化三级分类数据的查询。
- /**
- * 使用Redisson分布式锁来实现多个服务共享同一缓存中的数据
- * @return
- */
- public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedissonLock() {
-
- RLock lock = redissonClient.getLock("CatelogJson-lock");
- //该方法会阻塞其他线程向下执行,只有释放锁之后才会接着向下执行
- lock.lock();
- Map<String, List<Catelog2Vo>> catelogJsonFromDb;
- try {
- //从数据库中查询分类数据
- catelogJsonFromDb = getCatelogJsonFromDb();
- } finally {
- lock.unlock();
- }
-
- return catelogJsonFromDb;
-
- }
我们系统的一致性解决方案:
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2、读写数据的时候,加上分布式的读写锁。
在更新分类数据的时候,删除缓存中的旧数据。
SpringCache的文档:https://docs.spring.io/spring/docs/5.3.0-SNAPSHOT/spring-framework-reference/integration.html#cache
整合SpringCache,简化缓存的开发
1)引入依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
引入spring-boot-starter-data-redis
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- <exclusions>
- <exclusion>
- <groupId>io.lettuce</groupId>
- <artifactId>lettuce-core</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
2)编写配置
(1)缓存的自动配置了哪些?
(2)配置使用Redis作为缓存
修改“application.properties”文件,指定使用redis作为缓存,spring.cache.type=redis
(3)和缓存有关的注解
(4)测试使用缓存
(1)开启缓存功能,在主启动类上,标注@EnableCaching
(2)只需要使用注解,就可以完成缓存操作
(3)在业务方法的头部标上@Cacheable,加上该注解后,表示当前方法需要将进行缓存,如果缓存中有,方法无效调用,如果缓存中没有,则会调用方法,最后将方法的结果放入到缓存中。
(4)指定缓存分区。每一个需要缓存的数据,我们都需要来指定要放到哪个名字的缓存中。通常按照业务类型进行划分。
如:我们将一级分类数据放入到缓存中,指定缓存名字为“category”
- @Cacheable({"category"})
- @Override
- public List<CategoryEntity> getLevel1Categories() {
- //找出一级分类
- List<CategoryEntity> categoryEntities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
- return categoryEntities;
- }
(5)访问:http://localhost:10000/
(6)查看Redis
能够看到一级分类信息,已经被放入到缓存中了,而且再次访问的时候,没有查询数据库,而是直接从缓存中获取。
上面我们将一级分类数据的信息缓存到Redis中了,缓存到Redis中数据具有如下的特点:
然而这些并不能够满足我们的需要,我们希望:
针对于第一点,我们使用@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
上面我们解决了第一个命名问题和第二个设置存活时间问题,但是如何将数据以JSON的形式缓存到Redis呢?
这涉及到修改缓存管理器的设置,CacheAutoConfiguration导入了RedisCacheConfiguration,而RedisCacheConfiguration中自动配置了缓存管理器RedisCacheManager,而RedisCacheManager要初始化所有的缓存,每个缓存决定使用什么样的配置,如果RedisCacheConfiguration有,就用已有的,没有就用默认配置。
想要修改缓存的配置,只需要给容器中放一个“redisCacheConfiguration”即可,这样就会应用到当前RedisCacheManager管理的所有缓存分区中。
- private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
- CacheProperties cacheProperties, ClassLoader classLoader) {
- Redis redisProperties = cacheProperties.getRedis();
- org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
- .defaultCacheConfig();
- config = config.serializeValuesWith(
- SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
- if (redisProperties.getTimeToLive() != null) {
- config = config.entryTtl(redisProperties.getTimeToLive());
- }
- if (redisProperties.getKeyPrefix() != null) {
- config = config.prefixKeysWith(redisProperties.getKeyPrefix());
- }
- if (!redisProperties.isCacheNullValues()) {
- config = config.disableCachingNullValues();
- }
- if (!redisProperties.isUseKeyPrefix()) {
- config = config.disableKeyPrefix();
- }
- return config;
- }
Redis中的序列化器:org.springframework.data.redis.serializer.RedisSerializer
在Redis中放入自动配置类,设置JSON序列化机制
- package com.atguigu.gulimall.product.config;
-
- import org.springframework.boot.autoconfigure.cache.CacheProperties;
- import org.springframework.boot.context.properties.EnableConfigurationProperties;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.cache.RedisCacheConfiguration;
- import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
- import org.springframework.data.redis.serializer.RedisSerializationContext;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- /**
- * @Description: MyCacheConfig
- * @Author: WangTianShun
- * @Date: 2020/11/11 12:57
- * @Version 1.0
- */
- @EnableConfigurationProperties(CacheProperties.class)
- @Configuration
- @EnableCaching
- public class MyCacheConfig {
- /**
- * 配置文件中的东西没有用到
- *
- * 1、原来和配置文件绑定的配置类是这样的
- * @ConfigurationProperties(prefix="spring.cache")
- * public class CacheProperties
- * 2、让他生效
- * @EnableConfigurationProperties(CacheProperties.class)
- * @return
- */
- @Bean
- RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
-
- RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
-
- config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
- config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
- //将配置文件中的所有配置都生效
- CacheProperties.Redis redisProperties = cacheProperties.getRedis();
- //设置配置文件中的各项配置,如过期时间
- if (redisProperties.getTimeToLive() != null) {
- config = config.entryTtl(redisProperties.getTimeToLive());
- }
- if (redisProperties.getKeyPrefix() != null) {
- config = config.prefixKeysWith(redisProperties.getKeyPrefix());
- }
- if (!redisProperties.isCacheNullValues()) {
- config = config.disableCachingNullValues();
- }
- if (!redisProperties.isUseKeyPrefix()) {
- config = config.disableKeyPrefix();
- }
- return config;
- }
- }
查看Redis能够看到以JSON的形式,将数据缓存下来了:
在配置文件中,还可以指定一些缓存的自定义配置
- spring.cache.type=redis
-
- #设置超时时间,默认是毫秒
-
- spring.cache.redis.time-to-live=3600000
-
- #设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀
-
- spring.cache.redis.key-prefix=CACHE_
-
- spring.cache.redis.use-key-prefix=true
-
- #是否缓存空值,防止缓存穿透
-
- spring.cache.redis.cache-null-values=true
基于这个配置,在如果出现了null值,也会被保存到redis中:
如果配置“spring.cache.redis.use-key-prefix=false”,则生成的key没有前缀:
在上面实例中,在读模式中,我们将一级分类信息缓存到redis中,当请求再次获取数据时,直接从缓存中进行获取,但是如果执行的是写模式呢?
在写模式下,有两种方式来解决缓存一致性问题,双写模式和失效模式,在SpringCache中可以通过@CachePut来实现双写模式,使用@CacheEvict来实现失效模式。
实例:使用缓存失效机制实现更新数据库中值的是,使得缓存中的数据失效
(1)修改updateCascade方法,添加@CacheEvict注解,指明要删除哪个分类下的数据,并且确定key:
- @Cacheable(value = {"category"}, key = "#root.methodName") //代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,最后将方法放入缓存。
- @Override
- public List<CategoryEntity> getLevel1Category() {
- List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
- return categoryEntities;
- }
(2)启动gulimall-product,启动renren-fast,启动gulimall-gateway,启动项目的前端页面
(3)检查redis中是否有“category”命名空间下的数据,没有则访问http://localhost:10000/,生成数据
(4)修改数据:
(5)检查Redis中对应数据是否还存在
检查后发现数据没了,说明缓存失效策略是有效的。
另外在修改了一级缓存时,对应的二级缓存也需要更新,需要修改原来二级分类的执行逻辑。
将“getCatelogJson”恢复成为原来的逻辑,但是设置@Cacheable,非侵入的方式将查询结果缓存到redis中:
- @Cacheable(value = {"category"},key = "#root.methodName")
- @Override
- public Map<String, List<Catelog2Vo>> getCatelogJson() {
- log.info("查询数据库");
- //一次性查询出所有的分类数据,减少对于数据库的访问次数,后面的数据操作并不是到数据库中查询,而是直接从这个集合中获取,
- // 由于分类信息的数据量并不大,所以这种方式是可行的
- List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
-
- //1.查出所有一级分类
- List<CategoryEntity> level1Categories = getParentCid(categoryEntities,0L);
-
- Map<String, List<Catelog2Vo>> parent_cid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
- //2. 根据一级分类的id查找到对应的二级分类
- List<CategoryEntity> level2Categories = getParentCid(categoryEntities,level1.getCatId());
-
- //3. 根据二级分类,查找到对应的三级分类
- List<Catelog2Vo> catelog2Vos =null;
-
- if(null != level2Categories || level2Categories.size() > 0){
- catelog2Vos = level2Categories.stream().map(level2 -> {
- //得到对应的三级分类
- List<CategoryEntity> level3Categories = getParentCid(categoryEntities,level2.getCatId());
- //封装到Catalog3List
- List<Catalog3List> catalog3Lists = null;
- if (null != level3Categories) {
- catalog3Lists = level3Categories.stream().map(level3 -> {
- Catalog3List catalog3List = new Catalog3List(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
- return catalog3List;
- }).collect(Collectors.toList());
- }
- return new Catelog2Vo(level1.getCatId().toString(), catalog3Lists, level2.getCatId().toString(), level2.getName());
- }).collect(Collectors.toList());
- }
-
- return catelog2Vos;
- }));
- return parent_cid;
- }
再次访问,发现控制台数据未更新,还是第一次访问时的输出:
查看Redis中缓存的数据:
上面我们将一级和三级分类信息都缓存到了redis中,现在我们想要实现一种场景是,更新分类数据的时候,将缓存到redis中的一级和三级分类数据都清空。
借助于“@Caching”来完成
- @Caching(evict={
- @CacheEvict(value = {"category"},key = "'level1Categorys'"),
- @CacheEvict(value = {"category"},key = "'getCatelogJson'")
- })
- @Override
- @Transactional
- public void updateCascade(CategoryEntity category) {
- this.updateById(category);
- relationService.updateCategory(category.getCatId(),category.getName());
- }
查询redis,一级和三级分类数据已经被删除。
除了可以使用@Cache外,还可以使用@CacheEvict来完成:
@CacheEvict(value = {"category"},allEntries = true)
它表示要删除“category”分区下的所有数据。
可以看到存储同一类型的数据,都可以指定未同一个分区,可以批量删除这个分区下的数据。以后建议不使用分区前缀,而是使用默认的分区前缀。
Spring-Cache的不足:
1)读模式
2)写模式(缓存与数据一致)
总结:
pom添加thymeleaf
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
在虚拟机mydata/nginx/html/路径下创建search文件夹然后把搜索页的静态资源上传到该文件里
host添加
172.20.10.3 search.gulimall.com
配置nginx
重启nginx
修改网关断言配置
- - id: gulimall_host_route
- uri: lb://gulimall-product
- predicates:
- - Host=gulimall.com
-
- - id: gulimall_search_route
- uri: lb://gulimall-search
- predicates:
- - Host=search.gulimall.com
pom添加devtools
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-devtools</artifactId>
- <optional>true</optional>
- </dependency>
关闭thymeleaf缓存
spring.thymeleaf.cache=false
全文检索: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_高清屏
- GET gulimall_product/_search
- {
- "query": {
- "bool": {
- "must": [
- {
- "match": {
- "skuTitle": "华为"
- }
- }
- ],
- "filter": [
- {
- "term": {
- "catalogId": "225"
- }
- },
- {
- "terms": {
- "brandId": [
- "2"
- ]
- }
- },
- {
- "term": {
- "hasStock": "false"
- }
- },
- {
- "range": {
- "skuPrice": {
- "gte": 1000,
- "lte": 7000
- }
- }
- },
- {
- "nested": {
- "path": "attrs",
- "query": {
- "bool": {
- "must": [
- {
- "term": {
- "attrs.attrId": {
- "value": "6"
- }
- }
- }
- ]
- }
- }
- }
- }
- ]
- }
- },
- "sort": [
- {
- "skuPrice": {
- "order": "desc"
- }
- }
- ],
- "from": 0,
- "size": 5,
- "highlight": {
- "fields": {"skuTitle": {}},
- "pre_tags": "<b style='color:red'>",
- "post_tags": "</b>"
- },
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brandId",
- "size": 10
- },
- "aggs": {
- "brandNameAgg": {
- "terms": {
- "field": "brandName",
- "size": 10
- }
- },
-
- "brandImgAgg": {
- "terms": {
- "field": "brandImg",
- "size": 10
- }
- }
-
- }
- },
- "catalogAgg":{
- "terms": {
- "field": "catalogId",
- "size": 10
- },
- "aggs": {
- "catalogNameAgg": {
- "terms": {
- "field": "catalogName",
- "size": 10
- }
- }
- }
- },
- "attrs":{
- "nested": {
- "path": "attrs"
- },
- "aggs": {
- "attrIdAgg": {
- "terms": {
- "field": "attrs.attrId",
- "size": 10
- },
- "aggs": {
- "attrNameAgg": {
- "terms": {
- "field": "attrs.attrName",
- "size": 10
- }
- }
- }
- }
- }
- }
- }
- }
修改映射
- PUT gulimall_product
- {
- "mappings": {
- "properties": {
- "attrs": {
- "type": "nested",
- "properties": {
- "attrId": {
- "type": "long"
- },
- "attrName": {
- "type": "keyword"
- },
- "attrValue": {
- "type": "keyword"
- }
- }
- },
- "autoGeneratedTimestamp": {
- "type": "long"
- },
- "brandId": {
- "type": "long"
- },
- "brandImg": {
- "type": "keyword"
- },
- "brandName": {
- "type": "keyword"
- },
- "catalogId": {
- "type": "long"
- },
- "catalogName": {
- "type": "keyword"
- },
- "description": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- },
- "hasStock": {
- "type": "boolean"
- },
- "hotScore": {
- "type": "long"
- },
- "parentTask": {
- "properties": {
- "id": {
- "type": "long"
- },
- "nodeId": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- },
- "set": {
- "type": "boolean"
- }
- }
- },
- "refreshPolicy": {
- "type": "text",
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- },
- "retry": {
- "type": "boolean"
- },
- "saleCount": {
- "type": "long"
- },
- "shouldStoreResult": {
- "type": "boolean"
- },
- "skuId": {
- "type": "long"
- },
- "skuImg": {
- "type": "keyword"
- },
- "skuPrice": {
- "type": "keyword"
- },
- "skuTitle": {
- "type": "text",
- "analyzer": "ik_smart"
- },
- "spuId": {
- "type": "keyword"
- }
- }
- }
- }
迁移数据
- POST _reindex
- {
- "source": {
- "index": "product"
- },
- "dest": {
- "index": "gulimall_product"
- }
- }
修改gulimall-search的常量
修改com.atguigu.gulimall.search.constant.EsConstant类,代码如下
- public class EsConstant {
- public static final String PRODUCT_INDEX = "gulimall_product"; //sku数据在es中的索引
- }
1) 请求参数和返回结果
请求参数的封装
- package com.atguigu.gulimall.search.vo;
-
- import lombok.Data;
-
- import java.util.List;
-
- /**
- * @Description: SearchParam
- * @Author: WangTianShun
- * @Date: 2020/11/12 16:21
- * @Version 1.0
- *
- * 封装页面所有可能传递过来的查询条件
- * catalog3Id=225&keyword=小米&sort=saleCount_asc
- */
- @Data
- public class SearchParam {
- private String keyword;//页面传递过来的全文匹配关键字
-
- private Long catalog3Id;//三级分类id
-
- /**
- * sort=saleCount_asc/desc
- * sort=skuPrice_asc/desc
- * sort=hotScore_asc/desc
- */
- private String sort;//排序条件
-
- /**
- * 好多的过滤条件
- * hasStock(是否有货)、skuPrice区间、brandId、catalog3Id、attrs
- * hasStock=0/1
- * skuPrice=1_500
- */
- private Integer hasStock;//是否只显示有货
-
- private String skuPrice;//价格区间查询
-
- private List<Long> brandId;//按照品牌进行查询,可以多选
-
- private List<String> attrs;//按照属性进行筛选
-
- private Integer pageNum = 1;//页码
- }
返回结果
- package com.atguigu.gulimall.search.vo;
-
- import com.atguigu.common.to.es.SkuEsModel;
- import lombok.Data;
-
- import java.util.List;
-
- /**
- * @Description: SearchResponse
- * @Author: WangTianShun
- * @Date: 2020/11/12 16:49
- * @Version 1.0
- */
- @Data
- public class SearchResult {
-
- /**
- * 查询到的商品信息
- */
- private List<SkuEsModel> products;
-
- private Integer pageNum;//当前页码
-
- private Long total;//总记录数
-
- private Integer totalPages;//总页码
-
- private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
-
- private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
-
- private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
-
- //=====================以上是返给页面的信息==========================
-
- @Data
- public static class BrandVo{
- private Long brandId;
-
- private String brandName;
-
- private String brandImg;
- }
-
- @Data
- public static class CatalogVo{
- private Long catalogId;
-
- private String catalogName;
-
- private String brandImg;
- }
-
- @Data
- public static class AttrVo{
- private Long attrId;
-
- private String attrName;
-
- private List<String> attrValue;
- }
- }
2) 主体逻辑
修改“com.atguigu.gulimall.search.controller.SearchController”类,代码如下:
- /**
- * 自动将页面提交过来的所有请求查询参数封装成指定的对象
- * @param param
- * @return
- */
- @GetMapping("/list.html")
- public String listPage(SearchParam searchParam, Model model) {
- SearchResult result = mallSearchService.search(searchParam);
- System.out.println("===================="+result);
- model.addAttribute("result", result);
- return "list";
- }
修改“com.atguigu.gulimall.search.service.MallSearchService”类,代码如下:
-
- /**
- *
- * @param param 检索的所有参数
- * @return 返回检索的结果,里面包含页面需要的所有信息
- */
- SearchResult search(SearchParam param);
主要逻辑在service层进行,service层将封装好的SearchParam
组建查询条件,再将返回后的结果封装成SearchResult
- package com.atguigu.gulimall.search.service.impl;
-
- import com.alibaba.fastjson.JSON;
- import com.atguigu.common.to.es.SkuEsModel;
- import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
- import com.atguigu.gulimall.search.constant.EsConstant;
- import com.atguigu.gulimall.search.service.MallSearchService;
- import com.atguigu.gulimall.search.vo.SearchParam;
- import com.atguigu.gulimall.search.vo.SearchResult;
- import org.apache.commons.lang.StringUtils;
- import org.apache.lucene.search.join.ScoreMode;
- import org.elasticsearch.action.search.SearchRequest;
- import org.elasticsearch.action.search.SearchResponse;
- import org.elasticsearch.client.RestHighLevelClient;
- import org.elasticsearch.index.query.*;
- import org.elasticsearch.search.SearchHit;
- import org.elasticsearch.search.SearchHits;
- import org.elasticsearch.search.aggregations.AggregationBuilders;
- import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
- import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
- import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
- import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
- import org.elasticsearch.search.aggregations.bucket.terms.Terms;
- import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
- import org.elasticsearch.search.builder.SearchSourceBuilder;
- import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
- import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
- import org.elasticsearch.search.sort.SortOrder;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.stream.Collectors;
-
- /**
- * @Description: MallSearchServiceImpl
- * @Author: WangTianShun
- * @Date: 2020/11/12 16:24
- * @Version 1.0
- */
- @Service
- public class MallSearchServiceImpl implements MallSearchService {
-
- @Autowired
- RestHighLevelClient restHighLevelClient;
-
- //去es进行检索
- @Override
- public SearchResult search(SearchParam param) {
- //动态构建出查询需要的DSL语句
- SearchResult result = null;
- //1、准备检索请求
- SearchRequest searchRequest = buildSearchRequest(param);
- try {
- //2、执行检索请求
- SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
- //分析响应数据封装我们需要的格式
- result = buildSearchResult(response,param);
- } catch (IOException e) {
- e.printStackTrace();
- }
- return result;
- }
-
- /**
- * 准备检索请求
- * 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存),排序,分页,高亮,聚合分析
- * @return
- */
- private SearchRequest buildSearchRequest(SearchParam param) {
- SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
- /**
- * 过滤(按照属性、分类、品牌、价格区间、库存)
- */
- //1、构建bool-query
- BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
- sourceBuilder.query(boolQuery);
- //1.1 must-模糊匹配、
- if (!StringUtils.isEmpty(param.getKeyword())){
- boolQuery.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
- }
- //1.2.1 filter-按照三级分类id查询
- if (null != param.getCatalog3Id()){
- boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
- }
- //1.2.2 filter-按照品牌id查询
- if (null != param.getBrandId() && param.getBrandId().size()>0) {
- boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
- }
- //1.2.3 filter-按照是否有库存进行查询
- if (null != param.getHasStock() ) {
- boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
- }
- //1.2.4 filter-按照区间进行查询 1_500/_500/500_
- RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
- if (!StringUtils.isEmpty(param.getSkuPrice())) {
- String[] prices = param.getSkuPrice().split("_");
- if (prices.length == 1) {
- if (param.getSkuPrice().startsWith("_")) {
- rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
- }else {
- rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
- }
- } else if (prices.length == 2) {
- //_6000会截取成["","6000"]
- if (!prices[0].isEmpty()) {
- rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
- }
- rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
- }
- boolQuery.filter(rangeQueryBuilder);
- }
- //1.2.5 filter-按照属性进行查询
- List<String> attrs = param.getAttrs();
- if (null != attrs && attrs.size() > 0) {
- //attrs=1_5寸:8寸&2_16G:8G
- attrs.forEach(attr->{
- BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
- String[] attrSplit = attr.split("_");
- queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
- String[] attrValues = attrSplit[1].split(":");
- queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
- //每一个必须都得生成一个nested查询
- NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
- boolQuery.filter(nestedQueryBuilder);
- });
- }
- //把以前所有的条件都拿来进行封装
- sourceBuilder.query(boolQuery);
- /**
- * 排序,分页,高亮,
- */
- //2.1 排序 eg:sort=saleCount_desc/asc
- if (!StringUtils.isEmpty(param.getSort())) {
- String[] sortSplit = param.getSort().split("_");
- sourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
- }
-
- //2.2、分页
- sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
- sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
-
- //2.3 高亮highlight
- if (!StringUtils.isEmpty(param.getKeyword())) {
- HighlightBuilder highlightBuilder = new HighlightBuilder();
- highlightBuilder.field("skuTitle");
- highlightBuilder.preTags("<b style='color:red'>");
- highlightBuilder.postTags("</b>");
- sourceBuilder.highlighter(highlightBuilder);
- }
-
- /**
- * 聚合分析
- */
- //5. 聚合
- //5.1 按照品牌聚合
- TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
- //品牌聚合的子聚合
- TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
- TermsAggregationBuilder brand_img_agg = AggregationBuilders.terms("brand_img_agg").field("brandImg");
- brand_agg.subAggregation(brand_name_agg);
- brand_agg.subAggregation(brand_img_agg);
- sourceBuilder.aggregation(brand_agg);
-
- //5.2 按照catalog聚合
- TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
- TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
- catalog_agg.subAggregation(catalog_name_agg);
- sourceBuilder.aggregation(catalog_agg);
-
- //5.3 按照attrs聚合
- NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attr_agg", "attrs");
- //按照attrId聚合
- TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
- //按照attrId聚合之后再按照attrName和attrValue聚合
- TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
- TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
- attr_id_agg.subAggregation(attr_name_agg);
- attr_id_agg.subAggregation(attr_value_agg);
-
- nestedAggregationBuilder.subAggregation(attr_id_agg);
- sourceBuilder.aggregation(nestedAggregationBuilder);
-
- String s = sourceBuilder.toString();
- System.out.println("构建的DSL"+s);
-
- SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
- return request;
-
- }
-
- /**
- * 构建结果数据
- * @param response
- * @return
- */
- private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {
- SearchResult result = new SearchResult();
- //1、返回的所有查询到的商品
- SearchHits hits = response.getHits();
- List<SkuEsModel> esModels = new ArrayList<>();
- if (null != hits.getHits() && hits.getHits().length>0){
- for (SearchHit hit : hits.getHits()) {
- String sourceAsString = hit.getSourceAsString();
- SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
- if (!StringUtils.isEmpty(param.getKeyword())) {
- HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
- esModel.setSkuTitle(skuTitle.fragments()[0].string());
- }
- esModels.add(esModel);
- }
- }
- result.setProducts(esModels);
- //2、当前所有商品涉及到的所有属性
- List<SearchResult.AttrVo> attrVos = new ArrayList<>();
- ParsedNested attr_agg = response.getAggregations().get("attr_agg");
- ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
- for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
- SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
- //1、得到属性的id;
- long attrId = bucket.getKeyAsNumber().longValue();
- //2、得到属性的名字
- String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
- //3、得到属性的所有值
- List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
- String keyAsString = item.getKeyAsString();
- return keyAsString;
- }).collect(Collectors.toList());
- attrVo.setAttrId(attrId);
- attrVo.setAttrName(attrName);
- attrVo.setAttrValue(attrValues);
- attrVos.add(attrVo);
- }
-
- result.setAttrs(attrVos);
- //3、当前所有品牌涉及到的所有属性
- List<SearchResult.BrandVo> brandVos = new ArrayList<>();
- ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
- for (Terms.Bucket bucket : brand_agg.getBuckets()) {
- SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
- //1、得到品牌的id
- long brandId = bucket.getKeyAsNumber().longValue();
- //2、得到品牌的名
- String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
- //3、得到品牌的图片
- String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
- brandVo.setBrandId(brandId);
- brandVo.setBrandName(brandName);
- brandVo.setBrandImg(brandImg);
- brandVos.add(brandVo);
- }
- result.setBrands(brandVos);
- //4、当前商品所涉及的分类信息
- ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
-
- List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
- List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
- for (Terms.Bucket bucket : buckets) {
- SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
- //得到分类id
- String keyAsString = bucket.getKeyAsString();
- catalogVo.setCatalogId(Long.parseLong(keyAsString));
-
- //得到分类名
- ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
- String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
- catalogVo.setCatalogName(catalog_name);
- catalogVos.add(catalogVo);
- }
- result.setCatalogs(catalogVos);
- //===========以上从聚合信息获取到=============
- //5、分页信息-页码
- result.setPageNum(param.getPageNum());
- //6、分页信息-总记录数
- long total = hits.getTotalHits().value;
- result.setTotal(total);
- //7、分页信息-总页码-计算
- int totalPages = total%EsConstant.PRODUCT_PAGESIZE == 0 ?(int) total/EsConstant.PRODUCT_PAGESIZE:((int)total/EsConstant.PRODUCT_PAGESIZE+1);
- result.setTotalPages(totalPages);
- return result;
- }
-
-
- }
1) 基本数据渲染
将商品的基本属性渲染出来
- <div class="rig_tab">
- <!-- 遍历各个商品-->
- <div th:each="product : ${result.getProduct()}">
- <div class="ico">
- <i class="iconfont icon-weiguanzhu"></i>
- <a href="/static/search/#">关注</a>
- </div>
- <p class="da">
- <a th:href="|http://item.gulimall.com/${product.skuId}.html|" >
- <!--图片 -->
- <img class="dim" th:src="${product.skuImg}">
- </a>
- </p>
- <ul class="tab_im">
- <li><a href="/static/search/#" title="黑色">
- <img th:src="${product.skuImg}"></a></li>
- </ul>
- <p class="tab_R">
- <!-- 价格 -->
- <span th:text="'¥' + ${product.skuPrice}">¥5199.00</span>
- </p>
- <p class="tab_JE">
- <!-- 标题 -->
- <!-- 使用utext标签,使检索时高亮不会被转义-->
- <a href="/static/search/#" th:utext="${product.skuTitle}">
- Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
- </a>
- </p>
- <p class="tab_PI">已有<span>11万+</span>热门评价
- <a href="/static/search/#">二手有售</a>
- </p>
- <p class="tab_CP"><a href="/static/search/#" title="谷粒商城Apple产品专营店">谷粒商城Apple产品...</a>
- <a href='#' title="联系供应商进行咨询">
- <img src="/static/search/img/xcxc.png">
- </a>
- </p>
- <div class="tab_FO">
- <div class="FO_one">
- <p>自营
- <span>谷粒商城自营,品质保证</span>
- </p>
- <p>满赠
- <span>该商品参加满赠活动</span>
- </p>
- </div>
- </div>
- </div>
- </div>
2) 筛选条件渲染
将结果的品牌、分类、商品属性进行遍历显示,并且点击某个属性值时可以通过拼接url进行跳转
- <div class="JD_nav_logo">
- <!--品牌-->
- <div class="JD_nav_wrap">
- <div class="sl_key">
- <span>品牌:</span>
- </div>
- <div class="sl_value">
- <div class="sl_value_logo">
- <ul>
- <li th:each="brand: ${result.getBrands()}">
- <!--替换url-->
- <a href="#" th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
- <img src="/static/search/img/598033b4nd6055897.jpg" alt="" th:src="${brand.brandImg}">
- <div th:text="${brand.brandName}">
- 华为(HUAWEI)
- </div>
- </a>
- </li>
- </ul>
- </div>
- </div>
- <div class="sl_ext">
- <a href="#">
- 更多
- <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
- <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
- </a>
- <a href="#">
- 多选
- <i>+</i>
- <span>+</span>
- </a>
- </div>
- </div>
- <!--分类-->
- <div class="JD_pre" th:each="catalog: ${result.getCatalogs()}">
- <div class="sl_key">
- <span>分类:</span>
- </div>
- <div class="sl_value">
- <ul>
- <li><a href="#" th:text="${catalog.getCatalogName()}" th:href="${'javascript:searchProducts("catalogId",'+catalog.catalogId+')'}">0-安卓(Android)</a></li>
- </ul>
- </div>
- </div>
- <!--价格-->
- <div class="JD_pre">
- <div class="sl_key">
- <span>价格:</span>
- </div>
- <div class="sl_value">
- <ul>
- <li><a href="#">0-499</a></li>
- <li><a href="#">500-999</a></li>
- <li><a href="#">1000-1699</a></li>
- <li><a href="#">1700-2799</a></li>
- <li><a href="#">2800-4499</a></li>
- <li><a href="#">4500-11999</a></li>
- <li><a href="#">12000以上</a></li>
- <li class="sl_value_li">
- <input type="text">
- <p>-</p>
- <input type="text">
- <a href="#">确定</a>
- </li>
- </ul>
- </div>
- </div>
- <!--商品属性-->
- <div class="JD_pre" th:each="attr: ${result.getAttrs()}" >
- <div class="sl_key">
- <span th:text="${attr.getAttrName()}">系统:</span>
- </div>
- <div class="sl_value">
- <ul>
- <li th:each="val: ${attr.getAttrValue()}">
- <a href="#"
- th:text="${val}"
- th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+val+'")'}">0-安卓(Android)</a></li>
- </ul>
- </div>
- </div>
- </div>
- function searchProducts(name,value){
- //原来的页面
- var href = location.href + "";
- if (href.indexOf("?") != -1){
- location.href = location.href + "&" +name+ "=" + value;
- }else {
- location.href = location.href + "?" +name+ "=" + value;
- }
- }
- /**
- * @param url 目前的url
- * @param paramName 需要替换的参数属性名
- * @param replaceVal 需要替换的参数的新属性值
- * @param forceAdd 该参数是否可以重复查询(attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏)
- * @returns {string} 替换或添加后的url
- */
- function replaceParamVal(url, paramName, replaceVal,forceAdd) {
- var oUrl = url.toString();
- var nUrl;
- if (oUrl.indexOf(paramName) != -1) {
- if( forceAdd && oUrl.indexOf(paramName+"="+replaceVal)==-1) {
- if (oUrl.indexOf("?") != -1) {
- nUrl = oUrl + "&" + paramName + "=" + replaceVal;
- } else {
- nUrl = oUrl + "?" + paramName + "=" + replaceVal;
- }
- } else {
- var re = eval('/(' + paramName + '=)([^&]*)/gi');
- nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
- }
- } else {
- if (oUrl.indexOf("?") != -1) {
- nUrl = oUrl + "&" + paramName + "=" + replaceVal;
- } else {
- nUrl = oUrl + "?" + paramName + "=" + replaceVal;
- }
- }
- return nUrl;
- };
3) 分页数据渲染
将页码绑定至属性pn,当点击某页码时,通过获取pn值进行url拼接跳转页面
- <div class="filter_page">
- <div class="page_wrap">
- <span class="page_span1">
- <!-- 不是第一页时显示上一页 -->
- <a class="page_a" href="#" th:if="${result.pageNum>1}" th:attr="pn=${result.getPageNum()-1}">
- < 上一页
- </a>
- <!-- 将各个页码遍历显示,并将当前页码绑定至属性pn -->
- <a href="#" class="page_a"
- th:each="page: ${result.pageNavs}"
- th:text="${page}"
- th:style="${page==result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
- th:attr="pn=${page}"
- >1</a>
- <!-- 不是最后一页时显示下一页 -->
- <a href="#" class="page_a" th:if="${result.pageNum<result.totalPages}" th:attr="pn=${result.getPageNum()+1}">
- 下一页 >
- </a>
- </span>
- <span class="page_span2">
- <em>共<b th:text="${result.totalPages}">169</b>页 到第</em>
- <input type="number" value="1" class="page_input">
- <em>页</em>
- <a href="#">确定</a>
- </span>
- </div>
- </div>
- $(".page_a").click(function () {
- var pn=$(this).attr("pn");
- location.href=replaceParamVal(location.href,"pageNum",pn,false);
- console.log(replaceParamVal(location.href,"pageNum",pn,false))
- })
4) 页面排序和价格区间
页面排序功能需要保证,点击某个按钮时,样式会变红,并且其他的样式保持最初的样子;
点击某个排序时首先按升序显示,再次点击再变为降序,并且还会显示上升或下降箭头
页面排序跳转的思路是通过点击某个按钮时会向其class
属性添加/去除desc
,并根据属性值进行url拼接
- <div class="filter_top">
- <div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}">
- <!-- 通过判断当前class是否有desc来进行样式的渲染和箭头的显示-->
- <a sort="hotScore"
- th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
- th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')) ?
- 'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
- 综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
- #strings.endsWith(p,'desc')) ?'↓':'↑' }]]</a>
- <a sort="saleCount"
- th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
- th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount')) ?
- 'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
- 销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
- #strings.endsWith(p,'desc'))?'↓':'↑' }]]</a>
- <a sort="skuPrice"
- th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
- th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice')) ?
- 'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
- 价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
- #strings.endsWith(p,'desc'))?'↓':'↑' }]]</a>
- <a sort="hotScore" class="sort_a">评论分</a>
- <a sort="hotScore" class="sort_a">上架时间</a>
- <!--价格区间搜索-->
- <input id="skuPriceFrom" type="number"
- th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
- style="width: 100px; margin-left: 30px">
- -
- <input id="skuPriceTo" type="number"
- th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
- style="width: 100px">
- <button id="skuPriceSearchBtn">确定</button>
- </div>
- <div class="filter_top_right">
- <span class="fp-text">
- <b>1</b><em>/</em><i>169</i>
- </span>
- <a href="#" class="prev"><</a>
- <a href="#" class="next"> > </a>
- </div>
- </div>
- $(".sort_a").click(function () {
- //添加、剔除desc
- $(this).toggleClass("desc");
- //获取sort属性值并进行url跳转
- let sort = $(this).attr("sort");
- sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
- location.href = replaceParamVal(location.href, "sort", sort,false);
- return false;
- });
价格区间搜索函数
- $("#skuPriceSearchBtn").click(function () {
- var skuPriceFrom = $("#skuPriceFrom").val();
- var skuPriceTo = $("#skuPriceTo").val();
- location.href = replaceParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
- })
5)是否有库存
- <li>
- <a href="#" th:with="check = ${param.hasStock}">
- <input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')}">
- 仅显示有货
- </a>
- </li>
6)关键字搜索修改
- function searchProducts(name, value) {
- //原来的页面
- // var href = location.href + "";
- // if (href.indexOf("?") != -1) {
- // location.href = location.href + "&" + name + "=" + value;
- // } else {
- // location.href = location.href + "?" + name + "=" + value;
- // }
- location.href = replaceParamVal(location.href,name,value,true);
- }
- $("#skuPriceSearchBtn").click(function () {
- //1、拼上价格区间的查询条件
- var skuPriceFrom = $("#skuPriceFrom").val();
- var skuPriceTo = $("#skuPriceTo").val();
- location.href = replaceParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
- });
- /*TODO 是否有库存路径有bug*/
- $("#showHasStock").change(function(){
- if ($(this).prop('checked')){
- location.href = replaceParamVal(location.href, "hasStock", 1, false);
- }else {
- //没选中
- var re = eval('/(hasStock=)([^&]*)/gi');
- location.href = (location.href+"").replace(re,'');
- }
- return false;
- })
5) 面包屑导航
修改gulimall-search的pom
定义springcloud的版本
- <properties>
- <java.version>1.8</java.version>
- <elasticsearch.version>7.4.2</elasticsearch.version>
- <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
- </properties>
添加依赖管理
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-dependencies</artifactId>
- <version>${spring-cloud.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-openfeign</artifactId>
- </dependency>
主启动类添加 开启远程调用
修改com.atguigu.gulimall.search.feign.ProducteFeignService类,代码如下
- package com.atguigu.gulimall.search.feign;
-
- import com.atguigu.common.utils.R;
- import org.springframework.cloud.openfeign.FeignClient;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
-
-
- /**
- * @Description: ProducteFeignService
- * @Author: WangTianShun
- * @Date: 2020/11/15 21:27
- * @Version 1.0
- */
- @FeignClient("gulimall-product")
- public interface ProducteFeignService {
- @GetMapping("/product/attr/info/{attrId}")
- public R attrInfo(@PathVariable("attrId") Long attrId);
- }
修改com.atguigu.common.utils.R类,代码如下
- //利用fastJson进行逆转
- public <T> T getData(String key,TypeReference<T> typeReference) {
- Object data = get(key);
- String s = JSON.toJSONString(data);
- T t = JSON.parseObject(s, typeReference);
- return t;
- }
修改“com.atguigu.gulimall.search.feign.ProducteFeignService” 类,代码如下:
- package com.atguigu.gulimall.search.vo;
-
- import lombok.Data;
-
- /**
- * @Description: AttrResponseVo
- * @Author: WangTianShun
- * @Date: 2020/11/15 21:36
- * @Version 1.0
- */
- @Data
- public class AttrResponseVo {
- /**
- * 属性id
- */
- private Long attrId;
- /**
- * 属性名
- */
- private String attrName;
- /**
- * 是否需要检索[0-不需要,1-需要]
- */
- private Integer searchType;
- /**
- * 属性图标
- */
- private String icon;
- /**
- * 可选值列表[用逗号分隔]
- */
- private String valueSelect;
- /**
- * 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
- */
- private Integer attrType;
- /**
- * 启用状态[0 - 禁用,1 - 启用]
- */
- private Long enable;
- /**
- * 所属分类
- */
- private Long catelogId;
- /**
- * 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
- */
- private Integer showDesc;
-
- private Long attrGroupId;
-
- private String catelogName;
-
- private String groupName;
-
- private Long[] catelogPath;
- }
修改“com.atguigu.gulimall.search.vo.SearchResult” 类,代码如下:
- package com.atguigu.gulimall.search.vo;
-
- import com.atguigu.common.to.es.SkuEsModel;
- import lombok.Data;
-
- import java.util.ArrayList;
- import java.util.List;
-
- /**
- * @Description: SearchResponse
- * @Author: WangTianShun
- * @Date: 2020/11/12 16:49
- * @Version 1.0
- */
- @Data
- public class SearchResult {
-
- /**
- * 查询到的商品信息
- */
- private List<SkuEsModel> products;
-
- private Integer pageNum;//当前页码
-
- private Long total;//总记录数
-
- private Integer totalPages;//总页码
-
- private List<Integer> pageNavs;//导航页码
-
- private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
-
- private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的分类
-
- private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的属性
-
- //=====================以上是返给页面的信息==========================
-
- //面包屑导航数据
- private List<NavVo> navs = new ArrayList<>();
-
- @Data
- public static class NavVo{
- private String navName;
- private String navValue;
- private String link;
- }
-
-
- @Data
- public static class BrandVo{
- private Long brandId;
-
- private String brandName;
-
- private String brandImg;
- }
-
- @Data
- public static class CatalogVo{
- private Long catalogId;
-
- private String catalogName;
-
- private String brandImg;
- }
-
- @Data
- public static class AttrVo{
- private Long attrId;
-
- private String attrName;
-
- private List<String> attrValue;
- }
- }
修改“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl” 类,代码如下:
在封装结果时,将查询的属性值进行封装
- //6、构建面包屑导航功能
- List<String> attrs = param.getAttrs();
- if (attrs != null && attrs.size() > 0) {
- List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
- String[] split = attr.split("_");
- SearchResult.NavVo navVo = new SearchResult.NavVo();
- //6.1 设置属性值
- navVo.setNavValue(split[1]);
- //6.2 查询并设置属性名
- try {
- R r = producteFeignService.attrInfo(Long.parseLong(split[0]));
- if (r.getCode() == 0) {
- AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
- });
- navVo.setNavName(attrResponseVo.getAttrName());
- }else {
- navVo.setNavName(split[0]);
- }
- } catch (Exception e) {
-
- }
- //6.3 设置面包屑跳转链接(当点击该链接时剔除点击属性)
- //取消了这个面包屑以后,我们就要跳转到那个地方,将请求地址的url里面的当前置空
- //拿到所有的查询条件,去掉当前
- String queryString = param.get_queryString();
- String encode = null;
- try {
- encode = URLEncoder.encode(attr, "UTF-8");
- encode = encode.replace("+","20%");//浏览器对空格编码和java不一样
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- String replace = queryString.replace("&attrs=" + encode, "");
- navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
- return navVo;
- }).collect(Collectors.toList());
- result.setNavs(navVos);
- }
- return result;
因为远程调用可以给他添加缓存
页面渲染
- <div class="JD_ipone_one c">
- <!-- 遍历面包屑功能 -->
- <a th:href="${nav.link}" th:each="nav:${result.navs}"><span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> x</a>
- </div>
7) 条件筛选联动
就是将品牌和分类也封装进面包屑数据中,并且在页面进行th:if的判断,当url有该属性的查询条件时就不进行显示了
修改“com.atguigu.gulimall.search.service.impl.MallSearchServiceImpl”类,代码如下:
- //6、构建面包屑导航功能
- List<String> attrs = param.getAttrs();
- if (attrs != null && attrs.size() > 0) {
- List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
- String[] split = attr.split("_");
- SearchResult.NavVo navVo = new SearchResult.NavVo();
- //6.1 设置属性值
- navVo.setNavValue(split[1]);
- //6.2 查询并设置属性名
- try {
- R r = producteFeignService.attrInfo(Long.parseLong(split[0]));
- result.getAttrIds().add(Long.valueOf(split[0]));
- if (r.getCode() == 0) {
- AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
- });
- navVo.setNavName(attrResponseVo.getAttrName());
- }else {
- navVo.setNavName(split[0]);
- }
- } catch (Exception e) {
-
- }
- //6.3 设置面包屑跳转链接(当点击该链接时剔除点击属性)
- //取消了这个面包屑以后,我们就要跳转到那个地方,将请求地址的url里面的当前置空
- //拿到所有的查询条件,去掉当前
- String replace = replaceQueryString(param, attr,"attrs");
- navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
- return navVo;
- }).collect(Collectors.toList());
- result.setNavs(navVos);
- }
- //品牌、分类
- if (null != param.getBrandId() && param.getBrandId().size() > 0){
- List<SearchResult.NavVo> navs = result.getNavs();
- SearchResult.NavVo navVo = new SearchResult.NavVo();
- navVo.setNavName("品牌");
- //TODO 远程查询所有品牌
- R r = producteFeignService.brandInfo(param.getBrandId());
- if (0 == r.getCode()){
- List<BrandVo> brands = r.getData("brands", new TypeReference<List<BrandVo>>() {
- });
- StringBuffer buffer = new StringBuffer();
- String replace = "";
- for (BrandVo brand : brands) {
- buffer.append(brand.getBrandName()+";");
- replace = replaceQueryString(param, brand.getBrandId()+"","attrs");
- }
- navVo.setNavValue(buffer.toString());
- navVo.setLink("http://search.gulimall.com/list.html" + (replace.isEmpty()?"":"?"+replace));
- }
- navs.add(navVo);
- }
- //TODO 分类,不需要导航取消
-
-
- return result;
- }
-
- private String replaceQueryString(SearchParam param, String value,String key) {
- String queryString = param.get_queryString();
- String encode = null;
- try {
- encode = URLEncoder.encode(value, "UTF-8");
- encode = encode.replace("+","20%");//浏览器对空格编码和java不一样
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- return queryString.replace("&"+key+"=" + encode, "");
- }
attrIds作为记录url已经有的属性id
远程调用获取品牌信息
修改“com.atguigu.gulimall.search.feign.ProducteFeignService”类,代码如下:
- @RequestMapping("/product/brand/infos")
- public R brandInfo(@RequestParam("brandIds") List<Long> brandIds);
修改“com.atguigu.gulimall.search.vo.BrandVo”类,代码如下:
- package com.atguigu.gulimall.search.vo;
-
- import lombok.Data;
-
- /**
- * @author WangTianShun
- * @date 2020/10/15 10:00
- */
- @Data
- public class BrandVo {
-
- private Long brandId;
- private String brandName;
- }
修改“com.atguigu.gulimall.product.app.BrandController”类,代码如下:
- /**
- * 信息
- */
- @RequestMapping("/infos")
- public R info(@RequestParam("brandIds") List<Long> brandIds){
- List<BrandEntity> brands = brandService.getBrandsById(brandIds);
-
- return R.ok().put("brands", brands);
- }
修改“com.atguigu.gulimall.product.service.BrandService”类,代码如下:
List<BrandEntity> getBrandsById(List<Long> brandIds);
修改“com.atguigu.gulimall.product.service.impl.BrandServiceImpl”类,代码如下:
- @Override
- public List<BrandEntity> getBrandsById(List<Long> brandIds) {
- List<BrandEntity> brandId = baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id", brandIds));
- return brandId;
- }
- <!--品牌-->
- <div th:if="${#strings.isEmpty(brandid)}" class="JD_nav_wrap">
- <div class="sl_key">
- <span><b>品牌:</b></span>
- </div>
- <div class="sl_value">
- <div class="sl_value_logo">
- <ul>
- <li th:each="brand:${result.brands}">
- <a href="/static/search/#"
- th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
- <img th:src="${brand.brandImg}" alt="">
- <div th:text="${brand.brandName}">
- 华为(HUAWEI)
- </div>
- </a>
- </li>
- </ul>
- </div>
- </div>
- <div class="sl_ext">
- <a href="/static/search/#">
- 更多
- <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
- <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
- </a>
- <a href="/static/search/#">
- 多选
- <i>+</i>
- <span>+</span>
- </a>
- </div>
- </div>
- <!--分类-->
- <div class="JD_nav_wrap">
- <div class="sl_key">
- <span><b>分类:</b></span>
- </div>
- <div class="sl_value">
- <ul>
- <li th:each="catalog:${result.catalogs}">
- <a href="/static/search/#"
- th:href="${'javascript:searchProducts("catalog3",'+catalog.catalogId+')'}"
- th:text="${catalog.catalogName}">5.56英寸及以上</a>
- </li>
- </ul>
- </div>
- <div class="sl_ext">
- <a href="/static/search/#">
- 更多
- <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
- <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
- </a>
- <a href="/static/search/#">
- 多选
- <i>+</i>
- <span>+</span>
- </a>
- </div>
- </div>
- <!--其他所有需要展示的属性-->
- <div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">
- <div class="sl_key">
- <span th:text="${attr.attrName}">屏幕尺寸:</span>
- </div>
- <div class="sl_value">
- <ul>
- <li th:each="val:${attr.attrValue}">
- <a href="/static/search/#"
- th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+val+'")'}"
- th:text="${val}">5.56英寸及以上</a></li>
- </ul>
- </div>
- </div>
1、继承Thread
2、实现Runnable
3、实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
4、线程池
- package com.atguigu.gulimall.search.thread;
-
- import java.util.concurrent.*;
-
- /**
- * @Description: ThreadTest
- * @Author: WangTianShun
- * @Date: 2020/11/16 8:50
- * @Version 1.0
- */
- public class ThreadTest {
- public static ExecutorService service = Executors.newFixedThreadPool(10);
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- System.out.println("main.............start.......");
- /**
- * 1、继承Thread
- *
- * 2、实现Runnable
- *
- * 3、实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
- *
- * 4、线程池
- * 给线程池直接提交任务
- * service.execute(new Runnable01());
- * 创建
- * 1)、Executors
- *
- *总结:
- * 我们以后再业务代码里面,以上三种启动线程的方式都不用。将所有的多线程异步任务都交给线程池执行
- *
- * 区别:
- * 1、2不能得到返回值。3可以获取返回值
- * 1、2、3都不能控制资源
- * 4可以控制资源,性能稳定。
- */
-
- //当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
-
-
- // 1、继承Thread
- // Thread01 thread01 = new Thread01();
- // thread01.start();//启动线程
-
- // 2、实现Runnable
- // Runnable01 runnable01 = new Runnable01();
- // new Thread(runnable01).start();
-
- // 3、实现Callable接口+FutureTask
- FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
- new Thread(futureTask).start();
- //阻塞等待整个线程执行完成,获取返回结果
- Integer integer = futureTask.get();
-
- // 4、线程池
- service.execute(new Runnable01());
-
- System.out.println("main.............end......."+integer);
- }
-
- public static class Thread01 extends Thread{
- @Override
- public void run() {
- System.out.println("当前线程:"+Thread.currentThread().getId());
- int i = 10/2;
- System.out.println("当前运行结果:" + i);
- }
- }
-
- public static class Runnable01 implements Runnable{
-
- @Override
- public void run() {
- System.out.println("当前线程:"+Thread.currentThread().getId());
- int i = 10/2;
- System.out.println("当前运行结果:" + i);
- }
- }
-
- public static class Callable01 implements Callable<Integer>{
-
- @Override
- public Integer call() throws Exception {
- System.out.println("当前线程:"+Thread.currentThread().getId());
- int i = 10/2;
- System.out.println("当前运行结果:" + i);
- return i;
- }
- }
- }
(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:如果队列满了,按照我们指定的拒绝策略,拒绝执行
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
- ThreadPoolExecutor executor = new ThreadPoolExecutor(
- 5,
- 200,
- 10,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(100000),
- Executors.defaultThreadFactory(),
- new ThreadPoolExecutor.AbortPolicy());
newCachedThreadPool
创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在对垒中等待。
newScheduledThreadPool
创建一个定长线程池。支持定势及周期性人去执行
newSingleThreadExecutor
创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务。
(1) runAsync 和 supplyAsync方法
CompletableFuture 提供了四个静态方法来创建一个异步操作。
- public static CompletableFuture<Void> runAsync(Runnable runnable)
- public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
- public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
- public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- System.out.println("main.............start.......");
- // CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
- // System.out.println("当前线程:" + Thread.currentThread().getId());
- // int i = 10 / 2;
- // System.out.println("当前运行结果:" + i);
- // }, executor);
-
- CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
- System.out.println("当前线程:" + Thread.currentThread().getId());
- int i = 10 / 2;
- System.out.println("当前运行结果:" + i);
- return i;
- }, executor);
- Integer integer = future.get();
- System.out.println("main.............end......."+integer);
- }
当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
- //可以处理异常,无返回值
- public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
- public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
- public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
- //可以处理异常,有返回值
- public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
可以看到Action的类型是BiConsumer<? super T,? super Throwable>它可以处理正常的计算结果,或者异常情况。
- public class ThreadTest {
- public static ExecutorService executor = Executors.newFixedThreadPool(10);
-
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- System.out.println("main.............start.......");
- // CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
- // System.out.println("当前线程:" + Thread.currentThread().getId());
- // int i = 10 / 2;
- // System.out.println("当前运行结果:" + i);
- // }, executor);
-
- CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
- System.out.println("当前线程:" + Thread.currentThread().getId());
- int i = 10 / 0;
- System.out.println("当前运行结果:" + i);
- return i;
- }, executor).whenComplete((result,exception)->{
- //虽然能得到异常信息,但是没法修改返回数据
- System.out.println("异步任务完成了...结果是"+result+";异常是"+exception);
- //可以感知异常,同时返回默认值
- }).exceptionally(throwable -> {
- return 10;
- });
- Integer integer = future.get();
- System.out.println("main.............end......."+integer);
- }
handle 是执行任务完成时对结果的处理。 handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。
- public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
- public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
- public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Execut
- /**
- * 方法执行完成后的处理
- */
-
- CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
- System.out.println("当前线程:" + Thread.currentThread().getId());
- int i = 10 / 5;
- System.out.println("当前运行结果:" + i);
- return i;
- }, executor).handle((result,exception)->{
- if (result != null){
- return result*2;
- }
- if (exception != null){
- return 0;
- }
- return 0;
- });
- Integer integer = future.get();
- System.out.println("main.............end......."+integer);
- }
- /**
- * 线程串行化
- * 1)、thenRun 不能获取得到上一步的执行结果
- */
- // CompletableFuture<Void> thenRunAsync = CompletableFuture.supplyAsync(() -> {
- // System.out.println("当前线程:" + Thread.currentThread().getId());
- // int i = 10 / 5;
- // System.out.println("当前运行结果:" + i);
- // return i;
- // }, executor).thenRunAsync(() -> {
- // System.out.println("任务2启动了。。。。");
- // }, executor);
-
- /**
- * 线程串行化
- * 1)、thenRun 不能获取得到上一步的执行结果,无返回值
- * 2)、thenAcceptAsync能接收上一步返回结果,但无返回值
- */
- // CompletableFuture<Void> thenRunAsync = CompletableFuture.supplyAsync(() -> {
- // System.out.println("当前线程:" + Thread.currentThread().getId());
- // int i = 10 / 5;
- // System.out.println("当前运行结果:" + i);
- // return i;
- // }, executor).thenAcceptAsync(res -> {
- // System.out.println("任务2启动了。。。。"+res);
- // }, executor);
-
- /**
- * 线程串行化
- * 1)、thenRun 不能获取得到上一步的执行结果,无返回值
- * 2)、thenAcceptAsync能接收上一步返回结果,但无返回值
- * 3)、thenApplyAsync能接收上一步的返回结果,也有返回值
- */
- CompletableFuture<String> thenApplyAsync = CompletableFuture.supplyAsync(() -> {
- System.out.println("当前线程:" + Thread.currentThread().getId());
- int i = 10 / 5;
- System.out.println("当前运行结果:" + i);
- return i;
- }, executor).thenApplyAsync(res -> {
- System.out.println("任务2启动了。。。。" + res);
- return "hello" + res;
- }, executor);
-
- System.out.println("main.............end......."+ thenApplyAsync.get());
- }
thenCombine 会把两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
两个CompletionStage,都完成了计算才会执行下一步的操作(Runnable)
- public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable action);
- public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action);
- public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor
当两个CompletionStage都执行完成后,把结果一块交给thenAcceptBoth来进行消耗
- public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
- public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
- public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action, Executor executor);
thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
- public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
- public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
- public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
- /**
- * 不能得到两个任务的参数,也无返回结果
- */
- // future01.runAfterBothAsync(future02,()->{
- // System.out.println("任务三开始。。。");
- // },executor);
- /**
- * 能得到两个任务的参数,无返回结果
- */
- // future01.thenAcceptBothAsync(future02,(f1,f2)->{
- // System.out.println("任务三开始。。。之前的结果"+f1+":"+f2);
- // },executor);
-
- /**
- * 能得到两个任务的参数,无返回结果
- */
- CompletableFuture<String> thenCombineAsync = future01.thenCombineAsync(future02, (f1, f2) -> {
- System.out.println("任务三开始。。。之前的结果" + f1 + ":" + f2);
- return f1 + ":" + f2 + "->haha";
- }, executor);
- System.out.println("main.............end......." + thenCombineAsync.get());
- }
两个CompletionStage,任何一个完成了都会执行下一步的操作(Runnable)
- public CompletionStage<Void> runAfterEither(CompletionStage<?> other,Runnable action);
- public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action);
- public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);
两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的消耗操作。
- public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action);
- public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action);
- public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? supe
两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的转化操作。
- public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn);
- public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn);
- public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? sup
- CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
- System.out.println("任务1线程:" + Thread.currentThread().getId());
- int i = 10 / 5;
- System.out.println("任务1线程结束");
- return i;
- }, executor);
- CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
- System.out.println("任务2线程:" + Thread.currentThread().getId());
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("任务2线程结束");
- return "hello";
- }, executor);
thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。
- public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
- public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
- public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage
- CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
- System.out.println("查询商品的属性");
- return "黑色+256g";
- },executor);
-
- CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
- try {
- Thread.sleep(3000);
- System.out.println("查询商品的图片信息");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return "hello.jpg";
- },executor);
-
- CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
- System.out.println("查询商品的介绍");
- return "华为";
- },executor);
- CompletableFuture<Void> allOf = CompletableFuture.allOf(futureAttr, futureImg, futureDesc);
- allOf.get();//等待所有线程执行完
- System.out.println("main.............end......."+futureAttr.get()+"=>"+futureImg.get()+"=>"+futureDesc.get() );
- }
- CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
- System.out.println("查询商品的属性");
- return "黑色+256g";
- },executor);
-
- CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
- try {
- Thread.sleep(3000);
- System.out.println("查询商品的图片信息");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return "hello.jpg";
- },executor);
-
- CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
- System.out.println("查询商品的介绍");
- return "华为";
- },executor);
- // CompletableFuture<Void> allOf = CompletableFuture.allOf(futureAttr, futureImg, futureDesc);
- // allOf.get();//等待所有线程执行完
- CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureAttr, futureImg, futureDesc);
- System.out.println("main.............end......."+anyOf.get() );
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”类,代码如下:
- package com.atguigu.gulimall.product.web;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
-
- /**
- * @Description: ItemController
- * @Author: WangTianShun
- * @Date: 2020/11/16 15:31
- * @Version 1.0
- */
- @Controller
- public class ItemController {
-
- /**
- * 展示当前sku的详情
- * @param skuId
- * @return
- */
- @GetMapping("/{skuId}.html")
- public String skuItem(@PathVariable("skuId") Long skuId){
- System.out.println("准备查询"+skuId+"的详情");
- return "item";
- }
- }
修改gulimall-search的list.html
- <p class="da">
- <a th:href="|http://item.gulimall.com/${product.skuId}.html|">
- <img th:src="${product.skuImg}" class="dim">
- </a>
- </p>
测试效果
模仿京东商品详情页,如下图所示,包括sku基本信息,图片信息,销售属性,图片介绍和规格参数
因此建立以下vo
添加“com.atguigu.gulimall.product.vo.SkuItemVo”类,代码如下:
- package com.atguigu.gulimall.product.vo;
-
- import com.atguigu.gulimall.product.entity.SkuImagesEntity;
- import com.atguigu.gulimall.product.entity.SkuInfoEntity;
- import com.atguigu.gulimall.product.entity.SpuInfoDescEntity;
- import lombok.Data;
-
- import java.util.List;
-
- /**
- * @Description: ItemVo
- * @Author: WangTianShun
- * @Date: 2020/11/16 16:20
- * @Version 1.0
- */
- @Data
- public class SkuItemVo {
-
- //1、sku基本信息获取 pms_sku_info
- SkuInfoEntity info;
-
- //2、sku的图片信息 pms_sku_images
- List<SkuImagesEntity> images;
-
- //3、获取spu的销售属性组合
- List<SkuItemSaleAttrVo> saleAttr;
-
- //4、获取spu的介绍
- SpuInfoDescEntity desc;
-
- //5、获取spu的规格参数信息
- List<SpuItemAttrGroupVo> groupAttrs;
-
- }
添加“com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo”类,代码如下:
- package com.atguigu.gulimall.product.vo;
-
- import lombok.Data;
-
- import java.util.List;
-
- /**
- * @Description: SkuItemSaleAttrVo
- * @Author: WangTianShun
- * @Date: 2020/11/16 21:05
- * @Version 1.0
- */
- @Data
- public class SkuItemSaleAttrVo {
-
- private Long attrId;
-
- private String attrName;
-
- private String attrValues;
- }
添加“com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo”类,代码如下:
- package com.atguigu.gulimall.product.vo;
-
- import lombok.Data;
- import lombok.ToString;
-
- import java.util.List;
-
- /**
- * @Description: SpuItemAttrGroupVo
- * @Author: WangTianShun
- * @Date: 2020/11/16 21:02
- * @Version 1.0
- */
- @ToString
- @Data
- public class SpuItemAttrGroupVo {
-
- private String groupName;
-
- private List<Attr> attrs;
- }
(1) 总体思路
修改“com.atguigu.gulimall.product.web.ItemController”类,代码如下:
- @Controller
- public class ItemController {
-
- @Autowired
- SkuInfoService skuInfoService;
- /**
- * 展示当前sku的详情
- * @param skuId
- * @return
- */
- @GetMapping("/{skuId}.html")
- public String skuItem(@PathVariable("skuId") Long skuId, Model model) {
- SkuItemVo skuItemVo = skuInfoService.item(skuId);
- model.addAttribute("item", skuItemVo);
- return "item";
- }
修改“com.atguigu.gulimall.product.service.SkuInfoService”类,代码如下:
SkuItemVo item(Long skuId);
修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:
- @Override
- public SkuItemVo item(Long skuId) {
- SkuItemVo skuItemVo = new SkuItemVo();
- //1、sku基本信息获取 pms_sku_info
- SkuInfoEntity info = getById(skuId);
- skuItemVo.setInfo(info);
- Long catalogId = info.getCatalogId();
- Long spuId = info.getSpuId();
-
- //2、sku的图片信息 pms_sku_images
- List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
- skuItemVo.setImages(images);
-
- //3、获取spu的销售属性组合
- List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
- skuItemVo.setSaleAttr(saleAttrVos);
-
- //4、获取spu的介绍 pms_spu_info_desc
- SpuInfoDescEntity desc = spuInfoDescService.getById(spuId);
- skuItemVo.setDesc(desc);
-
- //5、获取spu的规格参数信息
- List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
- skuItemVo.setGroupAttrs(attrGroupVos);
- return skuItemVo;
- }
(2) 获取spu的图片信息
修改“com.atguigu.gulimall.product.service.SkuImagesService”类,代码如下:
List<SkuImagesEntity> getImagesBySkuId(Long skuId);
修改“com.atguigu.gulimall.product.service.impl.SkuImagesServiceImpl”类,代码如下:
- @Override
- public List<SkuImagesEntity> getImagesBySkuId(Long skuId) {
-
- List<SkuImagesEntity> imagesEntities = this.baseMapper.selectList(new QueryWrapper<SkuImagesEntity>().eq("sku_id", skuId));
- return imagesEntities;
- }
(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”类,代码如下:
- @Override
- public List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {
-
- SkuSaleAttrValueDao dao = this.baseMapper;
- List<SkuItemSaleAttrVo> saleAttrVos = dao.getSaleAttrsBySpuId(spuId);
- return saleAttrVos;
- }
修改“com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao”类,代码如下:
List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);
修改SkuSaleAttrValueDao.xml
- <select id="getSaleAttrsBySpuId" resultType="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
- SELECT ssav.attr_id attr_id, ssav.attr_name attr_name, GROUP_CONCAT(DISTINCT ssav.attr_value) attr_values FROM pms_sku_info info
- LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
- WHERE info.spu_id=#{spuId}
- GROUP BY ssav.attr_id,ssav.attr_name
-
- </select>
(5) 获取spu的规格参数信息
由于需要通过spuId
和catalogId
查询对应规格参数,所以我们需要通过pms_attr_group表
获得catalogId
和attrGroupName
然后通过 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”类,代码如下:
- @Override
- public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {
- //1、查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
- //1.1、
- AttrGroupDao baseMapper = this.getBaseMapper();
- List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
- return vos;
- }
修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:
- @Mapper
- public interface AttrGroupDao extends BaseMapper<AttrGroupEntity> {
-
- List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
- }
修改AttrGroupDao.xml
- <!--resultType 返回集合里面元素的类型,只要有嵌套属性就要封装自定义结果集-->
- <resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo">
- <result property="groupName" column="attr_group_name"/>
- <collection property="attrs" ofType="com.atguigu.gulimall.product.vo.Attr">
- <result property="attrName" column="attr_name"/>
- <result property="attrValue" column="attr_value"/>
- </collection>
- </resultMap>
- <select id="getAttrGroupWithAttrsBySpuId" resultMap="spuItemAttrGroupVo">
- SELECT pav.spu_id, ag.attr_group_name, ag.attr_group_id, aar.attr_id, attr.attr_name, pav.attr_value
- FROM pms_attr_group ag
- LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
- LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
- LEFT JOIN pms_product_attr_value pav ON pav.attr_id = attr.attr_id
- WHERE ag.catelog_id = 225 AND pav.spu_id = 2
- </select>
1)、添加thymeleaf的名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- <div class="box-yuyue">
- <div class="yuyue-one">
- <img src="/static/item/img/7270ffc3baecdd448958f9f5e69cf60f.png" alt="" /> 预约抢购
- </div>
- <div class="yuyue-two">
- <ul>
- <li>
- <img src="/static/item/img/f64963b63d6e5849977ddd6afddc1db5.png" />
- <span>190103</span> 人预约
- </li>
- <li>
- <img src="/static/item/img/36860afb69afa241beeb33ae86678093.png" /> 预约剩余
- <span id="timer">
-
- </span>
- </li>
- </ul>
- </div>
- </div>
- <div class="box-summary clear">
- <ul>
- <li>京东价</li>
- <li>
- <span>¥</span>
- <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
- </li>
- <li>
- 预约享资格
- </li>
- <li>
- <a href="/static/item/">
- 预约说明
- </a>
- </li>
- </ul>
- </div>
- <li>
- <span th:text="${item.hasStock ? '有货' : '无货'}">无货</span>, 此商品暂时售完
- </li>
- <div class="imgbox">
- <div class="probox">
- <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
- <div class="hoverbox"></div>
- </div>
- <div class="showbox">
- <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
- </div>
- </div>
-
- <div class="box-lh">
-
- <div class="box-lh-one">
- <ul>
- <li th:each="img:${item.images}"th:if="${!#strings.isEmpty(img.imgUrl)}"><img th:src="${img.imgUrl}" /></li>
- </ul>
- </div>
- <div id="left">
- < </div>
- <div id="right">
- >
- </div>
-
- </div>
-
- <div class="boxx-one">
- <ul>
- <li>
- <span>
- <img src="/static/item/img/b769782fe4ecca40913ad375a71cb92d.png" alt="" />关注
- </span>
- <span>
- <img src="/static/item/img/9224fcea62bfff479a6712ba3a6b47cc.png" alt="" />
- 对比
- </span>
- </li>
- <li>
-
- </li>
- </ul>
- </div>
-
- </div>
- <div class="box-attr clear" th:each="attr:${item.saleAttr}">
- <dl>
- <dt>选择[[${attr.attrName}]]</dt>
- <dd th:each="val:${#strings.listSplit(attr.attrValues,',')}">
- <a href="/static/item/#">
- [[${val}]]
- <!-- <img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
- </a>
- </dd>
- </dl>
- </div>
<img class="xiaoguo" th:src="${descp}" th:each="descp:${#strings.listSplit(item.desc.decript,',')}"/>
- <!--商品介绍-->
- <div class="allquanbushop">
- <ul class="shopjieshao">
- <li class="jieshoa" style="background: #e4393c;">
- <a style="color: white;">商品介绍</a>
- </li>
- <li class="baozhuang">
- <a>规格与包装</a>
- </li>
- <li class="baozhang">
- <a href="/static/item/##">售后保障</a>
- </li>
- <li class="pingjia">
- <a href="/static/item/##">商品评价(4万+)</a>
- </li>
- <li class="shuoming">
- <a href="/static/item/##">预约说明</a>
- </li>
-
- </ul>
- <li class="baozhuang actives" id="li2">
- <div class="guiGebox">
- <div class="guiGe" th:each="group:${item.groupAttrs}">
- <h3 th:text="${group.groupName}">主体</h3>
- <dl >
- <div th:each="attr:${group.attrs}">
- <dt th:text="${attr.attrName}">品牌</dt>
- <dd th:text="${attr.attrValue}">华为(HUAWEI)</dd>
- </div>
- </dl>
- </div>
- <div class="package-list">
- <h3>包装清单</h3>
- <p>手机(含内置电池) X 1、5A大电流华为SuperCharge充电器X 1、5A USB数据线 X 1、半入耳式线控耳机 X 1、快速指南X 1、三包凭证 X 1、取卡针 X 1、保护壳 X 1</p>
- </div>
- </div>
- </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”类,代码如下:
- @Data
- public class AttrValueWithSkuIdVo {
- private String attrValue;
-
- private String skuIds;
- }
修改“com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo”类,代码如下:
- @Data
- public class SkuItemSaleAttrVo {
-
- private Long attrId;
-
- private String attrName;
-
- private List<AttrValueWithSkuIdVo> attrValues;
- }
修改SkuItemSaleAttrVo.xml
- <resultMap id="skuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
- <result property="attrId" column="attr_id"/>
- <result property="attrName" column="attr_name"/>
- <collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo">
- <result property="attrValue" column="attr_value"/>
- <result property="skuIds" column="sku_ids"/>
- </collection>
- </resultMap>
-
- <select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
- 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
- LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
- WHERE info.spu_id = #{spuId}
- GROUP BY ssav.attr_id,ssav.attr_name, ssav.attr_value
-
- </select>
效果如下:
- <script>
- $(".sku_attr_value").click(function(){
- //1、点击的元素添加上自定义的属性,为了识别我们是刚才被点击的、
- var skus = new Array();
- $(this).addClass("clicked");
- var curr = $(this).attr("skus").split(",");
- //当前被点击的所有sku组合数组放进去
- skus.push(curr);
- //去掉同一行的所有的checked
- $(this).parent().parent().find(".sku_attr_value").removeClass("checked");
- $("a[class='sku_attr_value checked']").each(function(){
- skus.push($(this).attr("skus").split(","));
- });
- console.log(skus);
- //2、取出他们的交集,得到skuId
- // console.log($(skus[0]).filter(skus[1])[0]);
- var filterEle = skus[0];
- for (var i = 1; i < skus.length; i++){
- filterEle = $(filterEle).filter(skus[i])
- }
- console.log(filterEle[0]);
- //3、跳转
- location.href = "http://item.gulimall.com/"+filterEle[0]+".html";
- });
- $(function(){
- $(".sku_attr_value").parent().css({"border":"solid 1px #ccc"});
- $("a[class='sku_attr_value checked']").parent().css({"border":"solid 1px red"});
- //方法二
- // $(".sku_attr_value").parent().css({"border":"solid 1px #ccc"});
- // $(".sku_attr_value.checked").parent().css({"border":"solid 1px red"});
- })
- </script>
通过控制class中是否包换checked
属性来控制显示样式,因此要根据skuId
判断
- <div class="box-attr-3">
- <div class="box-attr clear" th:each="attr:${item.saleAttr}">
- <dl>
- <dt>选择[[${attr.attrName}]]</dt>
- <dd th:each="vals:${attr.attrValues}">
- <a
- th:attr="skus=${vals.skuIds},class=${#lists.contains(#strings.listSplit(vals.skuIds,','),item.info.skuId.toString())?'sku_attr_value checked' : 'sku_attr_value'}">
- [[${vals.attrValue}]]
- <!-- <img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
- </a>
- </dd>
- </dl>
- </div>
添加线程池属性配置类,并注入到容器中
- package com.atguigu.gulimall.product.config;
-
- import lombok.Data;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- import org.springframework.stereotype.Component;
-
- /**
- * @Description: ThreadPoolConfigProperties
- * @Author: WangTianShun
- * @Date: 2020/11/17 13:40
- * @Version 1.0
- */
- @ConfigurationProperties(prefix = "gulimall.thread")
- @Component
- @Data
- public class ThreadPoolConfigProperties {
- private Integer core;
- private Integer maxSize;
- private Integer keepAliveTime;
- }
添加application.yml
- #线程池属性的配置
- gulimall:
- thread:
- core: 20
- max-size: 200
- keep-alive-time: 10
线程池配置,获取线程池的属性值这里直接调用与配置文件相对应的属性配置类
- package com.atguigu.gulimall.product.config;
-
- import org.springframework.boot.context.properties.EnableConfigurationProperties;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- import java.util.concurrent.Executors;
- import java.util.concurrent.LinkedBlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @Description: MyThreadConfig
- * @Author: WangTianShun
- * @Date: 2020/11/17 13:31
- * @Version 1.0
- */
- //如果ThreadPoolConfigProperties.class类没有加上@Component注解,那么我们在需要的配置类里开启属性配置的类加到容器中
- //@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
- @Configuration
- public class MyThreadConfig {
- @Bean
- public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
- return new ThreadPoolExecutor(pool.getCore(),
- pool.getMaxSize(),
- pool.getKeepAliveTime(),
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(100000),
- Executors.defaultThreadFactory(),
- new ThreadPoolExecutor.AbortPolicy());
- }
- }
为了使我们的任务进行的更快,我们可以让查询的各个子任务多线程执行,但是由于各个任务之间可能有相互依赖的关系,因此就涉及到了异步编排。
在这次查询中spu的销售属性、介绍、规格参数信息都需要spuId
,因此依赖sku基本信息的获取,所以我们要让这些任务在1之后运行。因为我们需要1运行的结果,因此调用thenAcceptAsync()
可以接受上一步的结果且没有返回值。
最后时,我们需要调用get()
方法使得所有方法都已经执行完成
- @Autowired
- ThreadPoolExecutor executor;
- @Override
- public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
- SkuItemVo skuItemVo = new SkuItemVo();
-
- // 使用异步编排
- CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
- //1、sku基本信息获取 pms_sku_info
- SkuInfoEntity info = getById(skuId);
- skuItemVo.setInfo(info);
- return info;
- }, executor);
-
- CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
- //3、获取spu的销售属性组合
- List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
- skuItemVo.setSaleAttr(saleAttrVos);
- }, executor);
-
- CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
- //4、获取spu的介绍 pms_spu_info_desc
- SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
- skuItemVo.setDesc(desc);
- }, executor);
-
- CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
- //5、获取spu的规格参数信息
- List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
- skuItemVo.setGroupAttrs(attrGroupVos);
- }, executor);
-
- CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
- //2、sku的图片信息 pms_sku_images
- List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
- skuItemVo.setImages(images);
- }, executor);
-
- //等待所有任务都完成
- CompletableFuture.allOf(saleAttrFuture, descFuture, baseAttrFuture, imageFuture).get();
-
- return skuItemVo;
- }
未完,请看下一篇
谷粒商城-个人笔记(高级篇三):https://blog.csdn.net/wts563540/article/details/109713677
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。