9_商品详情页面解决方案
作者:互联网
需求分析
当搜索商品时,显示商品的详细信息,同时选择不同的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模板,将页头、页脚都可以拆分出来作为一个独立的模板,被
其他模板所引用
-
head.html :展示头部
-
foot.html :展示内容部分
-
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