命名缓存:多级个性化缓存配置的设计与实现
在业务系统日益复杂的背景下,不同类型缓存对容量、过期时间、键前缀等参数的要求各不相同。例如,用户信息缓存通常需要较长的 TTL,而验证码缓存只需几分钟即过期。传统的统一缓存配置已无法满足这种多样化的需求。因此,命名缓存(Named Cache)方案应运而生,它允许开发者通过配置文件为每个缓存名称独立定义策略,并同时支持 Redis 和 Caffeine 两种后端实现,从而显著提升缓存管理的灵活性与可维护性。
一、命名缓存的概念
命名缓存的核心思想是:在同一个缓存管理器内,依据缓存名称的不同,动态应用各异的缓存配置(例如 TTL、是否允许缓存空值、键前缀、初始容量等)。开发者仅需在配置文件中按 caches.<缓存名称> 格式声明参数,缓存管理器在创建对应缓存时便会自动采用这些个性化设定。
这种设计带来以下好处:
- 精细控制:针对不同业务场景精准调整缓存行为,避免“一刀切”策略。
- 配置友好:通过外部配置文件即可修改,无需改动代码,降低运维成本。
- 实现透明:开发时仍使用 Spring 标准的
@Cacheable注解,只需指定正确的缓存名称,底层自动适配。
小提示:命名缓存的设计精髓在于“默认配置 + 差异化覆盖”,开发者只需在配置文件中声明需要覆盖的字段,其余字段自动继承默认值,从而大幅减少配置冗余。
二、整体配置模型
2.1 配置属性类
项目将 NamedCacheProperties 定义为核心配置载体,其结构如下所示:
@Data
@ConfigurationProperties(prefix = "tutorials4j.cache.named")
public class NamedCacheProperties { private NamedCacheOptions defaults = new NamedCacheOptions(); private Map caches = new HashMap<>();
}
defaults:全局默认配置,所有缓存均继承此配置(除非被单独覆盖)。caches:键为缓存名称,值为对应缓存的特有配置。
NamedCacheOptions 定义了通用选项以及后端特有选项:
public class NamedCacheOptions { private Duration timeToLive; // 过期时间 private Boolean cacheNullValues;// 是否缓存null值 private Boolean enableStatistics; // 是否启用统计
private RedisOptions redis = new RedisOptions(); // Redis专属配置 private CaffeineOptions caffeine = new CaffeineOptions(); // Caffeine专属配置
}
其中 RedisOptions 包含 keyPrefix 键前缀;CaffeineOptions 则包含 initialCapacity、maximumSize、expireAfterAccess 等参数。
每个 NamedCacheOptions 均可通过调用 applyDefaults(NamedCacheOptions defaults) 方法,将未设置的属性从默认配置中合并,从而完美实现“默认配置 + 差异化覆盖”的模型。
2.2 配置示例
在 application.yml 中:
tutorials4j:cache: named:
defaults:
time-to-live: 60s
cache-null-values: false
redis:key-prefix: "default:"
caches:
userCache:time-to-live: 300scache-null-values: trueredis: key-prefix: "user:"
productCache:time-to-live: 600scaffeine: initial-capacity: 500 maximum-size: 20000
此时,userCache 会覆盖 TTL 和空值缓存策略,并使用自定义键前缀;productCache 则沿用默认 TTL,但修改了 Caffeine 的容量参数。
常见问题:如果配置文件中未定义某个缓存名称,是否会使用默认配置?答案是肯定的,因为默认配置适用于所有缓存,除非在 caches 项下单独定义,否则将自动采用 defaults 中的设置。
三、Redis 命名缓存的实现
3.1 缓存管理器创建器
RedisCacheManagerCreator 负责创建 RedisCacheManager 实例,并通过双重检查锁单例模式确保同一配置下只有一个管理器。其 newInstance() 方法用于构建 RedisCacheManager.Builder 链。
public RedisCacheManager newInstance() { RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig(); defaultConfig = RedisUtils.fillConfiguration(defaultConfig, properties.getDefaults()); RedisCacheManager.RedisCacheManagerBuilder builder =
RedisCacheManager.builder(factory).cacheDefaults(defaultConfig); // 应用所有 RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer.forEach(c -> c.customize(builder)); // 应用所有 CacheManagerCustomizer RedisCacheManager redisCacheManager = builder.build(); cacheManagerCustomizer.forEach(c -> c.customize(redisCacheManager)); return redisCacheManager;
}
3.2 命名缓存配置的注入
关键实现是 NamedRedisCacheManagerBuilderCustomizer,它实现了 RedisCacheManagerBuilderCustomizer 接口,会在 builder 构建前被调用。该类遍历 properties.getCaches(),为每个命名缓存创建独立的 RedisCacheConfiguration,然后通过 builder.withInitialCacheConfigurations(configMap) 一次性注入所有配置。
@Override
public void customize(RedisCacheManager.RedisCacheManagerBuilder builder) { RedisCacheConfiguration defaultConfig = builder.cacheDefaults(); Map configMap = new HashMap<>(); properties.getCaches().forEach((name, options) -> {
RedisCacheConfiguration namedConfig = RedisUtils.fillConfiguration(defaultConfig, options);
configMap.put(name, namedConfig); }); builder.withInitialCacheConfigurations(configMap);
}
RedisUtils.fillConfiguration 方法依据 NamedCacheOptions 中的 timeToLive、cacheNullValues、keyPrefix(通过 RedisOptions)修改默认配置。这样一来,当应用首次操作 userCache 时,RedisCacheManager 便会自动采用该缓存专属的配置。
3.3 自动装配
CacheRedisConfiguration 中条件化地注册了上述定制器和创建器:
@Bean(CacheManagerCreatorCategory.REDIS_CREATOR)
@ConditionalOnMissingBean
RedisCacheManagerCreator redisCacheManagerCreator(...) { ... }
此外,还注册了 JSON 序列化定制器等(与命名缓存主题无关的内容此处略过)。
小提示:RedisCacheManagerBuilderCustomizer 是一个强大的扩展点,如果需要自定义序列化方式或连接池,也可以通过实现该接口来注入自定义配置。
四、Caffeine 命名缓存的实现
Caffeine 作为进程内缓存,其命名缓存实现思路与 Redis 类似,但需要直接控制 CaffeineCacheManager 的缓存创建逻辑。
4.1 自定义缓存管理器
FlexibleCaffeineCacheManager 继承自 CaffeineCacheManager,重写了 createNativeCaffeineCache(String name) 方法:
@Override
protected Cache
关键步骤:
- 根据缓存名称查找是否存在专用配置。
- 调用
applyDefaults方法,将未设置的属性从全局默认值中补全。 - 创建一个新的
Caffeine构造器,并通过CaffeineUtils.copyOption设置过期时间、容量、访问过期等参数。 - 调用
build()方法生成原生Cache实例。
这样,每个命名缓存都将拥有独立的 Caffeine 对象,其大小、过期策略互不干扰。
4.2 缓存管理器创建器
CaffeineCacheManagerCreator 同样采用单例模式,其 newInstance() 方法直接创建 FlexibleCaffeineCacheManager,并注入全局 Caffeine 实例(该实例基于 properties.getDefaults() 构建,作为后备默认值)。
public CaffeineCacheManager newInstance() { FlexibleCaffeineCacheManager caffeineCacheManager = new FlexibleCaffeineCacheManager(properties); caffeineCacheManager.setCaffeine(caffeine); return caffeineCacheManager;
}
4.3 自动装配
CacheCaffeineConfiguration 负责创建默认 Caffeine Bean 和 CaffeineCacheManagerCreator Bean。同时,FlexibleCaffeineCacheManager 内部已处理命名缓存的配置,因此无需额外注册 CacheManagerCustomizer。
常见问题:如果多个缓存名称共享同一个 Caffeine 实例,如何保证隔离性?通过重写 createNativeCaffeineCache 方法,每个名称都会创建独立的 Cache 实例,从而实现完全隔离,互不影响。
五、使用示例
假设我们有一个业务服务,需要同时使用 Redis 缓存用户信息,使用 Caffeine 缓存产品目录:
@Service
public class UserService { @Cacheable(value = "userCache", key = "#userId") public User getUser(Long userId) { ... }
}
@Service
public class ProductService { @Cacheable(value = "productCache", key = "#productId") public Product getProduct(Long productId) { ... }
}
配置文件(application.yml)如下:
tutorials4j.cache.named:defaults: time-to-live: 60s cache-null-values: false redis:
key-prefix: "global:"caches: userCache:
time-to-live: 300s
cache-null-values: true
redis.key-prefix: "user:" productCache:
time-to-live: 600s
caffeine:
initial-capacity: 200
maximum-size: 10000
expire-after-access: 300s
运行时:
userCache操作 Redis 缓存:TTL 设置为 300 秒,允许缓存 null 值,键前缀为user:。productCache使用 Caffeine 缓存:最大容量 10000 条记录,初始容量 200,访问后 300 秒过期。
开发者只需关注 @Cacheable 注解中的缓存名称,底层管理器会自动应用对应的策略配置。
六、总结
命名缓存机制通过统一的配置模型将缓存名称与具体参数解耦,极大地提升了缓存管理的灵活性和可维护性。本文分别以 Redis 和 Caffeine 为例,展示了:
- 如何使用
NamedCacheProperties和NamedCacheOptions来描述差异化的缓存配置。 - 如何为 Redis 利用
RedisCacheManagerBuilderCustomizer注入不同的缓存配置。 - 如何为 Caffeine 通过重写
createNativeCaffeineCache方法为每个缓存名称创建独立实例。
这种设计可以轻松扩展至其他缓存提供者(如 EhCache),核心思想始终不变:让配置跟随缓存名称动态变化,而非全局僵化。在微服务或多业务模块的架构中,命名缓存是一项值得参考的基础设施实践。
