⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 夏壹分享 「夏壹分享」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

在 Spring Boot 项目开发中,直接注入 RedisTemplate 操作缓存是很常见的做法,但这种方式会把业务逻辑和 Redis 强绑定。

一旦后续想切换成本地缓存,或者对缓存架构做升级改造,业务代码就得大面积修改,维护成本极高。

本文基于 Spring Cache 抽象 和 条件装配 机制,打造了一套可插拔的缓存解决方案:只需修改一行配置,就能在 Caffeine 本地缓存和 Redis 分布式缓存之间自由切换,业务代码完全不用动。

同时,针对多租户场景下容易被忽略的“数据隔离”问题,我们通过装饰器模式在框架底层透明解决,业务开发人员不用操心缓存 Key 的租户前缀,实现无感知的多租户缓存隔离。

核心概念

Spring Cache 抽象

Spring Cache 是 Spring 框架提供的缓存抽象层,核心就两个关键接口:

  • CacheManager:缓存管理器,负责创建和管理 Cache 实例。
  • Cache:缓存操作接口,提供 get、put、evict、clear 等核心缓存操作方法。

⚠️ 核心要点:业务代码只依赖 CacheManager 和 Cache 接口,不直接引用 Caffeine 或 Redis 的具体实现类。

这是实现缓存无缝切换的基础,也是面向接口编程的典型应用。

整体架构设计

切换缓存类型仅需修改配置文件中 lanjii.cache.type 的值:

lanjii:
cache:
type: LOCAL # 可选值:LOCAL(Caffeine)、REDIS(Redis)

整体架构的核心思路是:基于 Spring 条件装配,根据配置动态激活对应的 CacheManager,同时通过装饰器模式实现多租户缓存隔离,全程对业务代码透明。

实现步骤

1 引入核心依赖

在缓存框架模块的 pom.xml 中,同时引入 Caffeine 和 Redis 相关依赖(后续通过条件装配控制只激活其一):

<dependencies>
<!-- Spring Cache 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Caffeine 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>

<!-- JSON 序列化依赖(支持 Java 8 时间类型) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<!-- Redis 分布式缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

⚠️ 关键说明:两个缓存依赖同时存在于 classpath 中,但通过条件装配逻辑,最终只会激活其中一个 CacheManager 实例,避免冲突。

2 定义缓存配置属性

通过 @ConfigurationProperties 注解,将 YAML 中的缓存配置映射为 Java 对象,方便代码中读取和使用:

package com.lanjii.framework.cache.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;

/**
* 缓存配置属性类
* 映射配置文件中 lanjii.cache 前缀的配置项
*/
@Data
@ConfigurationProperties(prefix = "lanjii.cache")
public class LanjiiCacheProperties {
/**
* 缓存类型:LOCAL(Caffeine)/ REDIS(Redis),默认 LOCAL
*/
private CacheType type = CacheType.LOCAL;

/**
* 是否缓存空值,防止缓存穿透,默认 true
*/
private boolean cacheNullValues = true;

/**
* 全局默认缓存过期时间,默认 1 小时
*/
private Duration defaultTtl = Duration.ofHours(1);

/**
* Caffeine 缓存最大条目数(Redis 模式下无效),默认 1000
*/
private long defaultMaxSize = 1000L;

/**
* 缓存类型枚举
*/
public enum CacheType {
LOCAL, // 本地缓存(Caffeine)
REDIS // 分布式缓存(Redis)
}
}

字段说明:

  • type:缓存切换核心开关,默认使用 LOCAL 本地缓存,降低初期部署成本。
  • cacheNullValues:开启后可缓存空值,避免大量请求查询不存在的 Key 直接穿透到数据库。
  • defaultTtl:全局缓存过期时间,支持 Spring 标准的 Duration 格式(如 1h、30m、7d)。
  • defaultMaxSize:仅对 Caffeine 生效,超过阈值后按 W-TinyLFU 算法淘汰旧数据。

3 定义缓存元数据(CacheDef)

每个缓存实例都有专属的过期时间、最大容量、是否租户隔离等属性,用 CacheDef 封装这些元数据:

package com.lanjii.framework.cache.core;

import java.time.Duration;

