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

9_商品详情页面解决方案需求分析当搜索商品时,显示商品的详细信息,同时选择不同的sku,进行不同的数据显示解决方案商家更改数据微服务,通过消息队列MQ监听到发生变化,微服务调用者使用Thymeleaf模板,生成相应的静态页面,储存在本地磁盘,当用户发送请求到微服务时,使用nginx技术进行相应页面的返回商品详情页面静态化1、建Module:supergo_page2、改pom<?xmlversion=”1.0″encoding=”UTF-8″?><projectxmlns=”http

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

需求分析

当搜索商品时,显示商品的详细信息,同时选择不同的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发布消息,搜索微服务就新增文档,静态页面微服务就生成相应的静态页面。


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 光棍节程序员闯关秀writeup[通俗易懂]

    光棍节程序员闯关秀writeup[通俗易懂]答题链接https://1111.segmentfault.com/第一关首先当然是右键查看源码啊点击链接进入下一关第二关还是老样子,右键查看源码这个key是要放在URL链接里敲回车的第三关根据前两关这个难度,第三关估计在请求头或者响应头里,先开burp刷新,拦截返回包拿到flag第四…

  • 【PyTorch】详解pytorch中nn模块的BatchNorm2d()函数

    【PyTorch】详解pytorch中nn模块的BatchNorm2d()函数基本原理在卷积神经网络的卷积层之后总会添加BatchNorm2d进行数据的归一化处理,这使得数据在进行Relu之前不会因为数据过大而导致网络性能的不稳定,BatchNorm2d()函数数学原理如下:BatchNorm2d()内部的参数如下:1.num_features:一般输…

  • RTCP「建议收藏」

    RTCP「建议收藏」RTCPRTCP协议将控制包周期发送给所有连接者,应用与数据包相同的分发机制。低层协议提供数据与控制包的复用,如使用单独的UDP端口号。RTCP执行下列四大功能:(1)主要是提供数据发布的质量反馈。R

  • vue3.0计算属性_vue计算属性什么时候执行

    vue3.0计算属性_vue计算属性什么时候执行前言一般情况下属性都是放到data中的,但是有些属性可能是需要经过一些逻辑计算后才能得出来,那么我们可以把这类属性变成计算属性。比如以下:<divid="example&quot

  • 虚拟机怎么退出vi编辑模式_手机系统占用30个g正常吗

    虚拟机怎么退出vi编辑模式_手机系统占用30个g正常吗ESC键->冒号->小写q->(可选:不保存退出加个惊叹号!)->(可选:如文件需保存加个小写w)  当编辑完文件,准备退出Vi返回到shell时,可以使用以下几种方法之一。   (1)在命令模式中,连按两次大写字母Z,若当前编辑的文件曾被修改过,则Vi保存该文件后退出,返回到shell;若当前编辑的文件没被修改过,则Vi直接退出,返回到shel

  • Pycharm的python interpreter选择「建议收藏」

    Pycharm的python interpreter选择「建议收藏」初学python时我在电脑装idle,装上了ANACONDA,也裸装了python3.9(也就是说我电脑上有两个独立的python,一个是python3.9,另一个是装在Anaconda里面的python3.7。在我装上Pycharm后,Pycharm自动使用Anaconda提供的环境,虽然Anaconda的包很全,但还是有缺少的包,当我使用pip命令安装需要的包时,确自动安装到了python3.9的安装目录下,而且命令行运行python时只运行python3.9而不是Anaconda里面的python

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号