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
相关推荐
超捻1 小时前
04 python 数据类型转换
后端
IT_陈寒1 小时前
Python开发者都在偷偷用的5个高效技巧,你竟然还不知道?
前端·人工智能·后端
kevinzeng1 小时前
mysql和redis数据一致性的策略
后端
小码哥_常1 小时前
一文搞懂双Token、SSO与第三方权限打通,附实战代码
后端
SimonKing1 小时前
5分钟学会!把代码从本地推送到 GitHub,就是这么简单
java·后端·程序员
灵境空间1 小时前
企业微信 AI 机器人 PHP SDK —— 免回调地址,三行代码接入,支持流式回复
后端
陈随易1 小时前
Vite 8正式发布,内置devtool,Wasm SSR 支持
前端·后端·程序员
CodeSheep2 小时前
首个OpenClaw龙虾大模型排行榜来了,国产AI霸榜了!
前端·后端·程序员
Moment2 小时前
想转 AI 全栈?这些 Agent 开发面试题你能答出来吗
前端·后端·面试