多级缓存架构实战指南

简介: 本文详解如何利用装饰器模式实现多级缓存架构,通过Caffeine、Redis与MySQL三级联动,兼顾高性能与数据一致性。采用SpringBoot实战,代码可落地,有效解决高并发场景下的缓存穿透、击穿、雪崩问题,提升系统稳定性与扩展性。

一、前言:为什么需要多级缓存+装饰器模式?

在高并发系统中,缓存是提升性能的核心手段,但单一缓存架构往往无法兼顾"性能、一致性、可用性"三大核心诉求。比如:本地缓存(Caffeine)速度快但无法集群共享,分布式缓存(Redis)可共享但存在网络开销,多级缓存(本地缓存+Redis+数据库)能平衡两者优势,但如何优雅地实现缓存层级的叠加、切换与扩展?

装饰器模式(Decorator Pattern)给出了完美答案——它允许我们动态地给一个对象添加额外的职责,通过"包装"的方式实现功能的叠加,而非继承。这种特性恰好匹配多级缓存的架构设计:将不同层级的缓存(本地、Redis)作为"装饰器",层层包裹核心的数据获取逻辑(数据库查询),既保证了各缓存组件的独立性,又能灵活组合缓存策略。

二、核心概念铺垫:先搞懂这3个关键问题

在动手实现前,我们先厘清核心概念,避免后续理解偏差。

2.1 多级缓存的核心价值与常见架构

多级缓存的核心目标是**"就近获取数据"**,减少网络IO和磁盘IO开销。常见的三级缓存架构如下:

  • 一级缓存(L1):本地缓存(如Caffeine),进程内存储,速度最快(微秒级),但仅单进程可见,适合存储热点数据(如首页Banner、高频查询配置)。
  • 二级缓存(L2):分布式缓存(如Redis),集群共享,速度次之(毫秒级),适合存储全局共享数据(如用户会话、商品详情)。
  • 三级缓存(L3):数据库(如MySQL),数据持久化存储,速度最慢(百毫秒级),是数据的最终来源。

架构工作流程:查询数据时,优先从L1获取;L1未命中则查询L2;L2未命中则查询L3,同时将数据回写到L1和L2;更新数据时,需同步失效各级缓存(避免数据不一致)。

2.2 装饰器模式的底层逻辑与优势

装饰器模式属于结构型设计模式,核心是**"组合优于继承"**,通过动态包装实现功能扩展。其核心角色包括:

  • 抽象组件(Component):定义核心功能的接口(如数据查询接口)。
  • 具体组件(ConcreteComponent):实现抽象组件,提供核心功能的基础实现(如数据库查询)。
  • 抽象装饰器(Decorator):实现抽象组件,持有抽象组件的引用,为具体装饰器提供统一的包装逻辑。
  • 具体装饰器(ConcreteDecorator):继承抽象装饰器,添加额外的职责(如本地缓存、Redis缓存的查询与回写)。

装饰器模式的优势:

  1. 灵活性:可动态组合多个装饰器,实现不同的功能叠加(如"本地缓存+Redis"或仅用"Redis")。
  2. 解耦性:各装饰器独立实现,不影响核心逻辑,符合单一职责原则。
  3. 可扩展性:新增缓存层级(如分布式缓存集群分片)时,只需新增装饰器,无需修改原有代码。
2.3 为什么多级缓存适合用装饰器模式?

如果用传统的继承方式实现多级缓存,会导致类爆炸(如:DBQuery、RedisDBQuery、CaffeineRedisDBQuery...),且无法动态切换缓存策略。而装饰器模式的"包装特性"恰好解决这个问题:

  • 核心逻辑(DB查询)封装为具体组件,不关心缓存逻辑。
  • 各缓存层级封装为具体装饰器,专注于自身的缓存读写逻辑。
  • 通过组合装饰器,动态构建多级缓存链路(如:CaffeineDecorator(RedisDecorator(DBQuery)))。

三、实战准备:环境搭建与核心依赖

我们将基于SpringBoot3.2构建实战项目,实现"本地缓存(Caffeine)+Redis+MySQL"的三级缓存架构,技术栈版本如下(均为最新稳定版):

  • JDK:17
  • SpringBoot:3.2.5
  • MyBatis-Plus:3.5.5.1
  • Redis:7.2.4
  • Caffeine:3.1.8
  • Lombok:1.18.30
  • Fastjson2:2.0.48
  • MySQL:8.0.36
