一、学习目标
- 商品详情功能实现
- 缓存同步处理
- 缓存穿透问题解决
- 缓存击穿问题解决
- 缓存雪崩问题解决
二、商品详情的实现方案
2.1 网页静态化方案
网页静态化是提升商品详情页访问性能的重要方式,核心步骤如下:
- 创建商品详情的 Thymeleaf 模板,定义页面展示结构;
- 开发消息接收服务,当商品数据变更时,触发静态页面生成;
- 搭建 Nginx 服务器,直接返回生成的静态页面,减少后端服务压力。
2.2 Redis 缓存商品信息方案
采用 Redis 缓存商品核心数据,降低数据库访问频次,业务逻辑如下:
根据商品 ID 查询 Redis 缓存:
- 缓存命中:直接返回数据;
- 缓存未命中:查询 MySQL 数据库,将数据写入 Redis,并设置缓存有效期(默认 1 天,可按需调整)。
三、商品详情功能开发
3.1 创建 power_shop_detail 工程
3.1.1 工程结构
基于 Spring Boot 搭建独立的商品详情服务,集成 Nacos 服务发现与 Feign 远程调用。
3.1.2 核心配置
pom.xml
xml
java
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.prowershop</groupId>
<artifactId>power_shop_item_feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.prowershop</groupId>
<artifactId>common_utils</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
application.yml
yaml
server:
port: 8094
spring:
application:
name: power-shop-detail
cloud:
nacos:
discovery:
server-addr: 192.168.204.129:8848
启动类
java
package com.powershop;
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);
}
}
3.2 商品信息查询实现
3.2.1 power_shop_item 服务改造(核心)
配置文件(application.yml)
yaml
# 缓存Key前缀
ITEM_INFO: ITEM_INFO
BASE: BASE
DESC: DESC
PARAM: PARAM
# 缓存有效期(秒)
ITEM_INFO_EXPIRE: 86400
Service 层实现(商品基础信息)
ItemServiceImpl.java
java
...
@Value("${ITEM_INFO}")
private String ITEM_INFO;
@Value("${BASE}")
private String BASE;
@Value("${PARAM}")
private String PARAM;
@Value("${ITEM_INFO_EXPIRE}")
private Integer ITEM_INFO_EXPIRE;
@Autowired
private RedisClient redisClient;
...
/**
* 查询商品基础信息
* @param itemId 商品ID
* @return 商品基础信息
*/
@Override
public TbItem selectItemInfo(Long itemId) {
// 1. 查询缓存
TbItem tbItem = (TbItem) redisClient.get(ITEM_INFO + ":" + itemId + ":" + BASE);
if (tbItem != null) {
return tbItem;
}
// 2. 缓存未命中,查询数据库
tbItem = tbItemMapper.selectByPrimaryKey(itemId);
// 3. 写入缓存并设置有效期
redisClient.set(ITEM_INFO + ":" + itemId + ":" + BASE, tbItem);
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + BASE, ITEM_INFO_EXPIRE);
return tbItem;
}
Service 层实现(商品描述 / 规格参数) 商品描述和规格参数查询逻辑与基础信息一致,仅 Key 后缀分别为DESC和PARAM,核心代码如下(以商品描述为例):
@Override
public TbItemDesc selectItemDescByItemId(Long itemId) {
// 1. 查询缓存
TbItemDesc tbItemDesc = (TbItemDesc) redisClient.get(ITEM_INFO + ":" + itemId + ":" + DESC);
if (tbItemDesc != null) {
return tbItemDesc;
}
// 2. 查询数据库
TbItemDescExample example = new TbItemDescExample();
example.createCriteria().andItemIdEqualTo(itemId);
List<TbItemDesc> itemDescList = tbItemDescMapper.selectByExampleWithBLOBs(example);
// 3. 写入缓存
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;
}
3.2.2 Feign 远程调用(power_shop_item_feign)
定义 Feign 接口,供 power_shop_detail 服务调用:
java
@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);
3.2.3 详情服务 Controller(power_shop_detail)
java
package com.powershop.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);
return tbItem != null ? Result.ok(tbItem) : Result.error("查无结果");
}
/**
* 查询商品描述
*/
@RequestMapping("/selectItemDescByItemId")
public Result selectItemDescByItemId(Long itemId) {
TbItemDesc tbItemDesc = itemServiceFeign.selectItemDescByItemId(itemId);
return tbItemDesc != null ? Result.ok(tbItemDesc) : Result.error("查无结果");
}
/**
* 查询商品规格参数
*/
@RequestMapping("/selectTbItemParamItemByItemId")
public Result selectTbItemParamItemByItemId(Long itemId) {
TbItemParamItem tbItemParamItem = itemServiceFeign.selectTbItemParamItemByItemId(itemId);
return tbItemParamItem != null ? Result.ok(tbItemParamItem) : Result.error("查无结果");
}
}
3.3 功能测试
启动所有服务后,通过接口访问商品详情数据,验证缓存写入和查询逻辑是否正常。

