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

一、学习目标

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

二、商品详情的实现方案

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 过期后的并发查库;
  • 缓存雪崩:过期时间随机化 / 差异化,热点数据永不过期。 通过合理的缓存设计和问题优化,可大幅提升系统的稳定性和性能。
相关推荐
神奇小汤圆4 小时前
责任链模式 + 策略模式:优雅处理多级请求的方式
后端
神奇小汤圆4 小时前
没啃透无锁队列,高并发底层你只懂了皮毛!
后端
这个DBA有点耶4 小时前
AI写的SQL跑崩了生产库,这锅谁背?
数据库·人工智能·程序员
大鸡腿同学5 小时前
大模型是怎么训练出来的?
后端
镜舟科技5 小时前
Databricks 再提 LTAP,AI 时代的数据底座为何重回大一统叙事?
数据库·架构·agent
lizhongxuan5 小时前
判断一个人懂不懂 agent harness
后端
Databend6 小时前
从湖仓升级为 Agent 时代的数据控制面,Snowflake 和 Databricks 有哪些布局
大数据·数据库·agent
非洲农业不发达6 小时前
windows终端体验大升级,让你拥有macos级别的美化
前端·后端
妙码生花6 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十七):登录接口完善,登录页接口整合,解决跨域
前端·后端·ai编程