ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-01-14 19:57:39  阅读:186  来源: 互联网

标签:result 缓存 return spuId 解决方案 goodsId 详情 public 页面


需求分析

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

在这里插入图片描述


解决方案

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

在这里插入图片描述


商品详情页面静态化

1、建Module:supergo_page

2、改pom

<?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、启动类

@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

# 端口
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页面,路径:\main\resources\templates\hello.html

<!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>

测试类:

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 :展示尾部

引入路径:\main\resources\templates\


7、商品操作微服务

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

supergo_manager_service8001

接口:

public interface ItemService extends BaseService<Item> {

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

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

impl:

@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:

@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
@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并添加生成静态页面的业务逻辑

@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

@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、测试

浏览器输入:

http://localhost:9004/html/build/149187842867925

在这里插入图片描述

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

在这里插入图片描述


商品库存数据缓存

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

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

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

1、改pom

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

2、改yml

spring:
  redis:
    host: 127.0.0.1
    password: 123456
    port: 6379

3、service

接口:

@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

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

5、测试

http://localhost:9004/goods/stock/149137842867935

在这里插入图片描述


6、缓存处理流程分析

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

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

在这里插入图片描述


缓存穿透

现象

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

解决方案

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

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

代码实现

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;
}

缓存击穿

现象

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

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

解决方案

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

情况二:加互斥锁

代码实现

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

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

情况二:

全局锁

@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已经从数据库查找完毕,并将数据存入缓存中

/**
     * 查询商品库存
     *
     * @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

vim goods.conf 
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、访问测试

访问:

http://192.168.77.138/149187842867925.html

在这里插入图片描述

http://192.168.77.138/149137842867935.html

在这里插入图片描述

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


RabbitMQ实现消息队列

在这里插入图片描述

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


标签:result,缓存,return,spuId,解决方案,goodsId,详情,public,页面
来源: https://blog.csdn.net/XJ0927/article/details/112631980

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有