一、前言:为什么需要多级缓存+装饰器模式?
在高并发系统中,缓存是提升性能的核心手段,但单一缓存架构往往无法兼顾"性能、一致性、可用性"三大核心诉求。比如:本地缓存(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缓存的查询与回写)。
装饰器模式的优势:
-
灵活性:可动态组合多个装饰器,实现不同的功能叠加(如"本地缓存+Redis"或仅用"Redis")。
-
解耦性:各装饰器独立实现,不影响核心逻辑,符合单一职责原则。
-
可扩展性:新增缓存层级(如分布式缓存集群分片)时,只需新增装饰器,无需修改原有代码。
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的依赖注入,将CaffeineCacheDecorator、RedisCacheDecorator和DBDataQueryServiceImpl组合起来,形成"本地缓存→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的商品),观察日志输出:
-
第一次调用:本地缓存未命中 → 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 -
第二次调用:本地缓存命中,直接返回数据。 日志如下:
INFO 12345 --- [nio-8080-exec-2] c.j.d.s.d.CaffeineCacheDecorator : 本地缓存命中:商品ID=1001, 商品名称=Apple iPhone 15 Pro -
清除本地缓存(可通过调试模式手动清除)后第三次调用:本地缓存未命中 → 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 缓存一致性保障
本架构通过"更新数据后同步失效各级缓存"的方式保障缓存一致性,核心逻辑:
-
先更新数据库,再失效缓存(避免先失效缓存导致并发查询穿透到数据库)。
-
先失效本地缓存,再失效Redis缓存(本地缓存仅单进程可见,失效成本低;Redis缓存集群共享,失效后需重新查询数据库回写)。
8.3 性能优化点
-
本地缓存使用Caffeine,基于LRU算法淘汰数据,支持过期时间配置,适合存储热点数据。
-
Redis缓存设置过期时间,避免缓存雪崩(可结合随机过期时间进一步优化)。
-
装饰器模式通过组合而非继承,减少类冗余,提高代码复用性。
九、常见问题与解决方案
9.1 缓存穿透问题
问题 :查询不存在的商品ID(如9999),会穿透本地缓存和Redis,直接访问数据库,导致数据库压力增大。 解决方案:
-
对查询结果为null的情况,也缓存空值(设置较短的过期时间,如5分钟)。
-
实现布隆过滤器,过滤不存在的商品ID,直接返回null。
9.2 缓存击穿问题
问题 :热点商品的缓存过期瞬间,大量并发请求穿透到数据库。 解决方案:
-
热点数据永不过期(通过后台线程定期更新缓存)。
-
缓存过期时,添加互斥锁(如Redis的SETNX),只允许一个线程查询数据库并回写缓存,其他线程等待。
9.3 缓存雪崩问题
问题 :大量缓存同时过期,导致大量并发请求穿透到数据库。 解决方案:
-
缓存过期时间添加随机值(如30±5分钟),避免集中过期。
-
多级缓存架构,本地缓存可作为最后一道屏障。
-
Redis集群部署,避免单点故障。
十、总结
本文通过"理论+实战"的方式,详细讲解了如何用装饰器模式实现多级缓存架构,核心要点:
-
多级缓存的核心价值是"就近获取数据",平衡性能与可用性。
-
装饰器模式通过"动态组合"实现缓存层级的叠加,灵活且解耦。
-
深入剖析了架构的扩展性、缓存一致性保障和性能优化点,并提供了常见缓存问题的解决方案。
通过本文的学习,你不仅能掌握多级缓存和装饰器模式的核心逻辑,还能直接将实战代码应用到生产环境中,解决高并发系统的性能瓶颈。后续可根据业务需求扩展缓存策略(如新增缓存预热、缓存降级等功能),进一步提升系统的可用性和稳定性。