/**
* 缓存元数据定义类
* 封装单个缓存的专属配置(过期时间、最大容量、租户隔离等)
*/
public class CacheDef {
// 缓存名称(唯一标识)
private final String name;
// 缓存过期时间
private final Duration ttl;
// Caffeine 最大条目数(Redis 无效)
private final long maxSize;
// 是否开启租户隔离
private final boolean tenantIsolated;

// 私有构造器,通过静态工厂方法创建实例
private CacheDef(String name, Duration ttl, long maxSize, boolean tenantIsolated) {
this.name = name;
this.ttl = ttl;
this.maxSize = maxSize;
this.tenantIsolated = tenantIsolated;
}

/**
* 静态工厂方法:创建默认最大容量、开启租户隔离的缓存定义
* @param name 缓存名称
* @param ttl 过期时间
* @return CacheDef 实例
*/
public static CacheDef of(String name, Duration ttl) {
return new CacheDef(name, ttl, 1000L, true);
}

/**
* 静态工厂方法:自定义最大容量、开启租户隔离的缓存定义
* @param name 缓存名称
* @param ttl 过期时间
* @param maxSize 最大条目数
* @return CacheDef 实例
*/
public static CacheDef of(String name, Duration ttl, long maxSize) {
return new CacheDef(name, ttl, maxSize, true);
}

/**
* 静态工厂方法:默认最大容量、自定义租户隔离的缓存定义
* @param name 缓存名称
* @param ttl 过期时间
* @param tenantIsolated 是否租户隔离
* @return CacheDef 实例
*/
public static CacheDef of(String name, Duration ttl, boolean tenantIsolated) {
return new CacheDef(name, ttl, 1000L, tenantIsolated);
}

/**
* 静态工厂方法:全参数自定义缓存定义
* @param name 缓存名称
* @param ttl 过期时间
* @param maxSize 最大条目数
* @param tenantIsolated 是否租户隔离
* @return CacheDef 实例
*/
public static CacheDef of(String name, Duration ttl, long maxSize, boolean tenantIsolated) {
return new CacheDef(name, ttl, maxSize, tenantIsolated);
}

// 省略 getter 方法
public String getName() { return name; }
public Duration getTtl() { return ttl; }
public long getMaxSize() { return maxSize; }
public boolean isTenantIsolated() { return tenantIsolated; }
}

设计说明:采用静态工厂方法而非 Builder 模式,因为缓存定义通常是常量,参数固定,工厂方法更简洁、易维护。

4 缓存注册表(CacheRegistry)

CacheRegistry 是缓存定义的中央注册表,业务模块启动时将自身的 CacheDef 注册进来,方便框架统一管理:

package com.lanjii.framework.cache.core;

import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
* 缓存定义注册表
* 统一管理所有业务模块的缓存定义,线程安全
*/
public class CacheRegistry {
// 线程安全的 Map 存储缓存定义,key=缓存名称,value=CacheDef
private final ConcurrentMap<String, CacheDef> cacheDefMap = new ConcurrentHashMap<>();

/**
* 注册单个缓存定义
* @param cacheDef 缓存定义实例
*/
public void register(CacheDef cacheDef) {
cacheDefMap.put(cacheDef.getName(), cacheDef);
}

/**
* 批量注册缓存定义
* @param cacheDefs 缓存定义数组
*/
public void registerAll(CacheDef... cacheDefs) {
for (CacheDef cacheDef : cacheDefs) {
register(cacheDef);
}
}

/**
* 根据名称获取缓存定义
* @param name 缓存名称
* @return 可选的 CacheDef 实例(避免空指针)
*/
public Optional<CacheDef> get(String name) {
return Optional.ofNullable(cacheDefMap.get(name));
}

/**
* 获取所有缓存定义
* @return 缓存定义集合
*/
public Collection<CacheDef> getAll() {
return cacheDefMap.values();
}
}

关键设计:使用 ConcurrentHashMap 保证线程安全,因为多个业务模块可能在 @PostConstruct 阶段并发注册缓存定义。

5 条件装配:实现“一行切换”的核心

这是实现缓存一键切换的关键!通过 @ConditionalOnProperty 注解,Spring Boot 会根据配置值动态创建对应的 CacheManager:

package com.lanjii.framework.cache.config;

import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.cache.properties.LanjiiCacheProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;

