谈谈我自己理解的Spring为什么一定要用三级缓存
问题和我理解的结论
二级缓存能解决循环依赖吗?为什么一定要三级缓存?
能,为了在尽可能保持原有的bean创建流程不改变(Spring的bean创建规范)的前提下解决代理循环依赖
个人理解的三级缓存由来
【谈谈我自己理解的Spring为什么一定要用三级缓存】我们不顺着spring的代码说。先来看看bean的初始化流程有一个整体规范步骤,大致可以这么描述(其实还存在初始化前置、属性赋值前置等等步骤,但这里不需要关心所以不列出):
1、实例化
2、属性赋值
3、初始化
4、初始化完成后的后置操作,而AOP的代理包装对象就在这一步完成
遵循这一大前提步骤,如果我们自己来做这个bean创建流程,首先肯定要有一个容器,所以至少一层缓存是需要的,那我们就先用一层缓存试试,如果只有一个缓存容器,容器的本质就是一个缓存,我们简单称这个缓存为成品缓存,用来存放最终成品bean实例
场景一、A依赖B
1、A实例化
2、A属性赋值,发现需要B
3、B实例化,属性赋值,B初始化,B放入成品缓存
4、A拿到成品B,直接赋值
5、A初始化
6、A放入成品缓存
如上,简单的创建流程1个缓存可以处理
场景二、A依赖B,B依赖A
1、A实例化
2、A属性赋值,发现需要B
3、B实例化
4、B属性赋值,发现需要A,但从成品缓存没拿到A,因为这时候A还没创建完成不能放入成品缓存,导致B实例创建失败
如上,只有一个缓存,遇到循环依赖问题就没辙了
那怎么办?
这里有一个核心思想解决思路就是,提前暴露,搞两个缓存,
缓存变为:
- 一个存最终成品(成品缓存)
- 一个存半成品(半成品缓存)
这时候流程如下:
1、A实例化,并将实例化的A直接放入半成品缓存
2、A属性赋值,发现需要B
3、B实例化,并将实例化的B直接放入半成品缓存
4、B属性赋值,发现需要A,直接从成品缓存拿没拿到,再从半成品缓存拿,拿到了临时A
5、B初始化
6、B初始化完,执行后置操作,这里可以把成品B放入成品缓存,并将半成品缓存的B删除
7、A拿到完整B,直接赋值完成
8、A初始化
9、A初始化完,执行后置操作,成品A放入成品缓存,并将半成品缓存的A删除
如上,简单的循环依赖问题2个缓存可以处理
1、A实例化,并将实例化的A直接放入半成品缓存
2、A属性赋值,发现需要B
3、B实例化,并将实例化的B直接放入半成品缓存
4、B属性赋值,发现需要A,直接从成品缓存拿没拿到,再从半成品缓存拿,拿到了临时A
5、B初始化
6、B初始化完,执行后置操作,这里可以把成品B放入成品缓存,并将半成品缓存的B删除
7、A拿到完整B,直接赋值完成
8、A初始化
9、A初始化完,执行后置操作,因为没有已存在代理包装A1,将A包装成代理A1,并将成品A1放入成品缓存,删除半成品缓存的A
如上,如果用简单的在半成品缓存保存临时对象有一个很明显的问题,就是创建B的时候拿到的是A本体,但A需要被AOP代理,所以其实真正被外界引用的应该是最后的成品A1,这两者不是同一个对象。
那要怎么办?
这里有另一个核心解决思路就是,不直接暴露对象本身而是暴露获取这个对象的工厂,这样每次要用到的时候直接取取工厂,然后工厂判断暴露的对象是否需要代理包装,需要的话就返回代理包装,不需要则返回普通的对象
缓存变为:
- 一个存最终成品(成品缓存)
- 一个存获取半成品的工厂(工厂缓存)
这时候流程如下:
1、A实例化,并将可以获取实例的工厂A放入工厂缓存
2、A属性赋值,发现需要B
3、B实例化,并将可以获取实例的工厂B放入工厂缓存
4、B属性赋值,发现需要A,直接从成品缓存拿没拿到,再从工厂缓存拿,拿到了工厂A,工厂A返回时发现A需要被代理,所以返回代理A1
5、B初始化
6、B初始化完,执行后置操作,把成品B放入成品缓存,并将工厂缓存的B删除
7、A拿到完整B,直接赋值完成
8、A初始化
9、A初始化完,执行后置操作,因为任何一个缓存里都没有已存在的代理包装A1,将A包装成代理A2,并将成品A1放入成品缓存,删除工厂缓存的工厂A
如上,很明显,工厂虽然解决了B引用不到A代理对象的问题,但是依然无法解决A1和A2不是同一个对象的问题
这里就需要引入再次最后一个缓存,那就是用来存工厂创建出来的临时对象的缓存
缓存变为:
- 一个存最终成品(成品缓存)
- 一个存工厂创建出来的临时变量(半成品缓存)
- 一个存获取半成品的工厂(工厂缓存)
这时候流程如下:
1、A实例化,并将可以获取实例的工厂A放入工厂缓存
2、A属性赋值,发现需要B
3、B实例化,并将可以获取实例的工厂B放入工厂缓存
4、B属性赋值,发现需要A,从工厂缓存拿,工厂A返回时发现A需要被代理,则生成代理A1,并将A1放入半成品缓存
5、B初始化
6、B初始化完,执行后置操作,把成品B放入成品缓存,删除工厂缓存
7、A拿到完整B,直接赋值完成
8、A初始化
9、A初始化完,执行后置操作,发现半成品缓存已存在代理包装A1,所以不再重新包装代理,直接将A1放入成品缓存,并删除半成品缓存
至此,各种场景都得到了解决,但我一开始也说了,这一路下来的解决思路都是基于Spring创建bean的大流程前提顺着来的,如果不顺着这个前提流程,可不可以直接用2个缓存解决存在AOP的循环依赖问题呢?
能。
还是基于场景三
1、A实例化,然后直接生成代理对象A1,放入半成品缓存(这样即使后面有C也需要用A,拿到的永远是一个对象,没有因为工厂会创建多个对象的问题)
2、A属性赋值,发现需要B
3、B实例化,然后直接生成半成品,放入半成品缓存
4、B属性赋值,发现需要A,直接从半成品缓存拿到A1,赋值完成
5、B初始化
6、B初始化完,执行后置操作,将半成品缓存中的B拿到成品缓存,删除半成品缓存的B
7、A得到完整B,赋值完成
8、A初始化
9、A初始化完,执行后置操作,发现半成品缓存已存在代理包装A1,将半成品缓存中的A1放到成品缓存,删除半成品缓存
总结
至此我个人认为,二级缓存可以解决所有问题,但要做成二级缓存需要将代理增强这一步放到实例化之后立马做。Spring的整体流程规范是先创建对象,再做增强操作,需要代理的毕竟是少数,所以Spring这里不会为了少数情况改变整体流程,在保留大流程不变的情况下,他做了三级缓存(原因还是参照前面一步步遇到各种情况下的演化)
推荐阅读
- Spring认证中国教育管理中心-Spring Data Elasticsearch教程二
- 微服务注册中心
- Spring IoC
- (为什么在 Spring 的配置里,最好不要配置 xsd 文件的版本号)