9_商品详情页面解决方案

2022-09-12 20:58:13 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

需求分析

当搜索商品时,显示商品的详细信息,同时选择不同的sku,进行不同的数据显示


解决方案

商家更改数据微服务,通过消息队列MQ监听到发生变化,微服务调用者使用Thymeleaf模板,生成相应的静态页面,储存在本地磁盘,当用户发送请求到微服务时,使用nginx技术进行相应页面的返回


商品详情页面静态化

1、建Module:supergo_page
2、改pom
代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>supergo_parent1</artifactId>
        <groupId>com.supergo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>supergo_page</artifactId>
    <dependencies>
        <!--feign服务-->
        <dependency>
            <groupId>com.supergo</groupId>
            <artifactId>supergo_manager_feign</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
		  <!-- thymeleaf 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>com.supergo</groupId>
            <artifactId>supergo-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 加入 redis 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

</project>
3、启动类
代码语言:javascript复制
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除数据库配置
@EnableEurekaClient
@EnableFeignClients("com.supergo.manager.feign") //feign服务调用
public class PageApplication9004 { 
   
    public static void main(String[] args) { 
   
        SpringApplication.run(PageApplication9004.class, args);
    }
}
4、建yml
代码语言:javascript复制
# 端口
server:
  port: 9004
eureka:
  client:
    register-with-eureka: true # 表示将自己注册到 eureka server ,默认为 true
    fetch-registry: true # 表示是否从eureka server 抓取已有的注册信息,默认为true。单节点为所谓,集群必须为 true,才能配合ribbon使用负载均衡
    service-url:
      # 单机版:只用注册进一个服务中心【defaultZone: http://127.0.0.1:7001/eureka/】
      defaultZone: http://eureka7001.com:7001/eureka/
      # 集群版:需要同时注册进每个注册中心
  # defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com/eureka/
  # 显示的服务主机名称
  instance:
    prefer-ip-address: true # 访问路径显示 ip【统一:方便调试】
    ip-address: 127.0.0.1
    instance-id: ${ 
   eureka.instance.ip-address}.${ 
   server.port}
    lease-renewal-interval-in-seconds: 3
    lease-expiration-duration-in-seconds: 10

#actuator服务监控与管理
management:
  endpoint:
    #开启端点
    shutdown:
      enabled: true
    health:
      show-details: always
  # 加载所有的端点
  endpoints:
    web:
      exposure:
        include: "*"

# thymeleaf 配置
spring:
  thymeleaf:
    prefix: classpath:/templates/ # 指定模板所在的目录
    check-template-location: true # 检查模板路径是否存在
    cache: false #cache: 是否缓存,开发模式下设置为false,避免改了模板还要重启服务器,线上设置为true,可以提高性能。
    suffix: .html
    mode: HTML5
5、静态化测试

编写html页面,路径:mainresourcestemplateshello.html

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title></head>
<body>
    <h1 th:text="${hello}"></h1>
</body>
</html>

测试类:

代码语言:javascript复制
package com.supergo.page;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.io.FileWriter;
import java.io.IOException;

/** * @Author: xj0927 * @Description: * @Date Created in 2021-01-05 15:48 * @Modified By: */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = PageApplication9004.class)
public class PageTest { 
   

    @Autowired
    private TemplateEngine engine;

    @Test
    public void test() throws IOException { 
   
        //测试时:使用context,springboot中可用使用model
        Context context = new Context();

        //存值
        context.setVariable("hello", "hello thymeleaf....");

        //将静态文件输入到磁盘[磁盘路径]
        FileWriter writer = new FileWriter("G:\temp\page\hello.html");
        //将thymeleaf里面的内容输出到磁盘
        //参数1:thymeleaf里面的值,参数2:取得thymeleaf,参数3:输出位置
        engine.process("hello", context, writer);

        //关闭文件
        writer.close();
    }
}

此时,就会将html页面保存到磁盘中。


6、引入thymeleaf模板

基于商品详情页面的静态页面创建thymeleaf模板,将页头、页脚都可以拆分出来作为一个独立的模板,被

其他模板所引用

  1. head.html :展示头部
  2. foot.html :展示内容部分
  3. item.html :展示尾部

引入路径:mainresourcestemplates


7、商品操作微服务

在supergo-manager中增加商品操作微服务,调用tk mybatis实现持久化操作

supergo_manager_service8001

接口:

代码语言:javascript复制
public interface ItemService extends BaseService<Item> { 
   

    //sku列表接口
    public List<Item> skuList(Long goodsId);

    //查询库存接口
    public int getItemStock(long itemId);
}

impl:

