spring三层缓存

128 阅读9分钟

在我开发一款优惠券推送服务的时候,由于设计的层级过多,出现了bean循环依赖的问题,所以来了解一下如何解决spirng循环依赖,了解到spring三级缓存,接下来,我们一起了解一下吧

先从宏观上开始说我们三层缓存的概念,首先,我们三级缓存是用来存储我们的bean对象的,循环依赖是因为,当我们创建bean时,互相依赖,导致我们的bean对象创建无法注入依赖,所以spring为了解决这个问题提出了三级缓存的概念

首先

三级缓存都有哪三级?

一级缓存: [singletonObjects]单例池,存储已经创建完成并且已经完成属性注入的对象

二级缓存: [earlySingletonObjects] 提前暴露的对象,存放已经创建完成,但是还没有注入属性的对象

三级缓存: [singletonFa tories] 提前暴露的对象,存放已经创建完成,但是还没有注入好的对象工厂对象

(拓展:每个beanfactory都对应着一个BeanDefinition)

在 Spring 框架中,`BeanDefinition` 是一个用于描述 Spring 容器中的 Bean 实例的元数据接口。它定义了 Bean 的属性、依赖关系和其他配置信息,允许 Spring 容器了解如何实例化、配置和管理特定的 Bean。
​
`BeanDefinition` 包含了以下关键信息:
​
1. Bean 类的全限定名(Class)
2. Bean 的作用域(Scope)
3. 是否懒加载(Lazy Initialization)
4. 是否是抽象的(Abstract)
5. 构造函数参数(Constructor Arguments)
6. 属性值(Properties)
7. 依赖关系(Dependencies)
8. 初始化方法和销毁方法
9. 自动装配模式(Autowire Mode)
10. 是否是单例(Singleton)
​
Spring 容器使用 `BeanDefinition` 来创建和管理 Bean 实例。当容器启动时,它会解析配置文件或注解中的 Bean 配置信息,然后将这些信息封装成 `BeanDefinition` 对象。在需要使用 Bean 实例时,容器根据 `BeanDefinition` 中的配置信息来创建实例并进行相应的初始化。
​
总之,`BeanDefinition` 是 Spring 框架中用于定义和描述 Bean 的元数据接口,它起到了告诉 Spring 容器如何管理和创建 Bean 的作用。
可以简单的将`BeanDefinition`抽象的理解成bean

接下来我们开始梳理这三级缓存的使用流程

当我们创建bean对象时,会检测当前bean对象是否涉及到循环依赖的问题,如果不涉及,那么就会通过beanfactory直接创建,创建完成后,会直接存放到一级缓存中,如果涉及循环依赖的问题,那么,就会将创建中的Bean对象半成品先存入二级缓存中,并且将对应Bean的BeanFactory放入到三级缓存中,并且暂停对应bean的初始化,当我们 其他的Bean要依赖我们的这个Bean时,会去三级缓存的BeanFactory中获取到二级缓存中的Bean,然后进行注入,当二级缓存中的bean完全被初始化后,会删除二级三级缓存中的存储,将对象放入到一级缓存中,同时删除二级和三级缓存中的内容

在二级缓存中,我们存储的不仅仅是半成品还有代理,接下来让我们了解一下这两者的使用场景和区别

当我们的spring解决循环依赖时,会像上面说的流程走一遍,但是在实际场景中,我们的循环依赖会有简单的循环依赖(几个bean互相循环依赖),复杂的循环依赖(很多bean互相循环依赖),

接下来我们了解一下半成品和代理都是什么

半成品: 在解决循环依赖时,首先会创建一个对象,但是这个对象是创建不完全的,因为里面还有很多参数没有注入,spring将他放入到二级缓存中,等其他需要他的bean时,便可以从二级缓存中获取,虽然他是一个未完全初始化的对象,但是这不会影响其他bean对他的依赖。当半成品被注入时,spring会检测出这是一个半成品,会在后续需要它时继续完成对它的初始化创建过程

代理: 当循环依赖过多时,spring会使用代理来解决问题,代理对象在概念上是一个中间人,,它可以拦截对真实对象的调用,在循环依赖的场景下,spring可以创建代理对象来充当某个bean,用来满足其他Bean对它的依赖,同时避免了直接初始化这个Bean,代理对象会延迟实际Bean的初始化,直到它真正被需要

下面我们举一个实际使用场景的例子

假设有两个类 A 和 B,它们相互依赖,即 A 依赖于 B,B 也依赖于 A。

半成品示例

A 创建时,发现需要依赖 B,但 B 尚未初始化。