/**
* 缓存自动配置类
* 核心:根据配置动态激活 Caffeine / Redis 缓存管理器
*/
@AutoConfiguration
@EnableCaching // 开启 Spring Cache 注解支持
@EnableConfigurationProperties(LanjiiCacheProperties.class) // 启用配置属性绑定
public class LanjiiCacheAutoConfiguration {

/**
* 注册缓存注册表 Bean
* @return CacheRegistry 实例
*/
@Bean
@ConditionalOnMissingBean // 避免重复注册
public CacheRegistry cacheRegistry() {
return new CacheRegistry();
}

/**
* 注册 Caffeine 缓存管理器(默认激活)
* @param cacheRegistry 缓存注册表
* @param properties 缓存配置属性
* @return 支持租户隔离的 CaffeineCacheManager
*/
@Bean
@ConditionalOnProperty(
name = "lanjii.cache.type",
havingValue = "LOCAL",
matchIfMissing = true // 配置缺失时默认激活
)
public CacheManager caffeineCacheManager(CacheRegistry cacheRegistry, LanjiiCacheProperties properties) {
return new TenantAwareCaffeineCacheManager(cacheRegistry, properties);
}
}

核心要点:matchIfMissing = true 表示如果配置文件中未指定 lanjii.cache.type,默认使用 LOCAL 模式(Caffeine)。

这样即使不配置缓存类型,系统也能以本地缓存方式正常运行,降低初期接入成本。

Redis 缓存管理器的配置单独放在一个类中,保证职责单一:

package com.lanjii.framework.cache.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.lanjii.framework.cache.core.CacheDef;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.cache.properties.LanjiiCacheProperties;
import com.lanjii.framework.context.tenant.TenantContext;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.util.HashMap;
import java.util.Map;

/**
* Redis 缓存配置类
* 仅当 lanjii.cache.type=REDIS 时激活
*/
@Configuration
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "REDIS")
public class RedisCacheConfigurationConfig {

/**
* 构建 Redis 缓存管理器
* @param cacheRegistry 缓存注册表
* @param properties 缓存配置属性
* @param connectionFactory Redis 连接工厂
* @return RedisCacheManager 实例
*/
@Bean
@ConditionalOnClass(RedisConnectionFactory.class) // 确保 Redis 依赖存在时才创建
public CacheManager redisCacheManager(CacheRegistry cacheRegistry,
LanjiiCacheProperties properties,
RedisConnectionFactory connectionFactory) {
// 1. 配置 JSON 序列化器(支持 Java 8 时间类型 + 类型信息)
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule()); // 支持 LocalDateTime 等时间类型
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 禁用时间戳序列化
// 激活默认类型信息(反序列化时还原对象类型)
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

// 2. 构建默认 Redis 缓存配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(properties.getDefaultTtl()) // 设置全局默认过期时间
.serializeValuesWith( // 设置值的序列化方式
RedisSerializationContext.SerializationPair.fromSerializer(serializer)
)
.computePrefixWith(cacheName -> {
// 多租户前缀:{tenantId}:{cacheName}::
Long tenantId = TenantContext.getTenantId();
String prefix = (tenantId != null) ? tenantId.toString() : "0";
return prefix + ":" + cacheName + "::";
});

// 禁用缓存空值(如果配置要求)
if (!properties.isCacheNullValues()) {
defaultConfig = defaultConfig.disableCachingNullValues();
}

// 3. 为每个注册的缓存设置专属过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
for (CacheDef cacheDef : cacheRegistry.getAll()) {
configMap.put(
cacheDef.getName(),
defaultConfig.entryTtl(cacheDef.getTtl()) // 覆盖默认过期时间
);
}

// 4. 构建并返回 RedisCacheManager
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig) // 设置默认配置
.withInitialCacheConfigurations(configMap) // 设置自定义缓存配置
.build();
}
}

