Spring Boot 缓存架构:一行配置切换 Caffeine 与 Redis,透明支持多租户隔离

前言

在 Spring Boot 项目中,直接注入 RedisTemplate 往往会把业务逻辑绑死在 Redis 上;当你想切换本地缓存或升级缓存架构时,业务代码就需要大面积改动。

本文基于 Spring Cache 抽象与条件装配,落地一套可插拔缓存方案:一行配置即可在 Caffeine 与 Redis 之间切换,业务代码零改动,并透明支持多租户缓存隔离。同时,针对多租户场景下容易被忽略的"数据隔离"问题,我们将通过装饰器模式,在框架底层透明地解决,确保业务开发人员无需操心 Key 的租户前缀。

核心概念

Spring Cache 抽象

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

  • CacheManager :缓存管理器,负责创建和管理 Cache 实例。
  • Cache :缓存操作接口,提供 getputevictclear 等方法。

⚠️ 关键点:业务代码只依赖 CacheManagerCache 接口,不直接引用 Caffeine 或 Redis 的实现类。这是实现无缝切换的基础。

整体架构设计

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

yaml 复制代码
lanjii:
  cache:
    type: LOCAL   # 切换为 REDIS 即可启用 Redis 缓存

实现步骤

第一步:引入依赖

在缓存框架模块的 pom.xml 中同时引入 Caffeine 和 Redis 依赖:

xml 复制代码
<dependencies>
    <!-- 内部依赖 -->
    <dependency>
        <groupId>com.lanjii</groupId>
        <artifactId>framework-context</artifactId>
    </dependency>
​
    <!-- 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>
​
    <!-- Jackson (序列化) -->
    <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

第二步:定义缓存配置属性

通过 @ConfigurationProperties 将 YAML 配置映射为 Java 对象:

java 复制代码
package com.lanjii.framework.cache.properties;
​
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
​
@Data
@ConfigurationProperties(prefix = "lanjii.cache")
public class LanjiiCacheProperties {
​
    /**
     * 缓存类型:LOCAL(Caffeine)或 REDIS
     */
    private CacheType type = CacheType.LOCAL;
​
    /**
     * 是否允许缓存 Null 值(防止缓存穿透)
     */
    private boolean cacheNullValues = true;
​
    /**
     * 默认过期时间,默认 1 小时
     */
    private Duration defaultTtl = Duration.ofHours(1);
​
    /**
     * 默认最大容量(仅 Local 模式有效),默认 1000
     */
    private long defaultMaxSize = 1000L;
​
    public enum CacheType {
        LOCAL,  // 本地缓存 (Caffeine)
        REDIS   // 分布式缓存 (Redis)
    }
}

各字段说明:

  • type :核心切换开关,LOCAL 表示使用 Caffeine,REDIS 表示使用 Redis。默认 LOCAL
  • cacheNullValues :是否允许缓存空值。设为 true 可防止缓存穿透(大量请求查询不存在的 key 直接打到数据库)。
  • defaultTtl :全局默认过期时间。支持 Spring 的 Duration 格式,如 1h30m7d
  • defaultMaxSize:Caffeine 本地缓存的最大条目数。超过后按 W-TinyLFU 算法淘汰。Redis 模式下此配置无效。

第三步:定义缓存元数据(CacheDef)

每个缓存实例都有自己的 TTL(过期时间)、最大容量、是否需要租户隔离等属性。我们用一个 CacheDef 类来封装这些元数据:

arduino 复制代码
package com.lanjii.framework.cache.core;
​
import java.time.Duration;
​
public class CacheDef {
​
    private final String name;           // 缓存名称
    private final Duration ttl;          // 过期时间
    private final long maxSize;          // 最大容量(仅 Local 模式)
    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;
    }
​
    // 快捷工厂方法
    public static CacheDef of(String name, Duration ttl) {
        return new CacheDef(name, ttl, 1000L, true);
    }
​
    public static CacheDef of(String name, Duration ttl, long maxSize) {
        return new CacheDef(name, ttl, maxSize, true);
    }
