商品详情实现与缓存问题(穿透、击穿、雪崩)解决方案
一、学习目标
- 商品详情功能实现
- 缓存同步处理
- 缓存穿透问题解决
- 缓存击穿问题解决
- 缓存雪崩问题解决
二、商品详情的实现方案
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
<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: 86400Service 层实现(商品基础信息)
ItemServiceImpl.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
... /** * 分布式锁(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_PARAM6.3.3 Service 层改造(以商品基础信息为例)
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 过期后的并发查库;
- 缓存雪崩:过期时间随机化 / 差异化,热点数据永不过期。 通过合理的缓存设计和问题优化,可大幅提升系统的稳定性和性能。