Redis 配置关键说明

  1. \1. 序列化设计:使用 GenericJackson2JsonRedisSerializer 替代默认的 JDK 序列化,Redis 中的数据为可读的 JSON 格式,便于调试。

  2. \2. 类型信息:activateDefaultTyping 会在 JSON 中嵌入 @class 字段,反序列化时能自动还原多态/泛型对象,但存在安全风险(下文会说明)。

  3. \3. 多租户隔离:通过 computePrefixWith 为每个缓存 Key 自动添加租户 ID 前缀,实现分布式环境下的租户数据隔离。

  4. \4. 安全警示:

    • activateDefaultTyping 是“双刃剑”:内网环境(Redis 有密码保护)可使用,开发效率高;
  • 公网环境禁止使用,建议在 DTO 类上显式添加 @JsonTypeInfo 注解,或自定义序列化器处理类型转换。

Redis Key 设计规范

采用冒号分隔的命名空间设计:{tenantId}:{cacheName}::{key}(示例:1001:userInfo::admin)。

优势:在 Redis 可视化工具中,可按租户、缓存名称、Key 维度快速排查问题,可读性和可维护性大幅提升。

6 本地缓存多租户隔离:装饰器模式实战

这是整套方案的技术难点!Spring 原生的 CaffeineCacheManager 是全局单例,多租户场景下会出现数据串读问题(租户 A 读到租户 B 的数据)。

Redis 可通过 Key 前缀解决,但本地缓存无官方支持,我们用装饰器模式包装 Cache 实例,自动为 Key 添加租户前缀:

package com.lanjii.framework.cache.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.lanjii.framework.cache.core.CacheDef;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.cache.properties.LanjiiCacheProperties;
import com.lanjii.framework.context.tenant.TenantContext;
import org.springframework.cache.Cache;
import org.springframework.cache.caffeine.CaffeineCacheManager;

import java.util.concurrent.Callable;

/**
* 支持租户隔离的 Caffeine 缓存管理器
* 核心:通过装饰器模式为 Key 自动添加租户前缀
*/
public class TenantAwareCaffeineCacheManager extends CaffeineCacheManager {
// 缓存注册表
private final CacheRegistry cacheRegistry;
// 缓存配置属性
private final LanjiiCacheProperties properties;

/**
* 构造器注入依赖
* @param cacheRegistry 缓存注册表
* @param properties 缓存配置属性
*/
public TenantAwareCaffeineCacheManager(CacheRegistry cacheRegistry, LanjiiCacheProperties properties) {
this.cacheRegistry = cacheRegistry;
this.properties = properties;
this.setAllowNullValues(properties.isCacheNullValues()); // 配置是否缓存空值
}

/**
* 重写 getCache 方法,返回支持租户隔离的 Cache 实例
* @param name 缓存名称
* @return 装饰后的 Cache 实例
*/
@Override
public Cache getCache(String name) {
// 获取原生 Caffeine Cache 实例
Cache delegate = super.getCache(name);

// 如果缓存不存在,创建并注册
if (delegate == null) {
delegate = createAndRegisterCache(name);
}
if (delegate == null) {
return null;
}

// 判断当前缓存是否需要租户隔离
CacheDef cacheDef = cacheRegistry.get(name).orElse(null);
if (cacheDef != null && cacheDef.isTenantIsolated()) {
// 用装饰器包装,添加租户前缀
return new TenantAwareCache(delegate);
}
// 无需租户隔离,返回原生实例
return delegate;
}

/**
* 同步创建并注册缓存(避免并发创建问题)
* @param name 缓存名称
* @return 创建后的 Cache 实例
*/
private synchronized Cache createAndRegisterCache(String name) {
// 双重检查:避免重复创建
Cache existing = super.getCache(name);
if (existing != null) {
return existing;
}

// 获取缓存定义(自定义配置)
CacheDef cacheDef = cacheRegistry.get(name).orElse(null);

// 构建 Caffeine 缓存实例
Caffeine<Object, Object> builder = Caffeine.newBuilder();
if (cacheDef != null) {
// 使用自定义配置
builder.expireAfterWrite(cacheDef.getTtl()) // 过期时间
.maximumSize(cacheDef.getMaxSize()); // 最大条目数
} else {
// 使用全局默认配置
builder.expireAfterWrite(properties.getDefaultTtl())
.maximumSize(properties.getDefaultMaxSize());
}

// 注册自定义缓存
this.registerCustomCache(name, builder.build());
return super.getCache(name);
}

/**
* 租户隔离缓存装饰器
* 包装原生 Cache,自动为 Key 添加租户前缀
*/
static class TenantAwareCache implements Cache {
// 被装饰的原生 Cache 实例
private final Cache delegate;

public TenantAwareCache(Cache delegate) {
this.delegate = delegate;
}

/**
* 生成带租户前缀的 Key
* @param key 原始 Key
* @return 带租户前缀的 Key
*/
private Object createTenantKey(Object key) {
Long tenantId = TenantContext.getTenantId();
String prefix = (tenantId != null) ? tenantId.toString() : "0";
return prefix + ":" + key;
}

// 装饰原生方法:添加租户前缀
@Override
public String getName() { return delegate.getName(); }

@Override
public Object getNativeCache() { return delegate.getNativeCache(); }

@Override
public ValueWrapper get(Object key) { return delegate.get(createTenantKey(key)); }

@Override
public <T> T get(Object key, Class<T> type) { return delegate.get(createTenantKey(key), type); }

@Override
public <T> T get(Object key, Callable<T> valueLoader) { return delegate.get(createTenantKey(key), valueLoader); }

@Override
public void put(Object key, Object value) { delegate.put(createTenantKey(key), value); }

@Override
public void evict(Object key) { delegate.evict(createTenantKey(key)); }

@Override
public boolean evictIfPresent(Object key) { return delegate.evictIfPresent(createTenantKey(key)); }

@Override
public void clear() { delegate.clear(); }

@Override
public boolean invalidate() { return delegate.invalidate(); }

@Override
public ValueWrapper putIfAbsent(Object key, Object value) { return delegate.putIfAbsent(createTenantKey(key), value); }
}
}