四、缓存同步(练习)
后台修改 / 删除商品时,需主动删除 Redis 中对应商品的缓存数据,避免缓存与数据库数据不一致。核心逻辑:在商品修改 / 删除接口中,调用redisClient.del(key)删除对应缓存 Key。
五、缓存穿透问题解决
5.1 问题描述
缓存穿透是指缓存和数据库中均无对应数据,恶意请求(如不存在的商品 ID)会直接穿透缓存访问数据库,导致数据库压力剧增。

例如:-1是不存在的商品ID

5.2 解决方案:缓存空对象
当数据库查询无结果时,向 Redis 写入空对象并设置短有效期(如 30 秒),后续请求直接从缓存获取空对象,避免穿透到数据库。
5.3 代码改造(以商品基础信息为例)
java
@Override
public TbItem selectItemInfo(Long itemId) {
// 1. 查询缓存
TbItem tbItem = (TbItem) redisClient.get(ITEM_INFO + ":" + itemId + ":" + BASE);
if (tbItem != null) {
return tbItem;
}
// 2. 查询数据库
tbItem = tbItemMapper.selectByPrimaryKey(itemId);
// 3. 解决缓存穿透:数据库无数据则缓存空对象
if (tbItem == null) {
redisClient.set(ITEM_INFO + ":" + itemId + ":" + BASE, new TbItem());
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + BASE, 30);
return tbItem;
}
// 4. 数据库有数据,正常缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":" + BASE, tbItem);
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + BASE, ITEM_INFO_EXPIRE);
return tbItem;
}
商品描述、规格参数的改造逻辑一致,均在数据库查询无结果时缓存空对象。
缓存穿透后,Redis效果:

六、缓存击穿问题解决
6.1 问题描述
缓存击穿是指热点商品 Key 过期瞬间,大量并发请求直接访问数据库,导致数据库压力骤增。

6.2 解决方案:分布式锁
通过分布式锁保证同一时间只有一个请求查询数据库并重建缓存,其他请求等待后从缓存获取数据。解决方案:
- 热点数据可设置永不过期;
- 分布式锁(Redis SETNX),del删除锁,finally或expire防止死锁,控制数据库查询并发。

6.3 代码改造
6.3.1 Redis 分布式锁工具(common_redis)
RedisClient.java
java
...
/**
* 分布式锁(SETNX)
* @param key 锁Key
* @param value 锁值(如商品ID)
* @param time 锁有效期(秒)
* @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;
}
}
/**
* 分布式锁
* @param key
* @param value
* @return
*/
public Boolean setnx(String key, Object value) {
try {
return redisTemplate.opsForValue().setIfAbsent(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
6.3.2 配置分布式锁 Key 前缀
yaml
# 分布式锁Key前缀
SETNX_LOCK_BASC: SETNX_LOCK_BASC
SETNX_LOCK_DESC: SETNX_LOCK_DESC
SETNX_LOCK_PARAM: SETNX_LOCK_PARAM
6.3.3 Service 层改造(以商品基础信息为例)
java
java
@Override
public Item selectItemInfo(Long itemId) {
//1、先查询redis缓存,有数据则return
Item item = (Item) redisClient.get(ITEM_INFO + ":" + itemId + ":" + BASE);
if (item != null){
return item;
}
/***************** 解决缓存击穿 1.setnx分布式锁 2.finally + del释放锁 *******************/
if (redisClient.setnx(SETNX_LOCK_BASC + ":" + itemId, itemId)) {
try {
//2、无数据则查询数据库
item = itemMapper.selectById(itemId);
//数据库没有,解决缓存穿透问题
if (item == null) {
item = new Item();
redisClient.set(ITEM_INFO + ":" + itemId + ":" + BASE, item); //将空对象缓存到Redis
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + BASE, 30); //设置过期时间30s
return item;
}
//数据库有,则添加缓存
redisClient.set(ITEM_INFO + ":" + itemId + ":" + BASE, item);
//3、设置过期时间
redisClient.expire(ITEM_INFO + ":" + itemId + ":" + BASE, ITEM_INFO_EXPIRE);
}finally {
//缓存击穿:删除锁
redisClient.del(SETNX_LOCK_BASC + ":" + itemId);
}
return item;
}else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return selectItemInfo(itemId); //回调
}
}
商品描述、规格参数的改造逻辑一致,仅锁 Key 前缀不同。
七、缓存雪崩问题解决
7.1 问题描述
缓存雪崩是指大量缓存 Key 在同一时间段集中过期,导致大量请求穿透到数据库,引发数据库宕机。

7.2 解决方案
- 缓存过期时间随机化:为不同商品的缓存设置随机过期时间(如 1 天 ± 随机分钟数),避免集中过期;
- 分类设置过期周期:不同分类商品的缓存有效期差异化(如热门分类 7 天,普通分类 1 天);
- 热点商品永不过期:核心热点商品缓存不设置过期时间,通过后台主动更新 / 删除缓存。
八、总结
本文围绕商品详情功能实现,详细讲解了 Redis 缓存的应用及缓存穿透、击穿、雪崩三大问题的解决方案:
- 缓存穿透:缓存空对象,避免请求直达数据库;
- 缓存击穿:分布式锁,控制热点 Key 过期后的并发查库;
- 缓存雪崩:过期时间随机化 / 差异化,热点数据永不过期。 通过合理的缓存设计和问题优化,可大幅提升系统的稳定性和性能。