Spring Boot缓存新玩法:一键切换,租户无忧
开篇:痛点引出
在咱们日常的 Spring Boot 项目开发里,缓存可是性能优化的关键一环。就拿之前我参与的一个电商项目来说,为了降低数据库压力、提升接口响应速度,引入了缓存机制。起初,我们直接注入 RedisTemplate 来操作缓存 ,代码写起来倒也简单直接:
java
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object getProductFromCache(String productId) {
return redisTemplate.opsForValue().get("product:" + productId);
}
public void setProductToCache(String productId, Object product) {
redisTemplate.opsForValue().set("product:" + productId, product);
}
但随着项目推进,问题逐渐暴露出来。业务发展迅速,流量激增,对缓存的性能和扩展性提出了更高要求。我们考虑引入本地缓存 Caffeine 来应对高并发场景下的读请求,同时减少网络开销。可当着手切换缓存架构时,才发现直接依赖 RedisTemplate 带来了大麻烦。业务代码里到处都是和 Redis 相关的操作,从缓存的读写到序列化方式,都被紧紧绑在了 Redis 上。要换成 Caffeine,几乎每个涉及缓存的方法都得大改,牵一发而动全身,代码改动量巨大,测试成本也直线上升 ,还伴随着很高的风险,稍有不慎就可能引入新的 Bug。
这还没完,项目后来拓展到支持多租户模式,不同租户的数据需要严格隔离。原本简单的缓存 Key 设计完全不够用了,要是继续用之前的方式,不同租户的数据很可能会相互干扰,导致数据泄露或业务逻辑出错。多租户场景下的数据隔离问题,在最初设计缓存架构时压根没考虑到,这下又成了一个棘手的难题 ,让整个缓存架构的改造雪上加霜。
相信不少小伙伴在开发中也遇到过类似的困境,那有没有一种优雅的解决方案,既能轻松切换缓存类型,又能在多租户场景下实现透明的数据隔离呢?别着急,接下来就给大家详细介绍一套基于 Spring Cache 抽象与条件装配实现的可插拔缓存方案 ,让这些痛点迎刃而解。
Spring Cache 抽象层:核心概念揭秘
要实现这么强大的可插拔缓存方案,Spring Cache 抽象层可是重中之重。它就像是一座桥梁,把业务代码和具体的缓存实现隔离开来,让我们能轻松切换不同的缓存技术 ,还能在多租户场景下优雅地处理数据隔离问题。这里面有两个核心角色,CacheManager 和 Cache,它们的作用至关重要,下面就来深入剖析一下。
(一)CacheManager
CacheManager 作为缓存管理器,是整个缓存架构的大管家 。它的主要职责就是创建和管理 Cache 实例,就好比一个图书馆管理员,负责管理图书馆里的各个书架(Cache 实例)。在 Spring Cache 体系里,不同的缓存实现(比如 Caffeine 和 Redis)都有对应的 CacheManager 实现类。像 CaffeineCacheManager 负责管理 Caffeine 缓存实例,RedisCacheManager 则掌控着 Redis 缓存实例。当我们在 Spring Boot 项目里配置启用缓存时,Spring 会根据配置创建相应的 CacheManager 实例 ,并把它纳入 Spring 容器进行管理。
比如说,在配置文件里指定使用 Caffeine 缓存:
yaml
spring:
cache:
cache-names: userCache, productCache
cache-manager: caffeineCacheManager
caffeine:
spec: maximumSize=1000,expireAfterWrite=600s
Spring 就会依据这些配置创建 CaffeineCacheManager,然后按照设定的参数(这里是最大容量 1000,写入后 600 秒过期)创建 userCache 和 productCache 这两个 Caffeine 缓存实例 ,并交给 CaffeineCacheManager 来管理。CacheManager 还提供了一些方法,像 getCache (String name),通过缓存名称就能获取对应的 Cache 实例,方便我们后续对缓存进行操作。
(二)Cache
Cache 则是直接和缓存数据打交道的操作接口,它提供了一系列方法,比如 get、put、evict、clear 等 ,这些方法就像是操作书架上书的动作。get 方法用于从缓存中获取数据,put 方法把数据存入缓存,evict 用来移除指定数据,clear 则是清空整个缓存。举个例子,在业务代码里,我们可以这样使用 Cache 接口:
java
@Autowired
private CacheManager cacheManager;
public Object getProductFromCache(String productId) {
Cache cache = cacheManager.getCache("productCache");
Cache.ValueWrapper valueWrapper = cache.get(productId);
return valueWrapper != null ? valueWrapper.get() : null;
}
public void setProductToCache(String productId, Object product) {
Cache cache = cacheManager.getCache("productCache");
cache.put(productId, product);
}
这里先通过 CacheManager 获取名为 productCache 的 Cache 实例,然后用 get 和 put 方法进行数据的读取和写入。业务代码在操作缓存时,只依赖 CacheManager 和 Cache 这两个接口,不会直接引用 Caffeine 或 Redis 的具体实现类 ,这就为实现无缝切换缓存类型奠定了基础。不管底层是 Caffeine 还是 Redis,业务代码都不用改动,只要 CacheManager 和 Cache 接口的契约不变,就能轻松应对各种缓存技术的更替 ,这也是整个可插拔缓存方案的核心要点。
架构设计:一行配置的神奇魔法
有了 Spring Cache 抽象层的基础,接下来就看看这套可插拔缓存方案的整体架构设计 ,感受一下一行配置切换缓存的神奇之处。整个架构主要包含配置文件、缓存配置类、缓存注册表以及不同缓存实现的配置和初始化 ,它们相互协作,共同实现了缓存类型的无缝切换和多租户数据隔离。
先来看配置文件,这是整个架构的 "指挥中心" 。在 application.yml 里,我们定义了一个关键配置项 lanjii.cache.type,它就像是一个开关,决定了项目使用哪种缓存:
yaml
lanjii:
cache:
type: LOCAL # 可切换为REDIS
cacheNullValues: true
defaultTtl: 1h
defaultMaxSize: 1000
当 type 为 LOCAL 时,启用 Caffeine 本地缓存 ;改成 REDIS,就切换到 Redis 分布式缓存,简单直接。这里还配置了 cacheNullValues(是否允许缓存空值)、defaultTtl(默认过期时间)和 defaultMaxSize(默认最大容量,仅 Caffeine 有效) ,这些配置项可以根据项目需求灵活调整。
缓存配置类负责读取配置文件中的属性,并将其映射为 Java 对象 ,方便在代码中使用。通过 @ConfigurationProperties 注解,把 lanjii.cache 前缀下的配置映射到 LanjiiCacheProperties 类中:
java
@Data
@ConfigurationProperties(prefix = "lanjii.cache")
public class LanjiiCacheProperties {
private CacheType type = CacheType.LOCAL;
private boolean cacheNullValues = true;
private Duration defaultTtl = Duration.ofHours(1);
private long defaultMaxSize = 1000L;
public enum CacheType {
LOCAL,
REDIS
}
}
这样,在其他地方就可以通过注入 LanjiiCacheProperties 来获取这些配置信息 ,为后续创建不同的 CacheManager 做准备。
缓存注册表(CacheRegistry)则是缓存定义的中央仓库 ,它管理着所有缓存实例的元数据。每个缓存实例都有自己的名称、过期时间、最大容量以及是否需要租户隔离等属性 ,这些信息都封装在 CacheDef 类中。业务模块在启动时,会把自己的 CacheDef 注册到 CacheRegistry 里 ,示例代码如下:
java
public class CacheRegistry {
private final Map<String, CacheDef> cacheDefMap = new ConcurrentHashMap<>();
public void register(CacheDef cacheDef) {
cacheDefMap.put(cacheDef.getName(), cacheDef);
}
public Optional<CacheDef> get(String name) {
return Optional.ofNullable(cacheDefMap.get(name));
}
}
比如,某个业务模块需要一个名为 userCache 的缓存,过期时间为 30 分钟,不需要租户隔离,就可以这样注册:
java
CacheDef userCacheDef = CacheDef.of("userCache", Duration.ofMinutes(30), false);
cacheRegistry.register(userCacheDef);
在不同缓存实现的配置和初始化部分,通过 Spring 的条件装配(@Conditional)机制 ,根据 lanjii.cache.type 的值来创建对应的 CacheManager。当 type 为 LOCAL 时,创建 CaffeineCacheManager;为 REDIS 时,创建 RedisCacheManager。以 CaffeineCacheManager 的配置为例:
java
@Configuration
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "LOCAL")
public class CaffeineCacheConfig {
@Autowired
private LanjiiCacheProperties cacheProperties;
@Autowired
private CacheRegistry cacheRegistry;
@Bean
public CaffeineCacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheNullValues(cacheProperties.isCacheNullValues());
List<String> cacheNames = cacheRegistry.getAll().stream()
.map(CacheDef::getName)
.collect(Collectors.toList());
cacheManager.setCacheNames(cacheNames);
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.expireAfterWrite(cacheProperties.getDefaultTtl())
.maximumSize(cacheProperties.getDefaultMaxSize());
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
这里先从 LanjiiCacheProperties 获取通用配置 ,再从 CacheRegistry 获取所有缓存实例的名称,然后根据配置创建 Caffeine 实例并设置到 CaffeineCacheManager 中 。RedisCacheManager 的配置类似,只是需要配置 Redis 的连接信息等。
通过这样的架构设计,当我们需要在 Caffeine 和 Redis 之间切换时 ,只需要修改配置文件中的 lanjii.cache.type 值,Spring 会自动根据条件装配创建对应的 CacheManager ,业务代码完全不需要改动,真正实现了一行配置切换缓存,极大地提高了缓存架构的灵活性和可维护性 。配合下面这张架构图,大家能更直观地理解整个架构的运作流程:
从图中可以清晰看到,业务代码通过 CacheManager 和 Cache 接口操作缓存 ,而具体使用哪种缓存,由配置文件中的 lanjii.cache.type 决定,中间的各个组件协同工作,完成了缓存类型的切换和管理 ,是不是很巧妙呢?
实现步骤:步步为营搭建架构
(一)引入依赖
在缓存框架模块的 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>
这里面,spring-boot-starter-cache 是 Spring Cache 的核心依赖 ,它提供了缓存抽象层的基础功能和注解支持 ,像 @Cacheable、@CachePut、@CacheEvict 等注解,让我们能方便地在业务代码中使用缓存 。com.github.ben-manes.caffeine 就是 Caffeine 缓存库的依赖,引入它才能在项目里使用 Caffeine 本地缓存 。org.springframework.boot:spring-boot-starter-data-redis 则是连接和操作 Redis 的关键依赖 ,有了它,项目就能和 Redis 服务器进行交互,实现分布式缓存的功能 。
而 Jackson 相关的依赖也不可或缺,com.fasterxml.jackson.core:jackson-databind 是 Jackson 库的核心,负责对象的序列化和反序列化 ,把 Java 对象转换成 JSON 字符串存入缓存,或者从缓存中读取 JSON 字符串再转成 Java 对象 。com.fasterxml.jackson.datatype:jackson-datatype-jsr310 主要用于处理 Java 8 中的日期和时间类型(如 LocalDateTime、ZonedDateTime 等) ,保证这些类型在序列化和反序列化过程中的准确性 。虽然两个依赖同时存在于 classpath 中,但别担心,通过 Spring 的条件装配机制 ,在运行时只会激活其中一个 CacheManager,根据配置文件里的 lanjii.cache.type 来决定启用 CaffeineCacheManager 还是 RedisCacheManager 。
(二)定义缓存配置属性
通过 @ConfigurationProperties 注解,我们能轻松将 YAML 配置映射为 Java 对象,方便在代码中读取和使用配置信息 。在 com.lanjii.framework.cache.properties 包下创建 LanjiiCacheProperties 类:
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 定义了全局默认的过期时间 ,这里默认是 1 小时,支持 Spring 的 Duration 格式,像 1h(1 小时)、30m(30 分钟)、7d(7 天)等都能使用 ,可以根据业务需求灵活调整不同缓存实例的过期时间 。defaultMaxSize 则是 Caffeine 本地缓存的默认最大条目数 ,当缓存中的数据量超过这个值时,Caffeine 会按照 W-TinyLFU 算法淘汰一些数据 ,以保证缓存的性能 ,不过这个配置在 Redis 模式下是无效的 ,因为 Redis 是分布式缓存,容量由 Redis 服务器的配置决定 。通过这样的配置类,我们把配置文件里的信息和 Java 代码紧密联系起来,让整个缓存架构的配置变得灵活又易于管理 。
(三)定义缓存元数据(CacheDef)
每个缓存实例都有自己独特的属性,比如 TTL(过期时间)、最大容量、是否需要租户隔离等 ,为了方便管理这些属性,我们创建一个 CacheDef 类来封装它们 。在 com.lanjii.framework.cache.core 包下定义 CacheDef 类:
java
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 类里,name 字段代表缓存的名称,每个缓存实例都有一个唯一的名称,方便在代码中识别和操作 。ttl 字段指定了该缓存实例中数据的过期时间 ,一旦数据在缓存中存在的时间超过这个值,就会被自动淘汰 。maxSize 字段仅在 Local 模式(即 Caffeine 缓存)下有效 ,它设置了缓存的最大容量 ,当缓存中的数据量达到这个上限时,Caffeine 会根据自身的淘汰算法(如 W-TinyLFU)移除一些数据 ,以保证缓存的高效运行 。tenantIsolated 字段则决定了该缓存实例是否需要进行租户隔离 ,在多租户场景下,如果设为 true,不同租户的数据会在缓存中严格隔离,避免数据相互干扰 。这里采用静态工厂方法来创建 CacheDef 实例 ,而不是 Builder 模式,因为缓存定义通常是常量,参数固定 ,静态工厂方法更简洁直观 ,比如通过 CacheDef.of ("userCache", Duration.ofMinutes (30)) 就能快速创建一个名为 userCache,过期时间为 30 分钟,默认最大容量且需要租户隔离的缓存定义实例 ,大大提高了代码的可读性和开发效率 。
(四)缓存注册表(CacheRegistry)
CacheRegistry 就像是一个中央仓库,负责管理所有缓存定义 ,业务模块在启动时,会把自己的 CacheDef 注册到这里 。在 com.lanjii.framework.cache.core 包下创建 CacheRegistry 类:
java
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 Optional<CacheDef> get(String name) {
return Optional.ofNullable(cacheDefMap.get(name));
}
public Collection<CacheDef> getAll() {
return cacheDefMap.values();
}
}
在这个类中,cacheDefMap 是一个 ConcurrentHashMap,用于存储所有的缓存定义 ,它以缓存名称作为键,对应的 CacheDef 实例作为值 。ConcurrentHashMap 保证了线程安全 ,因为在项目启动过程中,多个业务模块可能会在 @PostConstruct 阶段并发注册它们的缓存定义 ,如果使用普通的 HashMap,可能会出现线程安全问题,导致数据不一致或程序出错 。register 方法用于将一个 CacheDef 实例注册到缓存注册表中 ,只要调用 register (cacheDef),就能把指定的缓存定义存入 cacheDefMap 。get 方法通过缓存名称来获取对应的 CacheDef 实例 ,返回一个 Optional 类型,这样可以优雅地处理缓存定义不存在的情况 ,避免空指针异常 。getAll 方法则返回所有已注册的缓存定义集合 ,方便在需要的时候对所有缓存定义进行统一操作,比如在创建 CacheManager 时,获取所有缓存实例的名称,然后根据这些名称创建对应的缓存实例 。通过 CacheRegistry,我们实现了缓存定义的集中管理 ,让整个缓存架构的配置和维护更加清晰有序 。
多租户隔离:装饰器模式的巧妙运用
在多租户场景下,数据隔离至关重要,要是处理不好,不同租户的数据就可能相互混淆,引发严重的安全和业务问题 。这里,我们借助装饰器模式,在框架底层透明地实现多租户数据隔离 ,让业务开发人员完全不用操心 Key 的租户前缀问题,实现代码的简洁与高效。
(一)装饰器模式简介
装饰器模式是一种结构型设计模式,它允许向一个现有的对象添加新的功能 ,同时又不改变其结构。简单来说,就是把要扩展的功能封装在一个装饰器类里 ,然后通过组合的方式,将装饰器和原始对象关联起来 ,在运行时动态地为原始对象添加新功能 。这种模式比继承更灵活 ,因为继承是在编译时就确定了类的功能,而装饰器模式可以在运行时根据需要选择不同的装饰器 ,对对象进行不同的功能扩展 。就好比给手机贴膜、加保护壳,手机本身的功能不变 ,但通过这些 "装饰",增加了防刮、防摔等新功能 。
(二)实现多租户隔离的装饰器设计
在我们的缓存架构中,装饰器模式主要用于在缓存操作的关键方法(如 get 和 put)中 ,自动添加租户前缀到缓存 Key 上 。先定义一个抽象的 CacheDecorator 类,它实现 Cache 接口 ,并持有一个 Cache 实例的引用 ,代码如下:
java
public abstract class CacheDecorator implements Cache {
protected final Cache targetCache;
protected CacheDecorator(Cache targetCache) {
this.targetCache = targetCache;
}
@Override
public String getName() {
return targetCache.getName();
}
@Override
public Object getNativeCache() {
return targetCache.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
Object tenantAwareKey = wrapKeyWithTenant(key);
return targetCache.get(tenantAwareKey);
}
@Override
public <T> T get(Object key, Class<T> type) {
Object tenantAwareKey = wrapKeyWithTenant(key);
return targetCache.get(tenantAwareKey, type);
}
@Override
public void put(Object key, Object value) {
Object tenantAwareKey = wrapKeyWithTenant(key);
targetCache.put(tenantAwareKey, value);
}
@Override
public void evict(Object key) {
Object tenantAwareKey = wrapKeyWithTenant(key);
targetCache.evict(tenantAwareKey);
}
@Override
public void clear() {
targetCache.clear();
}
protected abstract Object wrapKeyWithTenant(Object key);
}
在这个抽象类里,除了 getName 和 getNativeCache 方法直接调用目标 Cache 的对应方法外 ,其他涉及缓存操作的方法(如 get、put、evict) ,都先调用 wrapKeyWithTenant 方法 ,给传入的 Key 加上租户前缀 ,然后再调用目标 Cache 的方法 ,进行实际的缓存操作 。wrapKeyWithTenant 是一个抽象方法 ,具体的实现由子类来完成 。
接下来,看看具体的 TenantAwareCacheDecorator 类 ,它继承自 CacheDecorator ,实现了多租户隔离的关键逻辑 :
java
public class TenantAwareCacheDecorator extends CacheDecorator {
private final TenantContext tenantContext;
public TenantAwareCacheDecorator(Cache targetCache, TenantContext tenantContext) {
super(targetCache);
this.tenantContext = tenantContext;
}
@Override
protected Object wrapKeyWithTenant(Object key) {
Long tenantId = tenantContext.getTenantId();
if (tenantId == null) {
throw new IllegalStateException("Tenant ID is not set in TenantContext");
}
return tenantId + ":" + key;
}
}
TenantAwareCacheDecorator 类的构造函数接受一个目标 Cache 和一个 TenantContext 实例 。TenantContext 是一个用于存储当前租户 ID 的上下文对象 ,通过 ThreadLocal 在请求线程内传递当前租户 ID ,确保在整个请求处理过程中,都能获取到正确的租户 ID 。在 wrapKeyWithTenant 方法中 ,从 TenantContext 获取租户 ID ,如果租户 ID 为空,抛出异常 ,表示当前请求没有设置租户 ID ,不满足多租户隔离的条件 ;如果租户 ID 存在,将租户 ID 和原始 Key 拼接起来 ,中间用冒号分隔 ,作为新的缓存 Key ,这样不同租户的数据就会存储在不同的 Key 下 ,实现了数据隔离 。
(三)装饰器的装配与使用
在 Spring 的配置类中,通过 @Bean 注解和条件装配 ,将 TenantAwareCacheDecorator 装配到 Spring 容器中 ,并应用到需要多租户隔离的 Cache 实例上 。以 Caffeine 缓存为例,配置如下:
java
@Configuration
@ConditionalOnProperty(name = "lanjii.cache.type", havingValue = "LOCAL")
public class CaffeineCacheConfig {
@Autowired
private LanjiiCacheProperties cacheProperties;
@Autowired
private CacheRegistry cacheRegistry;
@Autowired
private TenantContext tenantContext;
@Bean
public CaffeineCacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheNullValues(cacheProperties.isCacheNullValues());
List<String> cacheNames = cacheRegistry.getAll().stream()
.map(CacheDef::getName)
.collect(Collectors.toList());
cacheManager.setCacheNames(cacheNames);
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.expireAfterWrite(cacheProperties.getDefaultTtl())
.maximumSize(cacheProperties.getDefaultMaxSize());
cacheManager.setCaffeine(caffeine);
// 使用装饰器装饰缓存
Map<String, Cache> cacheMap = cacheManager.getCacheMap();
cacheMap.forEach((name, cache) -> {
CacheDef cacheDef = cacheRegistry.get(name).orElseThrow(() -> new IllegalArgumentException("CacheDef not found for cache: " + name));
if (cacheDef.isTenantIsolated()) {
Cache decoratedCache = new TenantAwareCacheDecorator(cache, tenantContext);
cacheMap.put(name, decoratedCache);
}
});
return cacheManager;
}
}
在这个配置中,先创建了一个普通的 CaffeineCacheManager ,并设置了一些基本属性 ,如是否允许缓存空值、缓存实例名称以及 Caffeine 的配置参数 。然后遍历缓存注册表中的所有缓存定义 ,对于需要租户隔离的缓存实例 ,创建 TenantAwareCacheDecorator 装饰器 ,将原有的 Cache 实例包装起来 ,并替换掉原来的 Cache 实例 ,这样在后续的缓存操作中 ,就会自动应用多租户隔离的逻辑 。
通过下面这张流程图,能更清晰地理解装饰器模式在多租户隔离中的工作流程 :
从流程图可以看到,当业务代码调用 Cache 的 get 或 put 方法时 ,实际调用的是 TenantAwareCacheDecorator 的对应方法 ,它先获取租户 ID ,给 Key 加上租户前缀 ,再调用目标 Cache 的方法 ,完成缓存操作 ,整个过程对业务代码完全透明 ,业务开发人员只需要像平常一样使用 Cache 接口 ,无需关心多租户隔离的具体实现细节 ,大大提高了开发效率和代码的可维护性 。