3.1 Maven依赖配置(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.2.5</version>
       <relativePath/>
   </parent>
   <groupId>com.jam.demo</groupId>
   <artifactId>multi-level-cache-decorator</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>multi-level-cache-decorator</name>
   <description>多级缓存架构装饰器模式实战</description>
   <properties>
       <java.version>17</java.version>
       <mybatis-plus.version>3.5.5.1</mybatis-plus.version>
       <caffeine.version>3.1.8</caffeine.version>
       <fastjson2.version>2.0.48</fastjson2.version>
       <lombok.version>1.18.30</lombok.version>
   </properties>
   <dependencies>
       <!-- SpringBoot核心依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
       </dependency>
       
       <!-- MyBatis-Plus -->
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       
       <!-- 数据库驱动 -->
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       
       <!-- 本地缓存Caffeine -->
       <dependency>
           <groupId>com.github.benmanes.caffeine</groupId>
           <artifactId>caffeine</artifactId>
           <version>${caffeine.version}</version>
       </dependency>
       
       <!-- Lombok -->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>${lombok.version}</version>
           <scope>provided</scope>
       </dependency>
       
       <!-- Fastjson2 -->
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       
       <!-- Swagger3 -->
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>2.3.0</version>
       </dependency>
       
       <!-- 测试依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

3.2 配置文件(application.yml)

spring:
 # 数据库配置
 datasource:
   url: jdbc:mysql://localhost:3306/multi_level_cache?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
   username: root
   password: root
   driver-class-name: com.mysql.cj.jdbc.Driver
 # Redis配置
 redis:
   host: localhost
   port: 6379
   password:
   database: 0
   lettuce:
     pool:
       max-active: 8
       max-idle: 8
       min-idle: 2

# MyBatis-Plus配置
mybatis-plus:
 mapper-locations: classpath:mapper/**/*.xml
 type-aliases-package: com.jam.demo.entity
 configuration:
   map-underscore-to-camel-case: true # 下划线转驼峰
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志

# 本地缓存Caffeine配置
caffeine:
 cache:
   maximum-size: 1000 # 最大缓存数量
   expire-after-write: 5 # 写入后过期时间(分钟)

# 服务器配置
server:
 port: 8080

# Swagger3配置
springdoc:
 api-docs:
   path: /v3/api-docs
 swagger-ui:
   path: /swagger-ui.html
   operationsSorter: method
 packages-to-scan: com.jam.demo.controller

3.3 数据库表设计(MySQL8.0)

创建商品表product,用于存储核心业务数据,SQL脚本可直接执行:

CREATE DATABASE IF NOT EXISTS multi_level_cache DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE multi_level_cache;

DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
 `product_name` varchar(255) NOT NULL COMMENT '商品名称',
 `price` decimal(10,2) NOT NULL COMMENT '商品价格',
 `stock` int NOT NULL DEFAULT 0 COMMENT '库存数量',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 PRIMARY KEY (`id`) USING BTREE,
 INDEX `idx_product_name` (`product_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';

-- 初始化测试数据
INSERT INTO `product` (`product_name`, `price`, `stock`) VALUES
('Apple iPhone 15 Pro', 9999.00, 500),
('华为Mate 60 Pro', 6999.00, 800),
('小米14 Ultra', 5999.00, 1000),
('三星S24 Ultra', 8999.00, 300),
('OPPO Find X7 Pro', 5499.00, 600);

四、核心架构设计:装饰器模式落地多级缓存

我们将按照"抽象组件→具体组件→抽象装饰器→具体装饰器"的顺序实现多级缓存架构,整体流程如下:

image.png

4.1 抽象组件:定义数据查询核心接口

创建DataQueryService接口,定义数据查询的核心方法(根据ID查询商品),作为装饰器模式的抽象组件:

package com.jam.demo.service;

import com.jam.demo.entity.Product;

/**
* 数据查询抽象组件:定义核心数据查询接口
* @author ken
*/

public interface DataQueryService {

   /**
    * 根据商品ID查询商品信息
    * @param productId 商品ID
    * @return 商品信息
    */

   Product queryProductById(Long productId);
}

4.2 具体组件:实现数据库查询核心逻辑

创建DBDataQueryServiceImpl类,实现DataQueryService接口,提供数据库查询的基础实现(三级缓存的最底层),依赖MyBatis-Plus操作数据库:

4.2.1 实体类(Product.java)

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
* 商品实体类
* @author ken
*/

@Data
@TableName("product")
public class Product {

   /**
    * 商品ID
    */

   @TableId(type = IdType.AUTO)
   private Long id;

   /**
    * 商品名称
    */

   private String productName;

   /**
    * 商品价格
    */

   private BigDecimal price;

   /**
    * 库存数量
    */

   private Integer stock;

   /**
    * 创建时间
    */

   private LocalDateTime createTime;

   /**
    * 更新时间
    */

   private LocalDateTime updateTime;
}

4.2.2 Mapper接口(ProductMapper.java)

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.Product;
import org.springframework.stereotype.Repository;

/**
* 商品Mapper接口
* @author ken
*/

@Repository
public interface ProductMapper extends BaseMapper<Product> {
}

4.2.3 具体组件实现类(DBDataQueryServiceImpl.java)

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.Product;
import com.jam.demo.mapper.ProductMapper;
import com.jam.demo.service.DataQueryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

/**
* 数据库查询具体组件:实现核心数据查询逻辑(三级缓存最底层)
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class DBDataQueryServiceImpl implements DataQueryService {

   private final ProductMapper productMapper;

   /**
    * 根据商品ID查询商品信息(从数据库查询)
    * @param productId 商品ID
    * @return 商品信息
    */

   @Override
   public Product queryProductById(Long productId) {
       // 校验参数
       if (ObjectUtils.isEmpty(productId)) {
           log.error("查询商品失败:商品ID不能为空");
           throw new IllegalArgumentException("商品ID不能为空");
       }

       // 从数据库查询商品
       LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
       queryWrapper.eq(Product::getId, productId);
       Product product = productMapper.selectOne(queryWrapper);

       if (ObjectUtils.isEmpty(product)) {
           log.warn("查询商品失败:商品ID={}不存在", productId);
           return null;
       }

       log.info("数据库查询成功:商品ID={}, 商品名称={}", productId, product.getProductName());
       return product;
   }
}

4.3 抽象装饰器:封装统一的装饰逻辑

创建CacheDecorator类,实现DataQueryService接口,持有DataQueryService的引用,作为所有缓存装饰器的父类,封装统一的包装逻辑:

package com.jam.demo.service.decorator;

import com.jam.demo.entity.Product;
import com.jam.demo.service.DataQueryService;
import lombok.RequiredArgsConstructor;

/**
* 缓存抽象装饰器:封装统一的装饰逻辑,持有抽象组件引用
* @author ken
*/

@RequiredArgsConstructor
public abstract class CacheDecorator implements DataQueryService {

   /**
    * 持有抽象组件引用(被装饰的对象)
    */

   protected final DataQueryService dataQueryService;

   /**
    * 抽象方法:缓存key生成(由具体装饰器实现)
    * @param productId 商品ID
    * @return 缓存key
    */

   protected abstract String generateCacheKey(Long productId);
}

4.4 具体装饰器1:本地缓存(Caffeine)装饰器

创建CaffeineCacheDecorator类,继承CacheDecorator,实现本地缓存的查询、回写与失效逻辑:

4.4.1 Caffeine配置类(CaffeineConfig.java)

package com.jam.demo.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;

/**
* Caffeine本地缓存配置类
* @author ken
*/

@Configuration
public class CaffeineConfig {

   /**
    * 最大缓存数量
    */

   @Value("${caffeine.cache.maximum-size}")
   private Long maximumSize;

   /**
    * 写入后过期时间(分钟)
    */

   @Value("${caffeine.cache.expire-after-write}")
   private Integer expireAfterWrite;

   /**
    * 构建Caffeine缓存实例
    * @return Caffeine缓存
    */

   @Bean
   public com.github.benmanes.caffeine.cache.Cache<Long, Object> caffeineCache() {
       return Caffeine.newBuilder()
               .maximumSize(maximumSize) // 最大缓存数量
               .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) // 写入后过期
               .recordStats() // 开启缓存统计
               .build();
   }
}

4.4.2 本地缓存装饰器实现(CaffeineCacheDecorator.java)

package com.jam.demo.service.decorator;

import com.jam.demo.entity.Product;
import com.jam.demo.service.DataQueryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.Objects;

/**
* 本地缓存(Caffeine)具体装饰器:添加本地缓存查询与回写功能
* @author ken
*/

@Slf4j
@Component
public class CaffeineCacheDecorator extends CacheDecorator {

   /**
    * Caffeine本地缓存实例
    */

   private final com.github.benmanes.caffeine.cache.Cache<Long, Object> caffeineCache;

   /**
    * 构造方法:注入被装饰对象和Caffeine缓存
    * @param dataQueryService 被装饰的抽象组件
    * @param caffeineCache Caffeine缓存实例
    */

   public CaffeineCacheDecorator(DataQueryService dataQueryService, com.github.benmanes.caffeine.cache.Cache<Long, Object> caffeineCache) {
       super(dataQueryService);
       this.caffeineCache = caffeineCache;
   }

   /**
    * 生成本地缓存key(直接使用商品ID作为key)
    * @param productId 商品ID
    * @return 缓存key
    */

   @Override
   protected String generateCacheKey(Long productId) {
       return "product:caffeine:" + productId;
   }

   /**
    * 装饰器核心方法:先查本地缓存,未命中则调用被装饰对象的查询方法,再回写缓存
    * @param productId 商品ID
    * @return 商品信息
    */

   @Override
   public Product queryProductById(Long productId) {
       // 1. 校验参数
       if (ObjectUtils.isEmpty(productId)) {
           log.error("本地缓存查询商品失败:商品ID不能为空");
           throw new IllegalArgumentException("商品ID不能为空");
       }

       // 2. 从本地缓存查询
       Product product = (Product) caffeineCache.getIfPresent(productId);
       if (!ObjectUtils.isEmpty(product)) {
           log.info("本地缓存命中:商品ID={}, 商品名称={}", productId, product.getProductName());
           return product;
       }

       // 3. 本地缓存未命中,调用被装饰对象的查询方法(可能是Redis装饰器或数据库查询)
       product = dataQueryService.queryProductById(productId);

       // 4. 若查询到数据,回写本地缓存
       if (!ObjectUtils.isEmpty(product)) {
           caffeineCache.put(productId, product);
           log.info("本地缓存回写成功:商品ID={}, 商品名称={}", productId, product.getProductName());
       }

       return product;
   }

   /**
    * 本地缓存失效(更新数据时调用)
    * @param productId 商品ID
    */

   public void evictCaffeineCache(Long productId) {
       if (!ObjectUtils.isEmpty(productId)) {
           caffeineCache.invalidate(productId);
           log.info("本地缓存失效成功:商品ID={}", productId);
       }
   }
}

4.5 具体装饰器2:Redis缓存装饰器

创建RedisCacheDecorator类,继承CacheDecorator,实现Redis缓存的查询、回写与失效逻辑,依赖Spring Data Redis操作Redis:

4.5.1 Redis配置类(RedisConfig.java)

package com.jam.demo.config;

import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.spring.data.redis.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* Redis配置类:配置RedisTemplate的序列化方式(使用Fastjson2)
* @author ken
*/

@Configuration
public class RedisConfig {

   /**
    * 构建RedisTemplate实例,指定序列化方式
    * @param connectionFactory Redis连接工厂
    * @return RedisTemplate
    */

   @Bean
   public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(connectionFactory);

       // 配置序列化器(String key使用StringRedisSerializer,Value使用Fastjson2序列化)
       StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
       GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(
               JSONReader.Feature.IgnoreNulls,
               JSONWriter.Feature.WriteMapNullValue
       );

       // key序列化
       redisTemplate.setKeySerializer(stringRedisSerializer);
       // value序列化
       redisTemplate.setValueSerializer(fastJsonRedisSerializer);
       // hash key序列化
       redisTemplate.setHashKeySerializer(stringRedisSerializer);
       // hash value序列化
       redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);

       redisTemplate.afterPropertiesSet();
       return redisTemplate;
   }
}

4.5.2 Redis缓存装饰器实现(RedisCacheDecorator.java)

package com.jam.demo.service.decorator;

import com.jam.demo.entity.Product;
import com.jam.demo.service.DataQueryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.concurrent.TimeUnit;

/**
* Redis缓存具体装饰器:添加Redis缓存查询与回写功能
* @author ken
*/

@Slf4j
@Component
public class RedisCacheDecorator extends CacheDecorator {

   /**
    * RedisTemplate实例
    */

   private final RedisTemplate<String, Object> redisTemplate;

   /**
    * Redis缓存过期时间(30分钟,可配置化,此处简化为硬编码)
    */

   private static final Long REDIS_CACHE_EXPIRE = 30L;

   /**
    * 构造方法:注入被装饰对象和RedisTemplate
    * @param dataQueryService 被装饰的抽象组件
    * @param redisTemplate RedisTemplate实例
    */

   public RedisCacheDecorator(DataQueryService dataQueryService, RedisTemplate<String, Object> redisTemplate) {
       super(dataQueryService);
       this.redisTemplate = redisTemplate;
   }

   /**
    * 生成Redis缓存key
    * @param productId 商品ID
    * @return 缓存key
    */

   @Override
   protected String generateCacheKey(Long productId) {
       return "product:redis:" + productId;
   }

   /**
    * 装饰器核心方法:先查Redis缓存,未命中则调用被装饰对象的查询方法,再回写缓存
    * @param productId 商品ID
    * @return 商品信息
    */

   @Override
   public Product queryProductById(Long productId) {
       // 1. 校验参数
       if (ObjectUtils.isEmpty(productId)) {
           log.error("Redis缓存查询商品失败:商品ID不能为空");
           throw new IllegalArgumentException("商品ID不能为空");
       }

       // 2. 生成Redis缓存key
       String cacheKey = generateCacheKey(productId);

       // 3. 从Redis查询
       Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
       if (!ObjectUtils.isEmpty(product)) {
           log.info("Redis缓存命中:商品ID={}, 商品名称={}", productId, product.getProductName());
           return product;
       }

       // 4. Redis未命中,调用被装饰对象的查询方法(此处为数据库查询)
       product = dataQueryService.queryProductById(productId);

       // 5. 若查询到数据,回写Redis缓存(设置过期时间)
       if (!ObjectUtils.isEmpty(product)) {
           redisTemplate.opsForValue().set(cacheKey, product, REDIS_CACHE_EXPIRE, TimeUnit.MINUTES);
           log.info("Redis缓存回写成功:商品ID={}, 商品名称={}, 过期时间={}分钟",
                   productId, product.getProductName(), REDIS_CACHE_EXPIRE);
       }

       return product;
   }

   /**
    * Redis缓存失效(更新数据时调用)
    * @param productId 商品ID
    */

   public void evictRedisCache(Long productId) {
       if (!ObjectUtils.isEmpty(productId)) {
           String cacheKey = generateCacheKey(productId);
           redisTemplate.delete(cacheKey);
           log.info("Redis缓存失效成功:商品ID={}, 缓存key={}", productId, cacheKey);
       }
   }
}

五、组合装饰器:构建完整的多级缓存链路

装饰器模式的核心价值在于"动态组合",我们需要通过Spring的依赖注入,将CaffeineCacheDecoratorRedisCacheDecoratorDBDataQueryServiceImpl组合起来,形成"本地缓存→Redis→数据库"的三级缓存链路。

5.1 缓存服务组装配置类(CacheServiceConfig.java)

通过@Bean注解手动组装缓存链路,确保装饰器的嵌套顺序正确(本地缓存装饰器包裹Redis装饰器,Redis装饰器包裹数据库查询组件):

package com.jam.demo.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.jam.demo.service.DataQueryService;
import com.jam.demo.service.impl.DBDataQueryServiceImpl;
import com.jam.demo.service.decorator.CaffeineCacheDecorator;
import com.jam.demo.service.decorator.RedisCacheDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

/**
* 缓存服务组装配置类:组合装饰器,构建多级缓存链路
* 链路顺序:CaffeineCacheDecorator → RedisCacheDecorator → DBDataQueryServiceImpl
* @author ken
*/

@Configuration
public class CacheServiceConfig {

   /**
    * 组装三级缓存链路:本地缓存装饰器包裹Redis装饰器,Redis装饰器包裹数据库查询
    * @param dbDataQueryService 数据库查询具体组件
    * @param redisTemplate RedisTemplate实例
    * @param caffeineCache Caffeine缓存实例
    * @return 装饰后的DataQueryService(包含完整的三级缓存链路)
    */

   @Bean
   public DataQueryService multiLevelCacheQueryService(DBDataQueryServiceImpl dbDataQueryService,
                                                      RedisTemplate<String, Object> redisTemplate,
                                                      Cache<Long, Object> caffeineCache)
{
       // 1. 数据库查询组件作为最底层被装饰对象
       // 2. 用Redis装饰器包裹数据库查询组件
       RedisCacheDecorator redisCacheDecorator = new RedisCacheDecorator(dbDataQueryService, redisTemplate);
       // 3. 用本地缓存装饰器包裹Redis装饰器,形成完整链路
       return new CaffeineCacheDecorator(redisCacheDecorator, caffeineCache);
   }

   /**
    * 单独注入Redis缓存装饰器(用于更新数据时失效Redis缓存)
    * @param dbDataQueryService 数据库查询具体组件
    * @param redisTemplate RedisTemplate实例
    * @return RedisCacheDecorator
    */

   @Bean
   public RedisCacheDecorator redisCacheDecorator(DBDataQueryServiceImpl dbDataQueryService,
                                                  RedisTemplate<String, Object> redisTemplate)
{
       return new RedisCacheDecorator(dbDataQueryService, redisTemplate);
   }

   /**
    * 单独注入Caffeine缓存装饰器(用于更新数据时失效本地缓存)
    * @param redisCacheDecorator Redis缓存装饰器
    * @param caffeineCache Caffeine缓存实例
    * @return CaffeineCacheDecorator
    */

   @Bean
   public CaffeineCacheDecorator caffeineCacheDecorator(RedisCacheDecorator redisCacheDecorator,
                                                        Cache<Long, Object> caffeineCache)
{
       return new CaffeineCacheDecorator(redisCacheDecorator, caffeineCache);
   }
}

5.2 业务服务层:封装缓存查询与数据更新逻辑

创建ProductService接口及其实现类,封装商品查询(调用多级缓存链路)和商品更新(同步失效各级缓存)的业务逻辑,并添加事务支持:

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.Product;

/**
* 商品服务接口
* @author ken
*/

public interface ProductService extends IService<Product> {

   /**
    * 根据商品ID查询商品(走多级缓存)
    * @param productId 商品ID
    * @return 商品信息
    */

   Product getProductByIdWithMultiLevelCache(Long productId);

   /**
    * 更新商品信息(同步失效各级缓存)
    * @param product 商品信息
    * @return 是否更新成功
    */

   boolean updateProductWithCacheEvict(Product product);
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.Product;
import com.jam.demo.mapper.ProductMapper;
import com.jam.demo.service.DataQueryService;
import com.jam.demo.service.ProductService;
import com.jam.demo.service.decorator.CaffeineCacheDecorator;
import com.jam.demo.service.decorator.RedisCacheDecorator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

/**
* 商品服务实现类
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {

   /**
    * 多级缓存查询服务(装饰器组合后的实例)
    */

   private final DataQueryService multiLevelCacheQueryService;

   /**
    * Redis缓存装饰器(用于失效Redis缓存)
    */

   private final RedisCacheDecorator redisCacheDecorator;

   /**
    * Caffeine缓存装饰器(用于失效本地缓存)
    */

   private final CaffeineCacheDecorator caffeineCacheDecorator;

   /**
    * 根据商品ID查询商品(走多级缓存链路)
    * @param productId 商品ID
    * @return 商品信息
    */

   @Override
   public Product getProductByIdWithMultiLevelCache(Long productId) {
       return multiLevelCacheQueryService.queryProductById(productId);
   }

   /**
    * 更新商品信息(同步失效各级缓存)
    * 采用声明式事务(若需更细粒度控制,可改用编程式事务)
    * @param product 商品信息
    * @return 是否更新成功
    */

   @Override
   @Transactional(rollbackFor = Exception.class)
   public boolean updateProductWithCacheEvict(Product product)
{
       // 1. 校验参数
       if (ObjectUtils.isEmpty(product) || ObjectUtils.isEmpty(product.getId())) {
           log.error("更新商品失败:商品信息或商品ID不能为空");
           throw new IllegalArgumentException("商品信息或商品ID不能为空");
       }

       // 2. 更新数据库
       boolean updateResult = updateById(product);
       if (!updateResult) {
           log.error("更新商品失败:商品ID={}不存在或更新失败", product.getId());
           return false;
       }

       // 3. 失效各级缓存(先失效本地缓存,再失效Redis缓存,避免缓存穿透)
       caffeineCacheDecorator.evictCaffeineCache(product.getId());
       redisCacheDecorator.evictRedisCache(product.getId());

       log.info("更新商品成功并失效缓存:商品ID={}, 新商品名称={}", product.getId(), product.getProductName());
       return true;
   }
}

六、控制层:对外提供API接口

创建ProductController类,对外提供商品查询和更新的RESTful API,并添加Swagger3注解,方便接口调试:

package com.jam.demo.controller;

import com.jam.demo.entity.Product;
import com.jam.demo.service.ProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.ObjectUtils;

/**
* 商品控制器:对外提供商品查询和更新API
* @author ken
*/

@Slf4j
@RestController
@RequestMapping("/api/product")
@RequiredArgsConstructor
@Tag(name = "商品管理", description = "商品查询(多级缓存)和更新接口")
public class ProductController {

   private final ProductService productService;

   /**
    * 根据商品ID查询商品(走多级缓存)
    * @param productId 商品ID
    * @return 商品信息
    */

   @Operation(
           summary = "根据商品ID查询商品",
           description = "优先从本地缓存查询,未命中则查询Redis,最后查询数据库,查询成功后回写各级缓存",
           parameters = {
                   @Parameter(name = "productId", description = "商品ID", required = true, schema = @Schema(type = "long"))
           },
           responses = {
                   @ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = Product.class))),
                   @ApiResponse(responseCode
= "400", description = "参数错误"),
                   @ApiResponse(responseCode = "404", description = "商品不存在")
           }
   )
   @GetMapping("/{productId}")
   public ResponseEntity<Product> getProductById(@PathVariable Long productId) {
       if (ObjectUtils.isEmpty(productId)) {
           return ResponseEntity.badRequest().build();
       }

       Product product = productService.getProductByIdWithMultiLevelCache(productId);
       if (ObjectUtils.isEmpty(product)) {
           return ResponseEntity.notFound().build();
       }

       return ResponseEntity.ok(product);
   }

   /**
    * 更新商品信息(同步失效各级缓存)
    * @param product 商品信息
    * @return 更新结果
    */

   @Operation(
           summary = "更新商品信息",
           description = "更新数据库后,同步失效本地缓存和Redis缓存,避免数据不一致",
           requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                   description = "商品信息(必须包含商品ID)",
                   required = true,
                   content = @Content(schema = @Schema(implementation = Product.class))
           ),
           responses
= {
                   @ApiResponse(responseCode = "200", description = "更新成功"),
                   @ApiResponse(responseCode = "400", description = "参数错误"),
                   @ApiResponse(responseCode = "500", description = "更新失败")
           }
   )
   @PutMapping
   public ResponseEntity<Boolean> updateProduct(@RequestBody Product product) {
       try {
           boolean updateResult = productService.updateProductWithCacheEvict(product);
           return ResponseEntity.ok(updateResult);
       } catch (IllegalArgumentException e) {
           log.error("更新商品参数错误:{}", e.getMessage());
           return ResponseEntity.badRequest().body(false);
       } catch (Exception e) {
           log.error("更新商品失败:{}", e.getMessage(), e);
           return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(false);
       }
   }
}

七、核心逻辑验证:测试多级缓存效果

我们通过Postman或Swagger3调试接口,验证多级缓存的查询链路和缓存失效逻辑,确保所有代码可正常运行。

7.1 启动项目

启动SpringBoot应用,确保MySQL和Redis服务已正常启动,应用启动成功后,访问http://localhost:8080/swagger-ui.html可看到Swagger3的接口文档。

7.2 测试缓存查询链路

调用接口GET /api/product/1001(查询ID为1001的商品),观察日志输出:

  1. 第一次调用:本地缓存未命中 → Redis缓存未命中 → 数据库查询成功 → 回写Redis缓存 → 回写本地缓存。 日志如下:

INFO 12345 --- [nio-8080-exec-1] c.j.d.s.i.DBDataQueryServiceImpl         : 数据库查询成功:商品ID=1001, 商品名称=Apple iPhone 15 Pro
INFO 12345 --- [nio-8080-exec-1] c.j.d.s.d.RedisCacheDecorator            : Redis缓存回写成功:商品ID=1001, 商品名称=Apple iPhone 15 Pro, 过期时间=30分钟
INFO 12345 --- [nio-8080-exec-1] c.j.d.s.d.CaffeineCacheDecorator         : 本地缓存回写成功:商品ID=1001, 商品名称=Apple iPhone 15 Pro

  1. 第二次调用:本地缓存命中,直接返回数据。 日志如下:

INFO 12345 --- [nio-8080-exec-2] c.j.d.s.d.CaffeineCacheDecorator         : 本地缓存命中:商品ID=1001, 商品名称=Apple iPhone 15 Pro

  1. 清除本地缓存(可通过调试模式手动清除)后第三次调用:本地缓存未命中 → Redis缓存命中 → 回写本地缓存。 日志如下:

INFO 12345 --- [nio-8080-exec-3] c.j.d.s.d.RedisCacheDecorator            : Redis缓存命中:商品ID=1001, 商品名称=Apple iPhone 15 Pro
INFO 12345 --- [nio-8080-exec-3] c.j.d.s.d.CaffeineCacheDecorator         : 本地缓存回写成功:商品ID=1001, 商品名称=Apple iPhone 15 Pro

7.3 测试缓存失效逻辑

调用接口PUT /api/product,传入更新后的商品信息:

{
 "id": 1001,
 "productName": "Apple iPhone 15 Pro (256GB)",
 "price": 9799.00,
 "stock": 450
}

观察日志输出:

INFO 12345 --- [nio-8080-exec-4] c.j.d.s.i.ProductServiceImpl             : 更新商品成功并失效缓存:商品ID=1001, 新商品名称=Apple iPhone 15 Pro (256GB)
INFO 12345 --- [nio-8080-exec-4] c.j.d.s.d.CaffeineCacheDecorator         : 本地缓存失效成功:商品ID=1001
INFO 12345 --- [nio-8080-exec-4] c.j.d.s.d.RedisCacheDecorator            : Redis缓存失效成功:商品ID=1001, 缓存key=product:redis:1001

再次调用GET /api/product/1001,观察日志:本地缓存未命中 → Redis缓存未命中 → 数据库查询(获取更新后的数据) → 回写各级缓存,说明缓存失效逻辑生效。

八、深度剖析:多级缓存+装饰器模式的核心设计亮点

8.1 装饰器模式的灵活扩展性

如果需要新增缓存层级(如"本地缓存→Redis→分布式缓存集群→数据库"),只需新增一个分布式缓存装饰器(如ClusterRedisCacheDecorator),修改CacheServiceConfig中的组合逻辑即可,无需修改原有任何代码,完全符合"开闭原则"。

示例:新增分布式缓存装饰器后的组合逻辑

@Bean
public DataQueryService multiLevelCacheQueryService(DBDataQueryServiceImpl dbDataQueryService,
                                                  RedisTemplate<String, Object> redisTemplate,
                                                  Cache<Long, Object> caffeineCache,
                                                  ClusterRedisTemplate clusterRedisTemplate)
{
   // 数据库 → 分布式缓存 → Redis → 本地缓存
   ClusterRedisCacheDecorator clusterRedisDecorator = new ClusterRedisCacheDecorator(dbDataQueryService, clusterRedisTemplate);
   RedisCacheDecorator redisCacheDecorator = new RedisCacheDecorator(clusterRedisDecorator, redisTemplate);
   return new CaffeineCacheDecorator(redisCacheDecorator, caffeineCache);
}

8.2 缓存一致性保障

本架构通过"更新数据后同步失效各级缓存"的方式保障缓存一致性,核心逻辑:

  1. 先更新数据库,再失效缓存(避免先失效缓存导致并发查询穿透到数据库)。
  2. 先失效本地缓存,再失效Redis缓存(本地缓存仅单进程可见,失效成本低;Redis缓存集群共享,失效后需重新查询数据库回写)。
8.3 性能优化点
  1. 本地缓存使用Caffeine,基于LRU算法淘汰数据,支持过期时间配置,适合存储热点数据。
  2. Redis缓存设置过期时间,避免缓存雪崩(可结合随机过期时间进一步优化)。
  3. 装饰器模式通过组合而非继承,减少类冗余,提高代码复用性。

九、常见问题与解决方案

9.1 缓存穿透问题

问题:查询不存在的商品ID(如9999),会穿透本地缓存和Redis,直接访问数据库,导致数据库压力增大。解决方案

  1. 对查询结果为null的情况,也缓存空值(设置较短的过期时间,如5分钟)。
  2. 实现布隆过滤器,过滤不存在的商品ID,直接返回null。
9.2 缓存击穿问题

问题:热点商品的缓存过期瞬间,大量并发请求穿透到数据库。解决方案

  1. 热点数据永不过期(通过后台线程定期更新缓存)。
  2. 缓存过期时,添加互斥锁(如Redis的SETNX),只允许一个线程查询数据库并回写缓存,其他线程等待。
9.3 缓存雪崩问题

问题:大量缓存同时过期,导致大量并发请求穿透到数据库。解决方案

  1. 缓存过期时间添加随机值(如30±5分钟),避免集中过期。
  2. 多级缓存架构,本地缓存可作为最后一道屏障。
  3. Redis集群部署,避免单点故障。

十、总结

本文通过"理论+实战"的方式,详细讲解了如何用装饰器模式实现多级缓存架构,核心要点:

  1. 多级缓存的核心价值是"就近获取数据",平衡性能与可用性。
  2. 装饰器模式通过"动态组合"实现缓存层级的叠加,灵活且解耦。
  3. 深入剖析了架构的扩展性、缓存一致性保障和性能优化点,并提供了常见缓存问题的解决方案。

通过本文的学习,你不仅能掌握多级缓存和装饰器模式的核心逻辑,还能直接将实战代码应用到生产环境中,解决高并发系统的性能瓶颈。后续可根据业务需求扩展缓存策略(如新增缓存预热、缓存降级等功能),进一步提升系统的可用性和稳定性。

目录
相关文章
|
1月前
|
存储 算法 Java
告别if-else臃肿代码!策略模式在业务中的落地实践与底层逻辑剖析
策略模式是Java后端开发中解决多分支逻辑问题的“利器”,其核心思想是“封装变化、依赖抽象”,通过抽象策略、具体策略、上下文(或工厂)三个核心角色,实现算法的灵活封装与扩展。
82 1
|
数据采集 Java 开发者
OpenSource - Spring Startup Ananlyzer
OpenSource - Spring Startup Ananlyzer
360 1
|
算法 Unix Linux
【C/C++ 实用工具】性能分析工具一览
【C/C++ 实用工具】性能分析工具一览
1144 0
|
1月前
|
机器学习/深度学习 数据采集 自然语言处理
基于深度学习+NLP豆瓣电影数据爬虫可视化推荐系统
本研究构建基于深度学习与NLP的豆瓣电影数据系统,融合LSTM、BERT与CNN技术,实现高效爬取、情感分析、个性化推荐与动态可视化,提升影视数据分析效率与推荐精准度,推动产业智能化升级。
|
存储 缓存 监控
一文读懂分布式架构知识体系(内含超全核心知识大图)
7月9日 19:00-21:30 阿里云开发者社区首场“Offer 5000”直播开启!15位团队技术大牛在线招人,更有《阿里云技术面试红宝书》助你拿下Offer!马上投递简历:https://developer.aliyun.com/special/offerday01
20143 0
|
数据采集 DataWorks 大数据
数据开发平台/工具对比测评:
数据开发平台/工具对比测评
448 23
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
1520 2
|
消息中间件 存储 NoSQL
三歪吐血总结了各个中间件是如何实现持久化的
到目前为止,三歪也已经接触到了不少的中间件了,比如说「Elasticsearch」「Redis」「HDFS」「Kafka」「HBase」等等。 可以发现的是,它们的持久化机制都差不得太多。今天想来总结一下,一方面想来回顾一下这些组件,一方面给还没入门过这些中间件的同学总结一下持久化的”套路“,后面再去学习的时候就会轻松很多。
365 59
三歪吐血总结了各个中间件是如何实现持久化的
|
算法 Java Go
深入了解堆排序算法
深入了解堆排序算法
290 1
|
数据采集 人工智能 运维
智能运维:AI在IT基础设施管理中的应用与挑战
随着人工智能技术的飞速发展,其在IT基础设施管理领域的应用日益广泛。本文将探讨AI技术在智能运维中的作用,分析其带来的优势与面临的挑战,并展望未来的发展趋势。