代码语言:javascript复制
@Service
public class ItemServiceImpl extends BaseServiceImpl<Item> implements ItemService { 
   

    @Autowired
    private ItemMapper itemMapper;

    /**** * 查询对应的SKU列表 * @param goodsId * @return */
    public List<Item> skuList(Long goodsId) { 
   
        //select * from tb_item where goods_id=? and status=1 order by is_default desc
        Example example = new Example(Item.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("goodsId", goodsId);
        criteria.andEqualTo("status", "1");
        //设置排序
        example.orderBy("isDefault").desc();
        return itemMapper.selectByExample(example);
    }

    //查库存
    @Override
    public int getItemStock(long itemId) { 
   
        Item item = new Item();
        item.setId(itemId);
        Item result = itemMapper.selectOne(item);
        return result.getStockCount();
    }

}

controller:

代码语言:javascript复制
@RestController
public class PageController { 
   

    @Autowired
    private PageService pageService;

    @GetMapping("/html/build/{goodsId}")
    public HttpResult buildHtml(@PathVariable Long goodsId) throws IOException { 
   
        HttpResult httpResult = pageService.buildGoodsPage(goodsId);
        return httpResult;
    }

    @GetMapping("/goods/stock/{goodsId}")
    public Map getGoodsStock(@PathVariable long goodsId) { 
   
        Map result = pageService.getItemStocks(goodsId);
        return result;
    }

}
supergo_manager_feign
代码语言:javascript复制
@FeignClient("supergo-manager")
public interface ApiGoodsFeign { 
   

    @GetMapping("/goods/{goodsId}")
    public Goods getGoodsById(@PathVariable("goodsId") long goodsId);

    @GetMapping("/goods/desc/{goodsId}")
    public Goodsdesc getGoodsDescById(@PathVariable("goodsId") long goodsId);

    @GetMapping("/goods/item/{goodsId}")
    public List<Item> getItemList(@PathVariable("goodsId") long goodsId);

}

8、service

创建PageService并添加生成静态页面的业务逻辑

代码语言:javascript复制
@Service
public class PageService { 
   

    @Autowired
    private TemplateEngine templateEngine; //thymeleaf提供的对象
    @Autowired
    private ApiGoodsFeign goodsFeign;
    @Autowired
    private ApiItemcatFeign itemCatFeign;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private GoodsLock goodsLock;

