多级缓存架构实战指南

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

在高并发系统中,缓存是提升性能的核心手段,但单一缓存架构往往无法兼顾"性能、一致性、可用性"三大核心诉求。比如:本地缓存(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);
四、核心架构设计:装饰器模式落地多级缓存

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

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
  2. 第二次调用:本地缓存命中,直接返回数据。 日志如下:

    复制代码
    INFO 12345 --- [nio-8080-exec-2] c.j.d.s.d.CaffeineCacheDecorator         : 本地缓存命中:商品ID=1001, 商品名称=Apple iPhone 15 Pro
  3. 清除本地缓存(可通过调试模式手动清除)后第三次调用:本地缓存未命中 → 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. 深入剖析了架构的扩展性、缓存一致性保障和性能优化点,并提供了常见缓存问题的解决方案。

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

相关推荐
没有bug.的程序员8 小时前
微服务的本质:不是拆服务,而是拆复杂度
java·jvm·spring·微服务·云原生·容器·架构
古城小栈8 小时前
微服务测试:TestContainers 集成测试实战指南
微服务·架构·集成测试
武子康8 小时前
Java-200 RabbitMQ 架构与 Exchange 路由:fanout/direct/topic/headers
java·架构·消息队列·系统架构·rabbitmq·java-rabbitmq·mq
古城小栈8 小时前
云原生架构:微服务 vs 单体应用的选择
微服务·云原生·架构
斯普信专业组8 小时前
Docker Registry 镜像缓存与客户端无感加速(以 Docker Hub 为例)
缓存·docker·eureka
MoonBit月兔8 小时前
海外开发者实践分享:用 MoonBit 开发 SQLC 插件(其一)
数据库·缓存·wasm·moonbit
IT界的奇葩8 小时前
康威定律对微服务的启示
微服务·云原生·架构
云空16 小时前
《解码机器人操作系统:从核心架构到未来趋势的深度解析》
架构·机器人
_oP_i20 小时前
Docker 整体架构
docker·容器·架构