1.商品详情的方案
1.1.网页静态化方案
1、创建商品详情的thymeleaf模板
2、创建服务接收消息,收到消息后创建静态页面
3、搭建nginx服务器,返回静态页面
1.2.redis缓存商品信息方案
使用redis做缓存,业务逻辑:
1、根据商品id到redis中查询
查得到,直接返回
2、查不到,查询mysql,
数据放到redis中
设置缓存的有效期一天的时间,可以根据实际情况调整。
需要使用String类型来保存商品数据,可以加前缀对redis中的key进行归类:
ITEM_INFO:123456:BASE:{}
ITEM_INFO:123456:DESC:{}
ITEM_INFO:123456:PARAM:{}
2.查询商品详情
2.1.创建power_shop_detail
2.1.1.创建工程
2.1.2.pom.xml
<?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>power_shop_parent</artifactId>
<groupId>com.bjpowernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>power_shop_detail</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.bjpowernode</groupId>
<artifactId>power_shop_item_feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
2.1.3.application.yml
server:
port: 8094
spring:
application:
name: power-shop-detail
cloud:
nacos:
discovery:
server-addr: 192.168.204.129:8848
2.1.4.logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="${catalina.base:-.}/logs/" />
<!-- 控制台输出 -->
<appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志输出编码 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</layout>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/server.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</layout>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!--myibatis log configure-->
<!-- 日志输出级别 -->
<root level="DEBUG">
<appender-ref ref="Stdout" />
<appender-ref ref="RollingFile" />
</root>
<!--日志异步到数据库 -->
<!-- <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
日志异步到数据库
<connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
连接池
<dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
<driverClass>com.mysql.jdbc.Driver</driverClass>
<url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
<user>root</user>
<password>root</password>
</dataSource>
</connectionSource>
</appender> -->
</configuration>
2.1.5.创建启动类
package com.bjpowernode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class PowerShopDetailApp {
public static void main(String[] args) {
SpringApplication.run(PowerShopDetailApp.class, args);
}
}
2.2.查询商品信息
2.2.1.power_shop_item
2.2.1.1.application.yml
#商品详情
ITEM_INFO: ITEM_INFO
BASE: BASE
DESC: DESC
PARAM: PARAM
ITEM_INFO_EXPIRE: 86400
2.2.1.2.service
- 修改ItemServiceImpl
@Value("${ITEM_INFO}")
private String ITEM_INFO;
@Value("${BASE}")
private String BASE;
@Value("${DESC}")
private String DESC;
@Value("${ITEM_INFO_EXPIRE}")
private Integer ITEM_INFO_EXPIRE;
@Autowired
private RedisClient redisClient;
/**
* 查询商品信息
* @param itemId
* @return
*/
@Override
public TbItem selectItemInfo(Long itemId) {
//查询缓存
TbItem tbItem = (TbItem) redisClient.get(ITEM_INFO + ":" + itemId + ":"+ BASE);
if(tbItem!=null){
return tbItem;
}
tbItem = tbItemMapper.selectByPrimaryKey(itemId);
//把数据保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ BASE,tbItem);
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ BASE,ITEM_INFO_EXPIRE);
return tbItem;
}
/**
* 根据商品 ID 查询商品描述
* @param itemId
* @return
*/
@Override
public TbItemDesc selectItemDescByItemId(Long itemId) {
//查询缓存
TbItemDesc tbItemDesc = (TbItemDesc) redisClient.get(
ITEM_INFO + ":" + itemId + ":"+ DESC);
if(tbItemDesc!=null){
return tbItemDesc;
}
TbItemDescExample example = new TbItemDescExample();
TbItemDescExample.Criteria criteria = example.createCriteria();
criteria.andItemIdEqualTo(itemId);
List<TbItemDesc> itemDescList =
this.tbItemDescMapper.selectByExampleWithBLOBs(example);
if(itemDescList!=null && itemDescList.size()>0){
//把数据保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ DESC,itemDescList.get(0));
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ DESC,ITEM_INFO_EXPIRE);
return itemDescList.get(0);
}
return null;
}
- 修改ItemParamServiceImpl
@Value("${ITEM_INFO}")
private String ITEM_INFO;
@Value("${PARAM}")
private String PARAM;
@Value("${ITEM_INFO_EXPIRE}")
private Integer ITEM_INFO_EXPIRE;
@Autowired
private RedisClient redisClient;
/**
* 根据商品id查询商品规格
* @param itemId
* @return
*/
@Override
public TbItemParamItem selectTbItemParamItemByItemId(Long itemId) {
//查询缓存
TbItemParamItem tbItemParamItem = (TbItemParamItem) redisClient.get(
ITEM_INFO + ":" + itemId + ":"+ PARAM);
if(tbItemParamItem!=null){
return tbItemParamItem;
}
TbItemParamItemExample example = new TbItemParamItemExample();
TbItemParamItemExample.Criteria criteria = example.createCriteria();
criteria.andItemIdEqualTo(itemId);
List<TbItemParamItem> tbItemParamItemList =
tbItemParamItemMapper.selectByExampleWithBLOBs(example);
if(tbItemParamItemList!=null && tbItemParamItemList.size()>0){
//把数据保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+
PARAM,tbItemParamItemList.get(0));
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ PARAM,ITEM_INFO_EXPIRE);
return tbItemParamItemList.get(0);
}
return null;
}
2.2.1.3.controller
- 修改ItemController
/**查询商品信息
* 根据商品id
* @param itemId
* @return
*/
@RequestMapping("/selectItemInfo")
public TbItem selectItemInfo(Long itemId){
return this.itemService.selectItemInfo(itemId);
}
/**
* 根据商品 ID 查询商品描述
*/
@RequestMapping("/selectItemDescByItemId")
public TbItemDesc selectItemDescByItemId(Long itemId){
return this.itemService.selectItemDescByItemId(itemId);
}
- 修改ItemParamController
/**
* 根据商品 ID 查询商品规格
*/
@RequestMapping("/selectTbItemParamItemByItemId")
public TbItemParamItem selectTbItemParamItemByItemId(@RequestParam Long itemId){
return itemParamService.selectTbItemParamItemByItemId(itemId);
}
2.2.2.power_shop_item_feign
2.2.2.1.pom.xml
<dependency>
<groupId>com.bjpowernode</groupId>
<artifactId>common_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.2.2.2.feign
@RequestMapping("/service/item/selectItemInfo")
TbItem selectItemInfo(@RequestParam("itemId") Long itemId);
@RequestMapping("/service/item/selectItemDescByItemId")
TbItemDesc selectItemDescByItemId(@RequestParam("itemId") Long itemId);
@RequestMapping("/service/itemParam/selectTbItemParamItemByItemId")
TbItemParamItem selectTbItemParamItemByItemId(@RequestParam("itemId") Long itemId);
2.2.3.power_shop_detail
2.2.3.1.controller
package com.bjpowernode.controller;
import com.bjpowershop.feign.ItemServiceFeign;
import com.bjpowershop.pojo.TbItem;
import com.bjpowershop.pojo.TbItemDesc;
import com.bjpowershop.pojo.TbItemParamItem;
import com.bjpowershop.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/frontend/detail")
public class DetailController {
@Autowired
private ItemServiceFeign itemServiceFeign;
/**
* 查询商品基本信息
*/
@RequestMapping("/selectItemInfo")
public Result selectItemInfo(Long itemId) {
TbItem tbItem = itemServiceFeign.selectItemInfo(itemId);
if (tbItem != null) {
return Result.ok(tbItem);
}
return Result.error("查无结果");
}
/**
* 查询商品介绍
*/
@RequestMapping("/selectItemDescByItemId")
public Result selectItemDescByItemId(Long itemId){
TbItemDesc tbItemDesc = itemServiceFeign.selectItemDescByItemId(itemId);
if(tbItemDesc != null){
return Result. ok (tbItemDesc);
}
return Result.error ("查无结果");
}
/**
* 根据商品 ID 查询商品规格参数
*/
@RequestMapping("/selectTbItemParamItemByItemId")
public Result selectTbItemParamItemByItemId(Long itemId){
TbItemParamItem tbItemParamItem =
itemServiceFeign.selectTbItemParamItemByItemId(itemId);
if(tbItemParamItem != null){
return Result.ok(tbItemParamItem);
}
return Result.error ("查无结果");
}
}
2.2.4.测试
3.缓存同步
后台修改或删除商品时直接删除redis中的商品
4.缓存穿透
4.1.描述
缓存穿透是指<font color=red>缓存和数据库中都没有数据</font>,而用户不断发起请求则这些<font color=red>请求会穿过缓存直接访问数据库</font>,如发起为id为“-1”的数据或id为特别大不存在的数据。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。
4.2.解决方案
**缓存空对象:**当存储层不命中时,创建空对象并将其缓存起来,同时会设置一个过期时间(避免控制占用更多的存储空间),之后再访问这个数据将会从缓存中获取,保护了后端数据源;
4.3.power_shop_item
4.3.1.service
- 修改ItemServiceImpl
/**
* 查询商品信息
* @param itemId
* @return
*/
@Override
public TbItem selectItemInfo(Long itemId) {
//查询缓存
TbItem tbItem = (TbItem) redisClient.get(ITEM_INFO + ":" + itemId + ":"+ BASE);
if(tbItem!=null){
return tbItem;
}
tbItem = tbItemMapper.selectByPrimaryKey(itemId);
/********************解决缓存穿透************************/
if(tbItem == null){
//把空对象保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ BASE,new TbItem());
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ BASE,30);
return tbItem;
}
//把数据保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ BASE,tbItem);
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ BASE,ITEM_INFO_EXPIRE);
return tbItem;
}
/**
* 根据商品 ID 查询商品介绍
* @param itemId
* @return
*/
@Override
public TbItemDesc selectItemDescByItemId(Long itemId) {
//查询缓存
TbItemDesc tbItemDesc = (TbItemDesc) redisClient.get(
ITEM_INFO + ":" + itemId + ":"+ DESC);
if(tbItemDesc!=null){
return tbItemDesc;
}
TbItemDescExample example = new TbItemDescExample();
TbItemDescExample.Criteria criteria = example.createCriteria();
criteria.andItemIdEqualTo(itemId);
List<TbItemDesc> itemDescList =
this.tbItemDescMapper.selectByExampleWithBLOBs(example);
if(itemDescList!=null && itemDescList.size()>0){
//把数据保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ DESC,itemDescList.get(0));
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ DESC,ITEM_INFO_EXPIRE);
return itemDescList.get(0);
}
/********************解决缓存穿透************************/
//把空对象保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ DESC,new TbItemDesc());
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ DESC,30);
return null;
}
- 修改ItemParamServiceImpl
/**
* 根据商品id查询商品规格
* @param itemId
* @return
*/
@Override
public TbItemParamItem selectTbItemParamItemByItemId(Long itemId) {
//查询缓存
TbItemParamItem tbItemParamItem = (TbItemParamItem) redisClient.get(
ITEM_INFO + ":" + itemId + ":"+ PARAM);
if(tbItemParamItem!=null){
return tbItemParamItem;
}
TbItemParamItemExample example = new TbItemParamItemExample();
TbItemParamItemExample.Criteria criteria = example.createCriteria();
criteria.andItemIdEqualTo(itemId);
List<TbItemParamItem> tbItemParamItemList =
tbItemParamItemMapper.selectByExampleWithBLOBs(example);
if(tbItemParamItemList!=null && tbItemParamItemList.size()>0){
//把数据保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+
PARAM,tbItemParamItemList.get(0));
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ PARAM,ITEM_INFO_EXPIRE);
return tbItemParamItemList.get(0);
}
/********************解决缓存穿透************************/
//把空对象保存到缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":"+ PARAM,new TbItemParamItem());
//设置缓存的有效期
redisClient.expire(ITEM_INFO + ":" + itemId + ":"+ PARAM,30);
return null;
}
5.缓存击穿
5.1.描述
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对一个key不停进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
5.2.解决方案
分布式锁:
-
设置热点数据永远不过期
-
加分布式锁
1、如何释放锁?del
2、业务处理失败?expire
5.3.common_redis
5.3.1.RedisClient
/**
* 分布式锁
* @param key
* @param count
* @param value
* @return
*/
public Boolean setnx(String key, Object value, long time) {
try {
return redisTemplate.opsForValue().setIfAbsent(key, value, time,
TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
5.4.power_shop_item
5.4.1.application.yml
#分布式锁
SETNX_LOCK_BASC: SETNX_LOCK_BASC
SETNX_LOCK_DESC: SETNX_LOCK_DESC
SETNX_LOCK_PARAM: SETNX_LOCK_PARAM
5.4.2.service
- 修改ItemServiceImpl
/**
* 查询商品信息
* @param itemId
* @return
*/
@Override
public TbItem selectItemInfo(Long itemId){
//1、先查询redis,如果有直接返回
TbItem tbItem = (TbItem) redisClient.get(ITEM_INFO+":"+itemId+":"+BASE);
if(tbItem!=null){
return tbItem;
}
/*****************解决缓存击穿***************/
if(redisClient.setnx(SETNX_LOCK_BASC+":"+itemId,itemId,5L)){
//2、再查询mysql,并把查询结果缓存到redis,并设置失效时间
tbItem = tbItemMapper.selectByPrimaryKey(itemId);
/*****************解决缓存穿透*****************/
if(tbItem!=null){
redisClient.set(ITEM_INFO+":"+itemId+":"+BASE,tbItem);
redisClient.expire(ITEM_INFO+":"+itemId+":"+BASE,ITEM_INFO_EXPIRE);
}else{
redisClient.set(ITEM_INFO+":"+itemId+":"+BASE,new TbItem());
redisClient.expire(ITEM_INFO+":"+itemId+":"+BASE,30L);
}
redisClient.del(SETNX_LOCK_BASC+":"+itemId);
return tbItem;
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return selectItemInfo(itemId);
}
}
/**
* 根据商品 ID 查询商品介绍
* @param itemId
* @return
*/
@Override
public TbItemDesc selectItemDescByItemId(Long itemId) {
//1、先查询redis,如果有直接返回
TbItemDesc tbItemDesc = (TbItemDesc) redisClient.get(ITEM_INFO + ":" + itemId +
":" + DESC);
if(tbItemDesc!=null){
return tbItemDesc;
}
if(redisClient.setnx(SETNX_DESC_LOCK_KEY+":"+itemId,itemId,30L)){
//2、再查询mysql,并把查询结果缓存到redis,并设置失效时间
tbItemDesc = tbItemDescMapper.selectByPrimaryKey(itemId);
if(tbItemDesc!=null){
redisClient.set(ITEM_INFO + ":" + itemId + ":" + DESC,tbItemDesc);
redisClient.expire(ITEM_INFO + ":" + itemId + ":" +
DESC,ITEM_INFO_EXPIRE);
}else{
redisClient.set(ITEM_INFO + ":" + itemId + ":" + DESC,new TbItemDesc());
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + DESC,30L);
}
redisClient.del(SETNX_DESC_LOCK_KEY+":"+itemId);
return tbItemDesc;
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return selectItemDescByItemId(itemId);
}
}
- 修改ItemParamServiceImpl
/**
* 根据商品id查询商品规格
* @param itemId
* @return
*/
@Override
public TbItemParamItem selectTbItemParamItemByItemId(Long itemId) {
//1、先查询redis,如果有直接返回
TbItemParamItem tbItemParamItem = (TbItemParamItem) redisClient.get(ITEM_INFO +
d":" + itemId + ":" + PARAM);
if(tbItemParamItem!=null){
return tbItemParamItem;
}
if(redisClient.setnx(SETNX_LOCK_PARAM+":"+itemId,itemId,30L)){
//2、再查询mysql,并把查询结果缓存到redis,并设置失效时间
TbItemParamItemExample tbItemParamItemExample = new TbItemParamItemExample();
TbItemParamItemExample.Criteria criteria =
tbItemParamItemExample.createCriteria();
criteria.andItemIdEqualTo(itemId);
List<TbItemParamItem> tbItemParamItems =
tbItemParamItemMapper.selectByExampleWithBLOBs(tbItemParamItemExample);
if(tbItemParamItems!=null && tbItemParamItems.size()>0){
tbItemParamItem = tbItemParamItems.get(0);
redisClient.set(ITEM_INFO + ":" + itemId + ":" + PARAM,tbItemParamItem);
redisClient.expire(ITEM_INFO + ":" + itemId + ":" +
PARAM,ITEM_INFO_EXPIRE);
}else{
redisClient.set(ITEM_INFO + ":" + itemId + ":" + PARAM,
new TbItemParamItem());
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + PARAM,30L);
}
redisClient.del(SETNX_LOCK_PARAM+":"+itemId);
return tbItemParamItem;
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return selectTbItemParamItemByItemId(itemId);
}
}
6.缓存雪崩
6.1.描述:
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
6.2.解决方案:
缓存数据的过期时间设置随机或不同分类商品缓存不同周期或热门类目的商品缓存永不过期