A 创建一个半成品(未完全初始化的版本),并将半成品放入二级缓存。

A 继续完成初始化过程。

B 创建时,发现需要依赖 A,但 A 已经在二级缓存中。

B 从二级缓存中获取 A 的半成品,并继续完成初始化过程。

代理示例

A 创建时,发现需要依赖 B,但 B 尚未初始化。

A 创建一个代理对象代替 B,并将代理对象放入二级缓存。

A 继续完成初始化过程。

B 创建时,发现需要依赖 A,但 A 已经在二级缓存中,是一个代理对象。

B 获取到 A 的代理对象,可以满足依赖关系。

当 B 需要调用 A 的方法时,代理对象会将调用委托给实际的 A。

总结:无论是半成品还是代理,在循环依赖的情况下,Spring 都会保证对象的初始化和依赖关系的正确性,以确保应用程序能够正常运行。

如果我上面说的这些还是无法让你明白什么时候使用半成品,什么时候使用代理对象,那么接下来我再详细的说一下。

使用半成品:

简单的循环依赖: 如果只以来了少数几个Bean,且依赖关系相对来说简单,那么Spring会选择使用半成品的方式。在 这种情况下,半成品是一个未完成初始化的Bean,它的属性值可能还没有被注入,但是这不会影响到其他Bean对它的依赖

使用代理:

复杂的循环依赖: 当循环依赖关系较为复杂,涉及多个 Bean 之间的相互依赖时,Spring 可能会选择使用代理来解决循环依赖。代理允许创建一个中间层,可以延迟实际 Bean 的初始化,从而绕过复杂的循环依赖问题。

解决多级循环依赖:有些情况下,可能存在多级的循环依赖,即 A 依赖于 B,B 依赖于 C,C 又依赖于 A。这种情况下,使用代理是更合适的选择,因为代理可以在需要时延迟实际 Bean 的初始化,以解决多级循环依赖的问题。

这个代理对象,我们spring会使用JDK动态代理或CGLIB来实现

但是,我有个疑问,那当循环依赖过于复杂,多级循环时,我就想用半成品来做可不可以,为什么不行呢?

当然,我们要是一定要使用半成品,是可以的,但是我们会遇到很多问题,我简单整理了一下。

  1. 复杂性增加: 循环依赖已经是一个复杂的问题,如果再引入半成品的概念,代码会更加难以理解和维护。半成品需要在创建过程中暂停,等待依赖的对象创建完成,这可能会引入更多的同步逻辑和状态管理,增加代码的复杂性。
  2. 同步问题: 在使用半成品的过程中,需要确保不会出现多线程并发访问的问题。因为对象的创建被暂停,可能会涉及到多个线程之间的等待和通信,如果同步逻辑不正确,容易引发死锁等问题。
  3. 维护困难: 半成品的概念可能会增加代码的复杂性,使得代码难以理解和维护。如果其他开发人员需要理解和修改这部分代码,可能会需要更多的时间和精力。
  4. 性能影响: 半成品的创建过程中需要暂停并等待依赖的对象创建完成,这可能会影响系统的性能。在并发情况下,半成品的等待可能会导致线程的阻塞,影响整体的系统吞吐量。
  5. 错误难以定位: 如果半成品的创建和依赖处理逻辑不正确,可能会引发难以预料的错误,而这些错误可能在不同的执行环境下表现出不同的问题,使得定位和解决问题变得更加困难。

尽管 Spring 使用三级缓存来处理循环依赖问题,但在实际的开发场景中,仍然可能会出现一些复杂的情况导致循环依赖问题。虽然 Spring 的三级缓存机制在很多情况下能够有效解决循环依赖,但并不是所有情况都能完全避免。

一些情况下可能会导致循环依赖问题:

  1. 多线程环境: 在多线程环境下,如果多个线程同时访问并创建相互依赖的 Bean,可能会出现循环依赖问题。
  2. 复杂依赖关系: 当 Bean 之间存在复杂的依赖关系,尤其是带有条件或动态依赖关系时,循环依赖问题可能会更复杂。
  3. 非单例 Bean: 如果涉及到原型(prototype)作用域的 Bean,因为每次获取都会创建新的实例,可能引发循环依赖问题。
  4. 自定义后置处理器: 如果自定义了 Bean 后置处理器,可能会影响 Spring 容器的加载顺序,从而导致循环依赖问题。

虽然 Spring 的三级缓存能够解决许多常见的循环依赖场景,但在特定情况下,仍然需要开发者注意和避免循环依赖的发生。优化代码结构、合理设计依赖关系,以及避免过于复杂的依赖链条,都可以帮助减少循环依赖的发生。