Spring Boot缓存新玩法:一键切换,租户无忧

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 接口 ,无需关心多租户隔离的具体实现细节 ,大大提高了开发效率和代码的可维护性 。

相关推荐
想你的液宝1 小时前
Spring Boot @RestControllerAdvice:统一异常处理的利器
后端
大傻^1 小时前
Spring AI Alibaba 企业级实战:从0到1构建智能客服系统
java·人工智能·后端·spring·springaialibaba
短剑重铸之日1 小时前
《ShardingSphere解读》11 解析引擎:SQL 解析流程应该包括哪些核心阶段?(上)
java·后端·spring·shardingsphere·分库分表
MekoLi292 小时前
MongoDB 新手完全指南:从入门到精通的实战手册
数据库·后端
会算数的⑨2 小时前
演进——从查日志到 AI 自治,企业监控体系的变迁
人工智能·分布式·后端·微服务·云原生
MekoLi292 小时前
ClickHouse 深度掌握与最佳实践指南
后端·架构
Leo8992 小时前
go从零单排之defer
后端
凛訫訫2 小时前
Java基础--面向对象高级(1)
后端
MekoLi292 小时前
ClickHouse 新手完全指南:从入门到架构师的最佳实践
后端·架构