​
    public static CacheDef of(String name, Duration ttl, boolean tenantIsolated) {
        return new CacheDef(name, ttl, 1000L, tenantIsolated);
    }
​
    public static CacheDef of(String name, Duration ttl, long maxSize, boolean tenantIsolated) {
        return new CacheDef(name, ttl, maxSize, tenantIsolated);
    }
​
    // getter 省略
}

CacheDef 采用静态工厂方法而非 Builder 模式,因为缓存定义通常是常量,参数固定,工厂方法更简洁。

第四步:缓存注册表(CacheRegistry)

CacheRegistry 作为缓存定义的中央注册表,业务模块在启动时将自己的 CacheDef 注册进来:

arduino 复制代码
package com.lanjii.framework.cache.core;
​
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
​
public class CacheRegistry {
​
    private final Map<String, CacheDef> cacheDefMap = new ConcurrentHashMap<>();
​
    /** 注册单个缓存定义 */
    public void register(CacheDef cacheDef) {
        cacheDefMap.put(cacheDef.getName(), cacheDef);
    }
​
    /** 批量注册 */
    public void registerAll(CacheDef... cacheDefs) {
        for (CacheDef cacheDef : cacheDefs) {
            register(cacheDef);
        }
    }
​
    /** 根据名称获取缓存定义 */
    public Optional<CacheDef> get(String name) {
        return Optional.ofNullable(cacheDefMap.get(name));
    }
​
    /** 获取所有已注册的缓存定义 */
    public Collection<CacheDef> getAll() {
        return cacheDefMap.values();
    }
}

使用 ConcurrentHashMap 保证线程安全,因为多个业务模块可能在 @PostConstruct 阶段并发注册。

第五步:真正实现"一行切换"的关键------条件装配

这是实现"一键切换"的关键。通过 @ConditionalOnProperty 注解,Spring Boot 会根据配置值决定创建哪个 CacheManager

kotlin 复制代码
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;
​
@AutoConfiguration
@EnableCaching
@EnableConfigurationProperties(LanjiiCacheProperties.class)
public class LanjiiCacheAutoConfiguration {
​
    /** 缓存注册表,供业务模块注入 */
    @Bean
    @ConditionalOnMissingBean
    public CacheRegistry cacheRegistry() {
        return new CacheRegistry();
    }
​
    /** LOCAL 模式 ------ Caffeine CacheManager(默认) */
    @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 模式。这样即使不额外配置缓存类型,系统也能先以本地缓存方式跑起来。

Redis 的配置放在单独的类中:

ini 复制代码
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.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;
​
@Configuration
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "REDIS")
public class RedisCacheConfiguration {
​
    @Bean
    @ConditionalOnClass(RedisConnectionFactory.class)
    public CacheManager redisCacheManager(CacheRegistry cacheRegistry,
                                          LanjiiCacheProperties properties,
                                          RedisConnectionFactory connectionFactory) {
        // 1. 配置 JSON 序列化(支持 Java 8 时间类型 + 类型信息)
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY
        );
​
        GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);
​
        // 2. 默认缓存配置
        org.springframework.data.redis.cache.RedisCacheConfiguration defaultConfig =
                org.springframework.data.redis.cache.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. 为每个已注册的缓存设置独立的 TTL
        Map<String, org.springframework.data.redis.cache.RedisCacheConfiguration> configMap = new HashMap<>();
        for (CacheDef cacheDef : cacheRegistry.getAll()) {
            configMap.put(cacheDef.getName(), defaultConfig.entryTtl(cacheDef.getTtl()));
        }
​
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(configMap)
                .build();
    }
}