核心设计思路

\1. 装饰器模式:TenantAwareCache 包装原生 Cache,所有 Key 操作都会先添加租户前缀,业务层无感知。
\2. 租户隔离开关:CacheDef 中的 tenantIsolated 字段可控制是否开启隔离,全局共享数据(如字典)可关闭,避免内存浪费。
\3. 并发安全:createAndRegisterCache 方法加同步锁,避免多线程并发创建缓存实例。

多租户使用注意事项

\1. 租户上下文必须提前设置:TenantContext.getTenantId() 需在请求入口(Filter/Interceptor)设置,否则所有 Key 都会用 0: 前缀,导致数据混读。
\2. clear 方法慎用:clear() 会清空缓存名下所有租户的数据,如需仅清空当前租户,应逐个 evict 已知 Key。
\3. 非多租户项目适配:将所有 CacheDef 的 tenantIsolated 设为 false 即可,框架会跳过前缀逻辑,无性能损耗。

7 缓存辅助工具(CacheHelper)

封装 Spring Cache 未覆盖的扩展能力(如获取缓存中所有值),简化业务层操作:

package com.lanjii.framework.cache.helper;

import com.lanjii.framework.cache.core.CacheDef;
import com.lanjii.framework.cache.core.CacheRegistry;
import com.lanjii.framework.context.tenant.TenantContext;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;

/**
* 缓存辅助工具类
* 封装扩展缓存操作,简化业务层调用
*/
@Component
@RequiredArgsConstructor
public class CacheHelper {
// 缓存管理器
private final CacheManager cacheManager;
// 缓存注册表
private final CacheRegistry cacheRegistry;

/**
* 获取缓存中所有值(仅支持 LOCAL 模式)
* @param cacheName 缓存名称
* @param type 数据类型
* @return 缓存值集合
*/
public <T> Collection<T> getAllValues(String cacheName, Class<T> type) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
return Collections.emptyList();
}

// 获取原生 Caffeine 缓存
Object nativeCache = cache.getNativeCache();
if (nativeCache instanceof com.github.benmanes.caffeine.cache.Cache) {
com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache =
(com.github.benmanes.caffeine.cache.Cache<Object, Object>) nativeCache;

// 判断是否需要租户隔离
CacheDef cacheDef = cacheRegistry.get(cacheName).orElse(null);
boolean tenantIsolated = cacheDef == null || cacheDef.isTenantIsolated();

if (tenantIsolated) {
// 过滤当前租户的数据
Long tenantId = TenantContext.getTenantId();
String tenantPrefix = (tenantId != null ? tenantId.toString() : "0") + ":";
return caffeineCache.asMap().entrySet().stream()
.filter(entry -> entry.getKey().toString().startsWith(tenantPrefix))
.map(entry -> type.cast(entry.getValue()))
.collect(Collectors.toList());
} else {
// 返回所有数据(全局共享)
return caffeineCache.asMap().values().stream()
.map(type::cast)
.collect(Collectors.toList());
}
}