    /** * 生成静态页面业务逻辑 * * @param goodsId 商品id * @return 生成结果 * @throws IOException */
    public HttpResult buildGoodsPage(long goodsId) throws IOException { 
   
        //输出静态文件位置
        FileWriter fileWriter = new FileWriter("G:\temp\goods\"   goodsId   ".html");

        Context context = getGoodsData(goodsId);
        templateEngine.process("item", context, fileWriter);
        fileWriter.close();
        return HttpResult.ok();
    }

    public Context getGoodsData(Long goodsId) { 
   
        Context context = new Context();
        // Goods、
        Goods goods = goodsFeign.getGoodsById(goodsId);
        System.out.println(goods);
        //查询商品的分类 3个分类
        Itemcat itemCat1 = itemCatFeign.getItemCatById(goods.getCategory1Id());
        Itemcat itemCat2 = itemCatFeign.getItemCatById(goods.getCategory2Id());
        Itemcat itemCat3 = itemCatFeign.getItemCatById(goods.getCategory3Id());
        // GoodsDesc、
        //GoodsDesc goodsDesc = goodsDescMapper.selectByPrimaryKey(goodsId);
        Goodsdesc goodsDesc = goodsFeign.getGoodsDescById(goodsId);

        //取图片列表
        String jsonImages = goodsDesc.getItemImages();
        if (StringUtils.isNotBlank(jsonImages)) { 
   
            try { 
   
                List<Map> imagesList = JSON.parseArray(jsonImages, Map.class);
                context.setVariable("itemImageList", imagesList);
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
        }
        //取属性信息[spu]
        String jsonCustomAttributeItems = goodsDesc.getCustomAttributeItems();
        if (StringUtils.isNotBlank(jsonCustomAttributeItems)) { 
   
            try { 
   
                List<Map> customAttributeList = JSON.parseArray(jsonCustomAttributeItems, Map.class);
                context.setVariable("customAttributeList", customAttributeList);
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
        }
        //提取规格数据[sku笛卡尔积]
        String jsonSpecificationItems = goodsDesc.getSpecificationItems();
        if (StringUtils.isNotBlank(jsonSpecificationItems)) { 
   
            try { 
   
                List<Map> specificationItems = JSON.parseArray(jsonSpecificationItems, Map.class);
                context.setVariable("specificationList", specificationItems);
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
        }
        // List<Item>[sku列表]
        List<Item> itemList = goodsFeign.getItemList(goodsId);
        context.setVariable("goods", goods);
        context.setVariable("goodsDesc", goodsDesc);
        context.setVariable("itemCat1", itemCat1);
        context.setVariable("itemCat2", itemCat2);
        context.setVariable("itemCat3", itemCat3);
        context.setVariable("itemList", itemList);
        return context;
    }
}
9、controller
代码语言:javascript复制
@RestController
public class PageController { 
   

    @Autowired
    private PageService pageService;

    @GetMapping("/html/build/{goodsId}")
    public HttpResult buildHtml(@PathVariable Long goodsId) throws IOException { 
   
        HttpResult httpResult = pageService.buildGoodsPage(goodsId);
        return httpResult;
    }

}
10、测试

浏览器输入:

代码语言:javascript复制
http://localhost:9004/html/build/149187842867925

打开生成在磁盘中的静态页面,便可以将对应数据应用到html页面上


商品库存数据缓存

库存是一个实时变化的量,我们不能生成静态文件时直接输出库存

应该是在静态页面展示完毕后,查询当前的库存数量

也就是当页面加载完毕后通过ajax方式查询库存,并显示到页面

1、改pom
代码语言:javascript复制
<!-- 加入 redis 支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、改yml
代码语言:javascript复制
spring:
  redis:
    host: 127.0.0.1
    password: 123456
    port: 6379
3、service

接口:

代码语言:javascript复制
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private GoodsLock goodsLock;

public Map<Object, Object> getItemStocks(Long spuId) { 
   
    //查询缓存[先查看redis中是否存在]
    Map<Object, Object> entries = redisTemplate.opsForHash().entries("goodsstock:"   spuId);
    if (entries != null && !entries.isEmpty()) { 
   
        return entries;
    }
    //查询数据库
    List<Item> itemList = goodsFeign.getItemList(spuId);

    Map<Object, Object> result = new HashMap();
    itemList.forEach(item -> { 
   
        result.put(item.getId(), item.getNum());
        //添加到缓存
        redisTemplate.opsForHash().put("goodsstock:"   spuId, item.getId().toString(), item.getNum().toString());
    });

    //设置缓存过期时间
    redisTemplate.expire("goodsstock:"   spuId, 1, TimeUnit.DAYS);
    
    //返回结果
    return result;
}
4、controller
代码语言:javascript复制
@GetMapping("/goods/stock/{goodsId}")
public Map getGoodsStock(@PathVariable long goodsId) { 
   
    Map result = pageService.getItemStocks(goodsId);
    return result;
}
5、测试
代码语言:javascript复制
http://localhost:9004/goods/stock/149137842867935

6、缓存处理流程分析

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结

果,数据库也没取到,那直接返回空结果


缓存穿透

现象

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大

解决方案

情况一:接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截

情况二:从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

代码实现
代码语言:javascript复制
public Map<Object, Object> getItemStocks(Long spuId) { 
   


    //防止缓存穿透,非法id直接返回
    if (spuId <= 0) { 
   
        Map result = new HashedMap();
        result.put("0", "0");
        return result;
    }

    //查询缓存[先查看redis中是否存在]
    Map<Object, Object> entries = redisTemplate.opsForHash().entries("goodsstock:"   spuId);
    if (entries != null && !entries.isEmpty()) { 
   
        return entries;
    }

    //查询数据库
    List<Item> itemList = goodsFeign.getItemList(spuId);
    //判断商品是否取到库存数据,添加空值缓存,防止缓存穿透
    if (itemList == null || itemList.isEmpty()) { 
   
        redisTemplate.opsForHash().put("goodsstock:"   spuId, "0", "0");
        redisTemplate.expire("goodsstock:"   spuId, 5, TimeUnit.MINUTES);
        Map result = new HashedMap();
        result.put("0", "0");
        return result;
    }

    Map<Object, Object> result = new HashMap();
    itemList.forEach(item -> { 
   
        result.put(item.getId(), item.getNum());
        //添加到缓存
        redisTemplate.opsForHash().put("goodsstock:"   spuId, item.getId().toString(), item.getNum().toString());
    });

    //设置缓存过期时间
    redisTemplate.expire("goodsstock:"   spuId, 1, TimeUnit.DAYS);
    return result;
}

缓存击穿

现象

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没

读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案

情况一:设置热点数据永远不过期

情况二:加互斥锁

代码实现

情况一:每次从缓存中拿数据,就将缓存的过期时间重置,这样就能保证缓存永不过期

代码语言:javascript复制
//查询缓存[先查看redis中是否存在]
Map<Object, Object> entries = redisTemplate.opsForHash().entries("goodsstock:"   spuId);
if (entries != null && !entries.isEmpty()) { 
   
    //可以在此重置过期时间
    redisTemplate.expire("goodsstock:"   spuId, 1, TimeUnit.DAYS);
    return entries;
}

情况二:

全局锁

代码语言:javascript复制
@Component
public class GoodsLock { 
   
    private ConcurrentHashMap<Long, ReentrantLock> lockMap = new ConcurrentHashMap<>();

    public ReentrantLock getLock(Long goodsId) { 
   
        return lockMap.getOrDefault(goodsId, new ReentrantLock());
    }

    public void removeLock(Long goodsId) { 
   
        lockMap.remove(goodsId);
    }
}

互斥锁:A线程查询数据,发现缓存中没有,需要去数据库中查找,此时给它上一把锁,只有获得该锁的线程才能访问数据库,若此时线程也查询该数据,但此时A线程还没有执行完毕,就让它等待一会,然后再去缓存中查找一下,此时可能A已经从数据库查找完毕,并将数据存入缓存中

代码语言:javascript复制
/** * 查询商品库存 * * @param spuId * @return */
public Map<Object, Object> getItemStocks(Long spuId) { 
   


    //防止缓存穿透,非法id直接返回
    if (spuId <= 0) { 
   
        Map result = new HashedMap();
        result.put("0", "0");
        return result;
    }

    //查询缓存[先查看redis中是否存在]
    Map<Object, Object> entries = redisTemplate.opsForHash().entries("goodsstock:"   spuId);
    if (entries != null && !entries.isEmpty()) { 
   
        //可以在此重置过期时间
        redisTemplate.expire("goodsstock:"   spuId, 1, TimeUnit.DAYS);
        return entries;
    }

    //保证同时只能有一个线程查询同一个商品
    ReentrantLock lock = goodsLock.getLock(spuId);
    if (lock.tryLock()) { 
   
        //查询数据库
        List<Item> itemList = goodsFeign.getItemList(spuId);
        //判断商品是否取到库存数据,添加空值缓存,防止缓存穿透
        if (itemList == null || itemList.isEmpty()) { 
   
            redisTemplate.opsForHash().put("goodsstock:"   spuId, "0", "0");
            redisTemplate.expire("goodsstock:"   spuId, 5, TimeUnit.MINUTES);
            Map result = new HashedMap();
            result.put("0", "0");
            return result;
        }

        Map<Object, Object> result = new HashMap();
        itemList.forEach(item -> { 
   
            result.put(item.getId(), item.getNum());
            //添加到缓存
            redisTemplate.opsForHash().put("goodsstock:"   spuId, item.getId().toString(), item.getNum().toString());
        });

        //设置缓存过期时间
        redisTemplate.expire("goodsstock:"   spuId, 1, TimeUnit.DAYS);

        //解锁
        lock.unlock();
        goodsLock.removeLock(spuId);
        //返回结果
        return result;
    } else { 
   
        try { 
   
            Thread.sleep(100);
        } catch (InterruptedException e) { 
   
            e.printStackTrace();
        }
        return getItemStocks(spuId);
    }
}

缓存雪崩

现象

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿

不同的是,缓存击穿指并发查同一条数据,缓存雪崩是大量不同数据都过期了,很多数据都查不到从而查数据库

解决方案

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

如果缓存数据库是分布式部署,将热点数据均匀分布在不同得缓存数据库中。

设置热点数据永远不过期。


Nginx 获取静态资源

1、存放静态资源

将生成的html页面和样式文件一起打包放在nginx的html目录下

2、配置nginx

路径:

nginx/conf.d

代码语言:javascript复制
vim goods.conf 
代码语言:javascript复制
server { 
    listen 80; # 监听的端口
    server_name localhost; # 域名或ip
    location / { # 访问路径配置
       root /usr/share/nginx/html;# 根目录
       index index.html; # 默认首页
    }
    error_page 500 502 503 504 /50x.html; # 错误页面
    location = /50x.html { 
    	root html; 
    }
}
3、访问测试

访问:

代码语言:javascript复制
http://192.168.77.138/149187842867925.html
代码语言:javascript复制
http://192.168.77.138/149137842867935.html

每次点击不同的配置,发送不同的请求,即可访问到对应的静态页面,再使用ajax发送请求到服务端获取商品库存


RabbitMQ实现消息队列

商家新增商品,使用RabbitMQ发布消息,搜索微服务和静态页面微服务同时监听rabbimq,一旦mq发布消息,搜索微服务就新增文档,静态页面微服务就生成相应的静态页面。


发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/152843.html原文链接:https://javaforall.cn

0 人点赞