找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

344

积分

0

好友

46

主题
发表于 前天 00:08 | 查看: 8| 回复: 0

后端开发中,你是否遇到过“缓存配置了却没生效”或“缓存空值导致穿透”的问题?其根本原因往往在于没有透彻理解 Spring CacheManager 的底层工作原理。本文将深入剖析 CacheManager 的核心源码,从缓存实例的创建到数据的存取,完整拆解其核心流程,帮助你从根本上解决缓存使用中的常见难题。

一、CacheManager:缓存实例的「大管家」

Spring Cache 的核心是 CacheManager 接口,它肩负着 管理缓存实例生命周期 的职责。我们最常用的 ConcurrentMapCacheManager 是其默认实现,让我们先从其 getCache 方法的源码入手:

// ConcurrentMapCacheManager.java
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
private boolean allowNullValues = true; // 默认允许缓存空值

@Override
public Cache getCache(String name) {
    // 1. 尝试从缓存Map中获取已存在的实例
    Cache cache = this.cacheMap.get(name);
    if (cache == null) {
        // 2. 使用双重检查锁定来创建新的缓存实例
        synchronized (this.cacheMap) {
            cache = this.cacheMap.get(name);
            if (cache == null) {
                // 3. 创建具体的ConcurrentMapCache实例
                cache = createConcurrentMapCache(name);
                this.cacheMap.put(name, cache);
            }
        }
    }
    return cache;
}

关键逻辑在于懒加载:只有在应用首次调用 getCache(name) 请求某个缓存时,才会真正创建并初始化对应的 ConcurrentMapCache 实例。这就解释了“为什么配置了缓存名称但从未使用,该缓存实例就不会被创建”的现象。

二、ConcurrentMapCache:内存缓存的「具体执行者」

ConcurrentMapCache 是 Spring 内置的内存缓存实现,其核心是 使用一个 ConcurrentMap 来存储数据,并妥善处理了空值、过期时间(默认不支持,需自行扩展)等细节。下面我们看它的构造方法及核心操作:

1. 实例创建:createConcurrentMapCache

ConcurrentMapCacheManager 中,createConcurrentMapCache 方法负责初始化缓存实例:

// ConcurrentMapCacheManager.java
protected Cache createConcurrentMapCache(String name) {
    // 传入缓存名称、底层存储容器以及是否允许空值的配置
    return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), this.allowNullValues);
}

这里的 ConcurrentHashMap 就是缓存数据的 底层存储容器,默认初始容量为256。

2. 数据存入:put方法的空值处理

当我们调用 cache.put(key, value) 存入数据时,ConcurrentMapCache 会对空值进行特殊处理,这是防止缓存穿透的关键:

// ConcurrentMapCache.java
private final ConcurrentMap<Object, Object> store;
private final boolean allowNullValues;
private static final Object NULL_HOLDER = new Object(); // 空值占位符

@Override
public void put(Object key, @Nullable Object value) {
    // 1. 处理空值:将用户传入的null转换为内部的NULL_HOLDER
    Object storeValue = toStoreValue(value);
    // 2. 将转换后的值存入底层的ConcurrentMap
    this.store.put(key, storeValue);
}

protected Object toStoreValue(@Nullable Object userValue) {
    if (userValue == null) {
        // 如果配置允许缓存空值,则用NULL_HOLDER占位符代替
        if (this.allowNullValues) {
            return NULL_HOLDER;
        }
        throw new IllegalArgumentException("Null value not allowed");
    }
    return userValue;
}

核心机制:允许缓存空值时,真实的 null 会被替换为 NULL_HOLDER 这个特殊的对象并存入 Map。这样即使数据库查询结果为空,这个“空结果”也会被缓存起来,避免了对数据库与中间件的频繁无效查询。

3. 数据获取:get方法的原值恢复

当调用 cache.get(key) 获取数据时,需要将内部存储的 NULL_HOLDER 还原为用户期望的 null

// ConcurrentMapCache.java
@Override
@Nullable
public ValueWrapper get(Object key) {
    // 1. 从底层存储中获取值(可能是NULL_HOLDER)
    Object storeValue = this.store.get(key);
    // 2. 转换为用户可见的格式(处理NULL_HOLDER的逆转换)
    return toValueWrapper(storeValue);
}

protected ValueWrapper toValueWrapper(@Nullable Object storeValue) {
    return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null);
}

protected Object fromStoreValue(@Nullable Object storeValue) {
    // 将内部的NULL_HOLDER占位符转换回null
    if (storeValue == NULL_HOLDER) {
        return null;
    }
    return storeValue;
}

这一步保证了调用方获取到的是 原始的业务对象值,无需感知底层的空值处理逻辑,实现了对应用层的透明化。

三、从创建到存取:缓存生命周期全流程

为了更直观地展示 CacheManager 及其组件如何协同工作,我们通过以下 UML 时序图来梳理从缓存实例创建到数据存取的全过程:

缓存全流程时序图 时序图清晰地揭示了四个关键步骤:

  1. 应用程序首次调用 getCache 方法获取指定名称的缓存实例。
  2. CacheManager 执行懒加载,创建并初始化 ConcurrentMapCache 及其底层存储。
  3. 存入数据(put)时,若值为空且允许,则进行 NULL_HOLDER 替换。
  4. 获取数据(get)时,执行逆向转换,将 NULL_HOLDER 恢复为 null

四、常见问题的源码级解答

结合源码,我们可以清晰地解释一些常见的缓存“坑”:

1. 为什么配置了 @Cacheable 但缓存没生效?

回顾 CacheManager.getCache 方法——它采用懒加载机制。如果你的 @Cacheable 注解中指定的 value(缓存名称)配置错误,或者对应的 CacheManager 没有正确扫描并配置该缓存名称,那么在第一次访问时就不会创建对应的缓存实例,导致注解失效。

2. 为什么缓存空值有时会导致问题?

关键在于 allowNullValues 属性:

  • 若设置为 false(默认为 true),尝试缓存 null 值会直接抛出 IllegalArgumentException
  • 若设置为 true,空值会被 NULL_HOLDER 替换并缓存,这确实能有效避免缓存穿透,但需要注意 NULL_HOLDER 对象本身会占用内存。如果系统中存在大量不同的键都对应空值,可能会造成一定的内存浪费。
3. 如何为内存缓存扩展过期时间功能?

标准的 ConcurrentMapCache 基于 ConcurrentHashMap,本身不支持过期时间。你可以通过继承 ConcurrentMapCache 并重写相关方法来实现,例如,将底层存储容器替换为支持过期特性的第三方 Map 实现(如 ExpiringMap),并在 put 方法中设置存活时间。

总结

Spring Cache 抽象的核心在于分层:CacheManager 作为“大管家”,负责缓存实例的创建与生命周期管理;而 ConcurrentMapCache 这样的具体实现则作为“执行者”,处理数据存储、空值转换等底层细节。深入理解这两个核心组件的源码与协作机制,能够帮助你诊断和解决绝大部分因配置或理解不到位而产生的缓存问题,从而在基于 Spring 框架的项目中更得心应手地使用缓存。




上一篇:Proxmox VE计算与存储分离架构实战:生产环境Ceph+RBD部署指南
下一篇:SpringBoot实战:ThreadLocal父子线程传值方案详解(含线程池场景)
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-7 01:37 , Processed in 0.071613 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表