一、学习目标商品详情功能实现缓存同步处理缓存穿透问题解决缓存击穿问题解决缓存雪崩问题解决二、商品详情的实现方案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.xmlxmldependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.prowershop/groupId artifactIdpower_shop_item_feign/artifactId version1.0-SNAPSHOT/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.prowershop/groupId artifactIdcommon_utils/artifactId version1.0-SNAPSHOT/version /dependency /dependenciesapplication.ymlyamlserver: port: 8094 spring: application: name: power-shop-detail cloud: nacos: discovery: server-addr: 192.168.204.129:8848启动类javapackage 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.ymlyaml# 缓存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); ListTbItemDesc 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 服务调用javaRequestMapping(/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 详情服务 Controllerpower_shop_detailjavapackage 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是不存在的商品ID5.2 解决方案缓存空对象当数据库查询无结果时向 Redis 写入空对象并设置短有效期如 30 秒后续请求直接从缓存获取空对象避免穿透到数据库。5.3 代码改造以商品基础信息为例javaOverride 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 SETNXdel删除锁finally或expire防止死锁控制数据库查询并发。6.3 代码改造6.3.1 Redis 分布式锁工具common_redisRedisClient.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 层改造以商品基础信息为例javaOverride 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 过期后的并发查库缓存雪崩过期时间随机化 / 差异化热点数据永不过期。 通过合理的缓存设计和问题优化可大幅提升系统的稳定性和性能。
商品详情实现与缓存问题(穿透、击穿、雪崩)解决方案
发布时间:2026/5/23 6:48:13
一、学习目标商品详情功能实现缓存同步处理缓存穿透问题解决缓存击穿问题解决缓存雪崩问题解决二、商品详情的实现方案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.xmlxmldependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.prowershop/groupId artifactIdpower_shop_item_feign/artifactId version1.0-SNAPSHOT/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.prowershop/groupId artifactIdcommon_utils/artifactId version1.0-SNAPSHOT/version /dependency /dependenciesapplication.ymlyamlserver: port: 8094 spring: application: name: power-shop-detail cloud: nacos: discovery: server-addr: 192.168.204.129:8848启动类javapackage 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.ymlyaml# 缓存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); ListTbItemDesc 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 服务调用javaRequestMapping(/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 详情服务 Controllerpower_shop_detailjavapackage 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是不存在的商品ID5.2 解决方案缓存空对象当数据库查询无结果时向 Redis 写入空对象并设置短有效期如 30 秒后续请求直接从缓存获取空对象避免穿透到数据库。5.3 代码改造以商品基础信息为例javaOverride 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 SETNXdel删除锁finally或expire防止死锁控制数据库查询并发。6.3 代码改造6.3.1 Redis 分布式锁工具common_redisRedisClient.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 层改造以商品基础信息为例javaOverride 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 过期后的并发查库缓存雪崩过期时间随机化 / 差异化热点数据永不过期。 通过合理的缓存设计和问题优化可大幅提升系统的稳定性和性能。