Spring三级缓存通俗易懂讲解
Spring三级缓存通俗易懂讲解
一、用盖房子的比喻理解三级缓存
想象一下我们要盖两栋房子:A房和B房,但是有个特殊要求:
- A房需要B房的钥匙才能装修
- B房需要A房的钥匙才能装修
- 这就是循环依赖!
Spring的三级缓存就像是三个不同阶段的钥匙保管箱:
1. 一级缓存:成品房钥匙箱(singletonObjects)
- 作用:存放完全装修好的房子的钥匙
- 特点:房子已经可以入住了,所有设施都齐全
- 比喻:房产证已经办好,可以随时交房
2. 二级缓存:半成品房钥匙箱(earlySingletonObjects)
- 作用:存放正在装修的房子的临时钥匙
- 特点:房子框架已经搭好,但内部装修还没完成
- 比喻:房子主体结构完成,可以临时参观但不能入住
3. 三级缓存:施工队联系方式箱(singletonFactories)
- 作用:存放施工队的联系方式,可以随时联系他们继续装修
- 特点:知道怎么把毛坯房变成精装修房
- 比喻:建筑公司的项目经理电话
二、具体盖房过程(循环依赖解决)
场景:A房和B房互相需要对方的钥匙
// 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房还是个毛坯房,只有框架
// 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房钥匙存到二级缓存(半成品房钥匙箱)
// 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中的实际代码
// Spring内部的三个缓存Map(简化版)
public class DefaultSingletonBeanRegistry {// 一级缓存:成品Beanprivate 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);
}
具体解决流程代码模拟
// 模拟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() 方法(找钥匙)
// 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() 方法(记录施工队)
// 记录施工队联系方式
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通过临时借用半成品钥匙的方式,让两个互相依赖的房子都能顺利盖完!
这样理解是不是就清楚多了?