// Redis 模式下不支持此操作
throw new UnsupportedOperationException("getAllValues is only supported in LOCAL mode");
}

/**
* 清空指定缓存
* @param cacheName 缓存名称
*/
public void clearAll(String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
}

/**
* 获取缓存实例
* @param cacheName 缓存名称
* @return Cache 实例
*/
public Cache getCache(String cacheName) {
return cacheManager.getCache(cacheName);
}
}

注意:getAllValues 仅支持 LOCAL 模式,Redis 模式下如需遍历数据,应使用 RedisTemplate + SCAN 命令(避免 KEYS * 阻塞 Redis)。

8 自动配置注册

让 Spring Boot 识别这套缓存框架,需在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中注册自动配置类:

com.lanjii.framework.cache.config.LanjiiCacheAutoConfiguration

说明:Spring Boot 3.x 采用此文件替代旧版的 spring.factories,实现自动配置类的加载。

业务模块接入示例

框架搭建完成后,业务模块接入仅需三步,全程无需关注底层缓存实现:

1 声明缓存常量

定义业务专属的缓存元数据(常量):

package com.lanjii.sys.config;

import com.lanjii.framework.cache.core.CacheDef;
import java.time.Duration;

/**
* 系统模块缓存常量
* 定义所有系统缓存的元数据
*/
public interface SystemCacheConstants {
/** 字典数据缓存(全局共享,无需租户隔离) */
CacheDef DICT_DATA = CacheDef.of("dictData", Duration.ofHours(24), false);

/** 系统配置缓存(全局共享) */
CacheDef SYS_CONFIG = CacheDef.of("sysConfig", Duration.ofDays(7), false);

/** 用户会话缓存(全局共享) */
CacheDef USER_SESSION = CacheDef.of("userSession", Duration.ofHours(24), false);

/** 用户信息缓存(租户隔离,默认配置) */
CacheDef USER_INFO = CacheDef.of("userInfo", Duration.ofHours(24));

/** 验证码缓存(租户隔离,5分钟过期) */
CacheDef CAPTCHA = CacheDef.of("captcha", Duration.ofMinutes(5));
}

2 启动时注册缓存

在业务模块配置类中,将缓存常量注册到 CacheRegistry:

package com.lanjii.sys.config;

import com.lanjii.framework.cache.core.CacheRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;

/**
* 系统缓存配置类
* 注册系统模块的缓存定义
*/
@Configuration
@RequiredArgsConstructor
public class SystemCacheConfig {
// 注入缓存注册表
private final CacheRegistry cacheRegistry;

/**
* 项目启动时注册缓存
*/
@PostConstruct
public void registerCaches() {
cacheRegistry.registerAll(
SystemCacheConstants.DICT_DATA,
SystemCacheConstants.SYS_CONFIG,
SystemCacheConstants.USER_SESSION,
SystemCacheConstants.USER_INFO,
SystemCacheConstants.CAPTCHA
);
}
}

3 业务层使用缓存

以系统配置服务为例,业务代码仅依赖 Spring Cache 接口,完全不感知底层缓存类型:

package com.lanjii.sys.service.impl;

import com.lanjii.framework.cache.helper.CacheHelper;
import com.lanjii.sys.config.SystemCacheConstants;
import com.lanjii.sys.dto.SysConfigDTO;
import com.lanjii.sys.entity.SysConfig;
import com.lanjii.sys.service.SysConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

