你真的理解Spring三级缓存吗

23 阅读5分钟

Spring三级缓存:循环依赖的解决方案

在Spring框架中,循环依赖是Bean创建过程中常见的问题 针对这种问题 我们可以通过编码手段去解决

而Spring也为我们提供了一种解决手段 也就是三级缓存

本文将从缓存设计、解决逻辑、并发安全及场景限制等维度,详细解析三级缓存的工作原理。

一、三级缓存的核心设计目的

三级缓存是Spring为解决Bean循环依赖问题专门设计的缓存体系,其核心思路是通过“对象初始化延后”的方式,

在Bean未完全初始化时提前暴露引用,从而打破循环依赖的死锁。

二、三级缓存的定义与职责

Spring在DefaultSingletonBeanRegistry类中定义了三级缓存,各自承担不同职责:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
    
    // 一级缓存:保存**完全初始化完成**的单例Bean(最终可用状态)
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
    
    // 二级缓存:保存**半成品Bean**(实例化完成、未初始化的早期引用)
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
    
    // 三级缓存:保存Bean的创建工厂(用于生成早期Bean引用)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}
缓存层级名称核心作用数据类型
一级缓存singletonObjects存储完全初始化的成熟BeanMap<String, Object>
二级缓存earlySingletonObjects存储实例化完成的早期Bean引用Map<String, Object>
三级缓存singletonFactories存储Bean的创建工厂Map<String, ObjectFactory>

三、三级缓存解决循环依赖的核心逻辑

Spring将Bean的创建分为实例化(调用构造方法创建对象)和初始化(填充属性、执行初始化方法)两个阶段,三级缓存的核心作用是在“实例化后、初始化前”提前暴露Bean引用。

核心获取逻辑(getSingleton方法)

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 优先从一级缓存获取完全初始化的Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    
    // 2. 一级缓存无数据,且当前Bean正在创建中 → 尝试二级缓存
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        
        // 3. 二级缓存无数据,且允许提前引用 → 从三级缓存生成早期Bean
        if (singletonObject == null && allowEarlyReference) {
            // 加锁保证缓存操作的线程安全(统一锁一级缓存避免多锁死锁)
            synchronized (this.singletonObjects) {
                // 双重检查:再次确认一级/二级缓存(防止并发写入)
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 4. 从三级缓存获取Bean工厂,生成早期Bean引用
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            // 5. 将早期Bean存入二级缓存,同时移除三级缓存的工厂
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

循环依赖解决流程(以A←→B循环依赖为例)

  1. Spring创建Bean A,完成实例化后,将A的创建工厂存入三级缓存

  2. 初始化A时,发现需要注入Bean B,触发B的创建流程;

  3. Spring创建Bean B,完成实例化后,将B的创建工厂存入三级缓存

  4. 初始化B时,发现需要注入Bean A,调用getSingleton(A)

    1. 一级缓存无A(未初始化完成),二级缓存无A;
    2. 从三级缓存获取A的工厂,生成A的早期引用,存入二级缓存
    3. 将A的早期引用返回给B,B完成初始化,存入一级缓存
  5. A拿到B的成熟引用(一级缓存),完成自身初始化,存入一级缓存

  6. 最终A、B均存入一级缓存,循环依赖问题解决。

四、缓存操作加锁的原因

缓存操作时对singletonObjects(一级缓存)加锁,核心目的是:

  1. 保证 缓存一致性:防止多线程并发操作缓存时,出现“读取到不完整数据”“重复创建Bean”等一致性问题;

  2. 避免 死锁 风险:统一锁定一级缓存,而非为三级缓存分别加锁,减少多锁嵌套导致的死锁概率;

  3. 双重检查保障:加锁后的“双重检查”逻辑,能有效防止并发场景下的缓存穿透。

那么 三级缓存一定能够解决循环依赖吗 答案是不能完全解决

在构造器注入产生的循环依赖 则需要采用其他方法

五、三级缓存的场景限制:无法解决构造器注入循环依赖

三级缓存仅能解决字段注入/setter注入的循环依赖,无法解决构造器注入的循环依赖,原因如下:

  1. Spring Bean的创建流程中,构造器注入发生在实例化阶段(调用构造方法时),此时Bean尚未完成实例化,无法生成早期引用;
  2. 三级缓存的核心逻辑是“实例化后、初始化前”提前暴露引用,而构造器注入的循环依赖发生在实例化阶段,缓存机制尚未介入。

构造器注入循环依赖的解决方案

通过Spring提供的@Lazy注解延迟构造器注入的Bean初始化:

  • @Lazy会让Spring在构造器注入时,返回一个Bean的代理对象(而非真实对象);
  • 真实对象的创建会延迟到首次使用时,从而打破构造器阶段的循环依赖。

示例:

@Component
public class A {
    // 构造器注入B时添加@Lazy,延迟B的初始化
    public A(@Lazy B b) {
        this.b = b;
    }
}

@Component
public class B {
    public B(A a) {
        this.a = a;
    }
}

总结

  1. 三级缓存的核心是通过“实例化后提前暴露早期引用”,解决字段/setter注入的循环依赖;
  2. 缓存操作加锁是为了保证并发安全,统一锁一级缓存可避免死锁;
  3. 三级缓存无法解决构造器注入循环依赖,需通过@Lazy注解延迟初始化解决。