Redis 配置中的几个关键设计点:

  • GenericJackson2JsonRedisSerializer :使用 JSON 序列化而非 Java 默认序列化,这样 Redis 中的数据人类可读,便于调试。同时注册了 JavaTimeModule 以正确序列化 LocalDateTime 等 Java 8 时间类型。
  • activateDefaultTyping :在 JSON 中保留类型信息(@class 字段),反序列化时能还原为正确的 Java 类型。
  • computePrefixWith :Redis key 格式为 {tenantId}:{cacheName}::{key},通过租户 ID 前缀实现多租户数据隔离。
  • 双重条件装配@ConditionalOnProperty 确保只在 type=REDIS 时生效;@ConditionalOnClass 确保 classpath 中有 Redis 依赖时才创建 Bean。

序列化陷阱与安全警示:

⚠️ 高危预警activateDefaultTyping 是一个"双刃剑"配置。

  • 好处 :它会在 JSON 中自动嵌入 @class 字段(如 {"@class":"com.example.User", "id":1}),这使得反序列化时能自动恢复出多态对象或泛型集合,开发极其便利。
  • 风险 :如果 Redis 端口对外暴露,或者攻击者能控制 Redis 中的数据,他们可以构造恶意 JSON(如指向 Runtime.exec 的类),在反序列化时触发远程代码执行(RCE)。

生产建议

  1. 内部系统:如果 Redis 仅在内网且有密码保护,可以使用此方案,开发效率最高。
  2. 公网系统绝对禁止 开启 activateDefaultTyping。替代方案是使用 @JsonTypeInfo 注解在特定的 DTO 类上显式声明类型信息,或者自定义 RedisSerializer 手动处理泛型转换。

Redis Key 设计规范:

本项目采用冒号分隔的命名空间设计:

css 复制代码
{tenantId}:{cacheName}::{key}
# 示例:1001:userInfo::admin

这样设计的好处:在 Redis GUI 工具中可以按写入时间、租户、缓存名称三个维度展开观察,排查问题非常直观。

第六步:难点攻克------本地缓存的多租户隔离

这一步是整个方案的技术难点,也是市面上大多数缓存教程没有覆盖的地方。

问题在哪里?

Spring 原生的 CaffeineCacheManager 是全局单例的,所有租户共享同一批缓存空间。在多租户 SaaS 场景下,如果租户 A 和租户 B 的用户恰好有相同的 key(如用户名 admin),就会发生数据串读------A 租户查到了 B 租户的缓存数据。

Redis 模式相对容易处理(通过 computePrefixWith 在 key 上添加租户前缀),但本地缓存没有类似的官方支持,需要自己动手解决。

解决思路:装饰器模式

CaffeineCacheManager 返回的 Cache 实例进行包装,用 TenantAwareCache 装饰器在每次 key 操作时自动注入租户前缀,业务层完全无感知:

csharp 复制代码
业务层调用 cache.get("admin")
      ↓
TenantAwareCache 拦截,生成 tenantKey = "1001:admin"
      ↓
实际操作底层 Caffeine:caffeineCache.get("1001:admin")

本地缓存模式下,所有租户数据都在同一个 JVM 内存中,需要通过 Key 前缀来隔离:

这一段代码只需要重点关注 3 件事:

  1. 如何根据 CacheDef 动态创建缓存
  2. 如何判断当前缓存是否需要租户隔离
  3. 如何在 key 层自动追加租户前缀
typescript 复制代码
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;
​
public class TenantAwareCaffeineCacheManager extends CaffeineCacheManager {
​
    private final CacheRegistry cacheRegistry;
    private final LanjiiCacheProperties properties;
​
    public TenantAwareCaffeineCacheManager(CacheRegistry cacheRegistry,
                                            LanjiiCacheProperties properties) {
        this.cacheRegistry = cacheRegistry;
        this.properties = properties;
        this.setAllowNullValues(properties.isCacheNullValues());
    }
​
    @Override
    public Cache getCache(String name) {
        Cache delegate = super.getCache(name);
​
        // 缓存不存在时,根据 CacheRegistry 或默认值动态创建
        if (delegate == null) {
            delegate = createAndRegisterCache(name);
        }
        if (delegate == null) {
            return null;
        }
​
        // 需要租户隔离的缓存,包装为 TenantAwareCache
        CacheDef cacheDef = cacheRegistry.get(name).orElse(null);
        if (cacheDef != null && cacheDef.isTenantIsolated()) {
            return new TenantAwareCache(delegate);
        }
        return delegate;
    }
​
    /** 动态创建并注册缓存 */
    private synchronized Cache createAndRegisterCache(String name) {
        Cache existing = super.getCache(name);
        if (existing != null) {
            return existing;  // 双重检查
        }
​
        CacheDef cacheDef = cacheRegistry.get(name).orElse(null);
​
        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 {
        private final Cache delegate;
​
        TenantAwareCache(Cache delegate) {
            this.delegate = delegate;
        }
​
        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); }
    }
}

