Spring三级缓存通俗易懂讲解
一、用盖房子的比喻理解三级缓存
想象一下我们要盖两栋房子:A房 和B房,但是有个特殊要求:
- A房需要B房的钥匙才能装修
- B房需要A房的钥匙才能装修
- 这就是循环依赖!
Spring的三级缓存就像是三个不同阶段的钥匙保管箱:
1. 一级缓存:成品房钥匙箱(singletonObjects)
- 作用 :存放完全装修好的房子的钥匙
- 特点:房子已经可以入住了,所有设施都齐全
- 比喻:房产证已经办好,可以随时交房
2. 二级缓存:半成品房钥匙箱(earlySingletonObjects)
- 作用 :存放正在装修的房子的临时钥匙
- 特点:房子框架已经搭好,但内部装修还没完成
- 比喻:房子主体结构完成,可以临时参观但不能入住
3. 三级缓存:施工队联系方式箱(singletonFactories)
- 作用 :存放施工队的联系方式,可以随时联系他们继续装修
- 特点:知道怎么把毛坯房变成精装修房
- 比喻:建筑公司的项目经理电话
二、具体盖房过程(循环依赖解决)
场景:A房和B房互相需要对方的钥匙
java
// A房需要B房的钥匙
@Service
public class HouseA {
private HouseB houseB; // 需要B房的钥匙
public void setHouseB(HouseB houseB) {
this.houseB = houseB;
}
}
// B房需要A房的钥匙
@Service
public class HouseB {
private HouseA houseA; // 需要A房的钥匙
public void setHouseA(HouseA houseA) {
this.houseA = houseA;
}
}
盖房步骤详解:
第1步:开始盖A房
- Spring说:"我要盖A房了!"
- 先把A房施工队联系方式 存到三级缓存(施工队联系方式箱)
- 现在A房还是个毛坯房,只有框架
java
// Spring内部操作:
三级缓存.put("houseA", () -> {
// 这个lambda就是施工队,知道怎么装修A房
return 装修A房();
});
第2步:A房装修需要B房钥匙
- A房施工队说:"我需要B房的钥匙才能继续装修A房"
- Spring去检查:B房盖好了吗?
第3步:开始盖B房
- Spring发现B房还没开始盖,于是:"我要盖B房了!"
- 把B房施工队联系方式 存到三级缓存
- B房现在也是毛坯房
第4步:B房装修需要A房钥匙
- B房施工队说:"我需要A房的钥匙才能装修B房"
- Spring去检查A房状态
第5步:关键操作 - 临时借用A房钥匙
- Spring发现A房在三级缓存有施工队联系方式
- 联系A房施工队:"先给我一个临时的A房钥匙"
- 施工队快速装修出一个临时可用的A房(早期引用)
- 把这个临时A房钥匙存到二级缓存(半成品房钥匙箱)
java
// Spring内部操作:
Object earlyA = 三级缓存.get("houseA").getObject(); // 调用施工队
二级缓存.put("houseA", earlyA); // 临时钥匙存到二级缓存
第6步:B房继续装修
- Spring把临时A房钥匙交给B房施工队
- B房施工队说:"好的,我有A房钥匙了,可以继续装修B房了"
- B房装修完成,钥匙存到一级缓存(成品房钥匙箱)
第7步:A房继续装修
- Spring说:"B房已经盖好了,这是B房的钥匙"
- A房施工队拿到B房钥匙,继续完成A房装修
- A房装修完成,钥匙也存到一级缓存
第8步:清理临时钥匙
- 从二级缓存和三级缓存中移除A房和B房的临时信息
- 现在一级缓存中有两把完整的钥匙:A房和B房
三、代码层面的具体流程
三级缓存在Spring中的实际代码
java
// Spring内部的三个缓存Map(简化版)
public class DefaultSingletonBeanRegistry {
// 一级缓存:成品Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期引用(半成品)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:Bean工厂(施工队)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
具体解决流程代码模拟
java
// 模拟Spring解决循环依赖的过程
public class SpringCacheDemo {
public static void main(String[] args) {
// 模拟三级缓存
Map<String, Object> 一级缓存 = new HashMap<>(); // 成品房
Map<String, Object> 二级缓存 = new HashMap<>(); // 半成品房
Map<String, Supplier<Object>> 三级缓存 = new HashMap<>(); // 施工队
System.out.println("=== 开始创建A房 ===");
// 第1步:A房开始施工,记录施工队
三级缓存.put("A房", () -> {
System.out.println(" A房施工队:我正在装修A房...");
Object a房实例 = new Object(); // 创建A房实例
System.out.println(" A房施工队:A房装修完成!");
return a房实例;
});
System.out.println(" A房施工队联系方式已记录到三级缓存");
System.out.println("\n=== A房需要B房钥匙 ===");
// 第2步:检查B房状态
if (!一级缓存.containsKey("B房")) {
System.out.println(" B房还没盖,先盖B房");
// 第3步:B房开始施工
三级缓存.put("B房", () -> {
System.out.println(" B房施工队:我正在装修B房...");
Object b房实例 = new Object();
System.out.println(" B房施工队:B房装修完成!");
return b房实例;
});
System.out.println(" B房施工队联系方式已记录到三级缓存");
System.out.println("\n=== B房需要A房钥匙 ===");
// 第4步:B房需要A房钥匙,检查A房状态
if (三级缓存.containsKey("A房")) {
System.out.println(" A房有施工队,先要个临时钥匙");
// 第5步:获取A房临时钥匙
Supplier<Object> a房施工队 = 三级缓存.get("A房");
Object 临时A房 = a房施工队.get(); // 快速装修临时A房
二级缓存.put("A房", 临时A房);
System.out.println(" A房临时钥匙已存到二级缓存");
// 第6步:B房继续施工
Supplier<Object> b房施工队 = 三级缓存.get("B房");
Object 成品B房 = b房施工队.get();
一级缓存.put("B房", 成品B房);
System.out.println(" B房成品钥匙已存到一级缓存");
// 第7步:A房继续施工(现在有B房钥匙了)
a房施工队 = 三级缓存.get("A房");
Object 成品A房 = a房施工队.get();
一级缓存.put("A房", 成品A房);
System.out.println(" A房成品钥匙已存到一级缓存");
// 第8步:清理缓存
二级缓存.remove("A房");
三级缓存.remove("A房");
三级缓存.remove("B房");
System.out.println(" 临时缓存已清理");
}
}
System.out.println("\n=== 最终结果 ===");
System.out.println("一级缓存(成品房):" + 一级缓存.keySet());
System.out.println("二级缓存(半成品):" + 二级缓存.keySet());
System.out.println("三级缓存(施工队):" + 三级缓存.keySet());
}
}
四、为什么需要三级缓存?
如果只有两级缓存会怎样?
假设我们只有一级缓存(成品房)和二级缓存(施工队):
- A房开始施工 → 施工队存到二级缓存
- A房需要B房钥匙
- B房开始施工 → 施工队存到二级缓存
- B房需要A房钥匙
- 从二级缓存找到A房施工队
- 问题:如果直接调用施工队,A房会被完整创建两次!
三级缓存的好处:
- 避免重复创建:通过二级缓存存放早期引用,确保同一个Bean只被创建一次
- 支持AOP代理:如果需要AOP代理,三级缓存可以确保代理对象的一致性
- 性能优化:减少不必要的对象创建和初始化
五、实际Spring源码中的关键方法
getSingleton() 方法(找钥匙)
java
// Spring源码简化版
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 第一级查找:成品房钥匙箱
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 第二级查找:半成品房钥匙箱
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 第三级查找:施工队联系方式箱
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 联系施工队,获取临时钥匙
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
addSingletonFactory() 方法(记录施工队)
java
// 记录施工队联系方式
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 存到三级缓存(施工队联系方式箱)
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
}
}
}
六、总结
记住这个简单的比喻:
- 一级缓存 = 成品房钥匙箱(可以入住的房子)
- 二级缓存 = 半成品房钥匙箱(正在装修的房子)
- 三级缓存 = 施工队联系方式箱(知道怎么装修房子的施工队)
当遇到循环依赖时,Spring通过临时借用半成品钥匙的方式,让两个互相依赖的房子都能顺利盖完!
这样理解是不是就清楚多了?