商品详情实现与缓存问题(穿透、击穿、雪崩)解决方案

一、学习目标

  • 商品详情功能实现
  • 缓存同步处理
  • 缓存穿透问题解决
  • 缓存击穿问题解决
  • 缓存雪崩问题解决

二、商品详情的实现方案

2.1 网页静态化方案

网页静态化是提升商品详情页访问性能的重要方式,核心步骤如下:

  1. 创建商品详情的 Thymeleaf 模板,定义页面展示结构;
  2. 开发消息接收服务,当商品数据变更时,触发静态页面生成;
  3. 搭建 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 后缀分别为DESCPARAM,核心代码如下(以商品描述为例):

复制代码
@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 解决方案:分布式锁

通过分布式锁保证同一时间只有一个请求查询数据库并重建缓存,其他请求等待后从缓存获取数据。解决方案:

  1. 热点数据可设置永不过期;
  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. 缓存过期时间随机化:为不同商品的缓存设置随机过期时间(如 1 天 ± 随机分钟数),避免集中过期;
  2. 分类设置过期周期:不同分类商品的缓存有效期差异化(如热门分类 7 天,普通分类 1 天);
  3. 热点商品永不过期:核心热点商品缓存不设置过期时间,通过后台主动更新 / 删除缓存。

八、总结

本文围绕商品详情功能实现,详细讲解了 Redis 缓存的应用及缓存穿透、击穿、雪崩三大问题的解决方案:

  • 缓存穿透:缓存空对象,避免请求直达数据库;
  • 缓存击穿:分布式锁,控制热点 Key 过期后的并发查库;
  • 缓存雪崩:过期时间随机化 / 差异化,热点数据永不过期。 通过合理的缓存设计和问题优化,可大幅提升系统的稳定性和性能。
相关推荐
苦逼的猿宝1 小时前
基于springboot的课程作业管理系统(源码+论文)
java·毕业设计·springboot·计算机毕业设计
我本楚狂人www1 小时前
Spring 两大核心思想(一):IoC
java·数据库·spring
熊文豪1 小时前
标量子查询消除:一次让查询性能提升千倍的优化器手术
数据库·电科金仓
他们叫我阿冠1 小时前
Day4学习--MySQL高级
数据库·学习·mysql
国冶机电安装2 小时前
非标系统控制柜制造:从特殊工况到定制控制的完整解析
数据库·制造
雨辰AI2 小时前
完整版信创微服务国产化架构实战:Nacos+Seata+Redis + 人大金仓(生产可落地)
数据库·redis·微服务·架构·政务
迷渡2 小时前
用 Rust 重写的 Bun 有 13365 个 unsafe!
开发语言·后端·rust
网管NO.12 小时前
SQL 模糊查询 + NULL 空值。LIKE 通配符 % 和_、IS NULL
数据库
Mr. zhihao2 小时前
Redis 内存管理深度解析:过期删除与内存淘汰策略
数据库·redis·缓存