这里使用了装饰器模式TenantAwareCache 包装了原始的 Cache,在每次操作时自动给 Key 加上 {tenantId}: 前缀,从而实现同一缓存空间内的多租户数据隔离。

⚠️ 注意 isTenantIsolatedCacheDef 中的可选项。对于字典数据、系统配置这类全局共享数据 (所有租户共用同一份),应设置为 false,跳过租户前缀包装,避免缓存被重复存储 N 份浪费内存。

多租户使用注意事项:

⚠️ 在使用多租户缓存隔离时,请务必注意以下几点:

  1. TenantContext 必须提前设置 :整套租户隔离机制依赖 TenantContext.getTenantId() 获取当前租户 ID。你需要在请求链路的入口处(通常是 Filter 或 Interceptor)设置好租户上下文,否则所有缓存操作都会落入 0: 默认命名空间,导致不同租户的数据混在一起。
  2. clear() 会清除所有租户的数据TenantAwareCache.clear() 调用的是底层 Caffeine 缓存的 clear(),会清空该缓存名下所有租户 的数据,而不仅仅是当前租户。如果只想清除当前租户的缓存,应该逐个 evict 已知的 key。
  3. 非多租户项目可跳过 :如果你的项目不涉及多租户,将所有 CacheDeftenantIsolated 设为 false 即可,框架会跳过租户前缀逻辑,零额外开销。

第七步:缓存辅助工具(CacheHelper)

CacheHelper 封装了 Spring Cache 接口未覆盖的扩展能力:

java 复制代码
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 模式可用)
     * Redis 模式需使用 SCAN 命令另行实现
     */
    @SuppressWarnings("unchecked")
    public <T> Collection<T> getAllValues(String cacheName, Class<T> type) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache == null) return Collections.emptyList();
​
        Object nativeCache = cache.getNativeCache();
        if (nativeCache instanceof com.github.benmanes.caffeine.cache.Cache) {
            var 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 -> (T) entry.getValue())
                        .collect(Collectors.toList());
            } else {
                return (Collection<T>) caffeineCache.asMap().values();
            }
        }
​
        throw new UnsupportedOperationException("getAllValues is only supported in LOCAL mode");
    }
​
    /** 清空指定缓存 */
    public void clearAll(String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) cache.clear();
    }
​
    /** 获取缓存实例 */
    public Cache getCache(String cacheName) {
        return cacheManager.getCache(cacheName);
    }
}

⚠️ getAllValues 方法在 Redis 模式下不可用。如果需要遍历 Redis 缓存数据,应使用 RedisTemplate 配合 SCAN 命令实现,避免使用 KEYS * 造成阻塞。

第八步:别忘了让 Spring Boot 识别这套缓存框架

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册自动配置类:

arduino 复制代码
com.lanjii.framework.cache.config.LanjiiCacheAutoConfiguration

Spring Boot 3.x 使用此文件替代了旧版的 spring.factories

业务模块接入

框架层搭建完成后,业务模块的接入非常简单,分为三步。

1. 声明缓存常量

ini 复制代码
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));
​
    /** 验证码缓存(租户隔离) */
    CacheDef CAPTCHA = CacheDef.of("captcha", Duration.ofMinutes(5));
}