/**
* 系统配置服务实现类
* 缓存使用示例:仅依赖 CacheManager / Cache 接口
*/
@Service("sysConfigService")
@RequiredArgsConstructor
public class SysConfigServiceImpl implements SysConfigService {
// 缓存管理器
private final CacheManager cacheManager;
// 缓存辅助工具
private final CacheHelper cacheHelper;

/**
* 获取系统配置缓存实例
* @return 配置缓存
*/
private Cache getConfigCache() {
return cacheManager.getCache(SystemCacheConstants.SYS_CONFIG.getName());
}

/**
* 根据 Key 查询系统配置(优先查缓存)
* @param configKey 配置 Key
* @return 系统配置
*/
@Override
public SysConfig getConfigByKey(String configKey) {
Cache cache = getConfigCache();

// 1. 先查缓存
SysConfig config = cache.get(configKey, SysConfig.class);
if (config != null) {
return config;
}

// 2. 缓存未命中,查数据库
config = getOne(new LambdaQueryWrapper<SysConfig>()
.eq(SysConfig::getConfigKey, configKey));

// 3. 数据库查到数据,写入缓存
if (config != null) {
cache.put(configKey, config);
}
return config;
}

/**
* 更新配置(更新后清除缓存)
* @param id 配置 ID
* @param dto 配置DTO
*/
@Override
public void updateByIdNew(Long id, SysConfigDTO dto) {
// 1. 查询原始配置
SysConfig originalConfig = getById(id);
// 2. 更新数据库
SysConfig entity = convertToEntity(dto);
updateById(entity);
// 3. 清除缓存(避免缓存脏数据)
getConfigCache().evict(originalConfig.getConfigKey());
}

/**
* 清空系统配置缓存
*/
@Override
public void clearCache() {
cacheHelper.clearAll(SystemCacheConstants.SYS_CONFIG.getName());
}

// 省略其他业务方法...
private SysConfig convertToEntity(SysConfigDTO dto) {
// 转换逻辑...
return new SysConfig();
}
}

核心优势:业务代码仅依赖 CacheManager 和 Cache 接口,切换缓存类型时,无需修改任何业务代码!

架构模式切换

这套方案的核心价值是“配置驱动架构”,可根据部署场景灵活切换缓存模式:

1 轻量级单机模式(LOCAL)

适合单体应用、私有化小型部署或低延迟要求的场景,无需依赖外部组件:

lanjii:
cache:
type: LOCAL # 默认值,可省略
default-ttl: 1h
default-max-size: 2000

2 分布式集群模式(REDIS)

适合微服务架构、多实例部署或数据持久化的场景,实现多节点缓存共享:

lanjii:
cache:
type: REDIS
default-ttl: 2h
cache-null-values: true
# Redis 基础配置(按需调整)
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0

总结

这套缓存架构方案通过 Spring Cache 抽象 + 条件装配 + 装饰器模式,实现了三大核心目标:

  1. 彻底解耦:业务代码仅依赖 Spring Cache 接口,与底层缓存实现(Caffeine/Redis)完全解耦。
  2. 一键切换:修改一行配置即可在本地缓存和分布式缓存之间切换,适配不同部署场景。
  3. 安全隔离:通过装饰器模式实现多租户缓存隔离,全程对业务透明,避免数据串读风险。

该方案兼顾了开发效率、架构灵活性和运维便捷性,可直接落地到生产环境,适配从单体到微服务的各类场景。

文章目录
  1. 1. 前言
  2. 2. 核心概念
    1. 2.0.1. Spring Cache 抽象
  • 3. 整体架构设计
  • 4. 实现步骤
    1. 4.0.1. 1 引入核心依赖
    2. 4.0.2. 2 定义缓存配置属性
    3. 4.0.3. 3 定义缓存元数据(CacheDef)
    4. 4.0.4. 4 缓存注册表(CacheRegistry)
    5. 4.0.5. 5 条件装配:实现“一行切换”的核心
      1. 4.0.5.1. Redis 配置关键说明
      2. 4.0.5.2. Redis Key 设计规范
    6. 4.0.6. 6 本地缓存多租户隔离:装饰器模式实战
      1. 4.0.6.1. 核心设计思路
      2. 4.0.6.2. 多租户使用注意事项
    7. 4.0.7. 7 缓存辅助工具(CacheHelper)
    8. 4.0.8. 8 自动配置注册
  • 5. 业务模块接入示例
    1. 5.0.1. 1 声明缓存常量
    2. 5.0.2. 2 启动时注册缓存
    3. 5.0.3. 3 业务层使用缓存
  • 6. 架构模式切换
    1. 6.0.1. 1 轻量级单机模式(LOCAL)
    2. 6.0.2. 2 分布式集群模式(REDIS)
    3. 6.0.3. 总结