注意 tenantIsolated 参数的使用:

  • false:如字典数据、系统配置,属于全局共享数据,不需要按租户隔离。
  • true(默认):如用户信息、验证码,每个租户的数据相互独立。

2. 启动时注册缓存

kotlin 复制代码
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. 在 Service 中使用缓存

以系统配置服务为例:

arduino 复制代码
@Service("sysConfigService")
@RequiredArgsConstructor
public class SysConfigServiceImpl extends BaseServiceImpl<SysConfigDao, SysConfig>
        implements SysConfigService {
​
    private final CacheManager cacheManager;
    private final CacheHelper cacheHelper;
​
    private Cache getConfigCache() {
        return cacheManager.getCache(SystemCacheConstants.SYS_CONFIG.getName());
    }
​
    /** 根据配置键获取配置(先查缓存,再查数据库) */
    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;
    }
​
    /** 更新配置时清除缓存 */
    @Override
    public void updateByIdNew(Long id, SysConfigDTO dto) {
        SysConfig originalConfig = getById(id);
        // ... 省略校验逻辑
        updateById(entity);
        getConfigCache().evict(originalConfig.getConfigKey());  // 清除旧缓存
    }
​
    /** 手动清空全部配置缓存 */
    @Override
    public void clearCache() {
        cacheHelper.clearAll(SystemCacheConstants.SYS_CONFIG.getName());
    }
}

可以看到,Service 层代码 只使用了 ​CacheManager 和 ​Cache 接口,完全不关心底层是 Caffeine 还是 Redis。切换缓存类型时,这里的代码无需做任何修改。

架构模式切换

这套方案的强大之处在于,你可以根据项目的实际部署需求,灵活选择架构模式,而无需改动代码。

模式一:轻量级单机模式 (Local)

适合单体应用私有化部署的小型环境对延迟极度敏感的场景。此时应用完全独立,不依赖任何外部缓存组件。

yaml 复制代码
lanjii:
  cache:
    type: LOCAL

模式二:分布式集群模式 (Redis)

适合微服务架构多实例部署需要数据持久化的场景。此时通过 Redis 实现多节点间的数据共享。

yaml 复制代码
lanjii:
  cache:
    type: REDIS

源码与在线体验

完整源码gitee.com/leven2018/l...

欢迎 Star ⭐ 和 Fork,项目包含本文涉及的所有代码(MCP 集成、多模型动态切换、RAG 知识库等)。

在线体验http://106.54.167.194/admin/index

总结

通过这套架构,我们成功实现了:

  1. 彻底解耦 :业务代码只认识 CacheManager,完全不知道底层是 Caffeine 还是 Redis。
  2. 一键切换:只需改一行配置,就能在单元测试(Local)和生产环境(Redis)之间无缝切换。
  3. 多租户安全:通过装饰器模式和 Key 前缀策略,在本地和分布式环境下都实现了透明的数据隔离。
相关推荐
tyung2 小时前
zhenyi-base 开源 | Go 高性能基础库:TCP 77万 QPS,无锁队列 16ns/op
后端·go
子兮曰2 小时前
Humanizer-zh 实战:把 AI 初稿改成“能发布”的技术文章
前端·javascript·后端
桦说编程2 小时前
你的函数什么颜色?—— 深入理解异步编程的本质问题(上)
后端·性能优化·编程语言
百度地图汽车版3 小时前
【AI地图 Tech说】第九期:让智能体拥有记忆——打造千人千面的小度想想
前端·后端
臣妾没空3 小时前
Elpis 全栈框架:从构建到发布的完整实践总结
前端·后端
喷火龙8号3 小时前
单 Token 认证方案的进阶优化:透明刷新机制
后端·架构
孟沐3 小时前
Java异常处理知识点整理(大白话版)
后端
ServBay3 小时前
告别面条代码,PSL 5.0 重构 PHP 性能与安全天花板
后端·php
孟沐4 小时前
Java 面向对象核心知识点(封装 / 继承 / 重写 / 多态)
后端