Skip to content

关于Spring的三级缓存

索引

Bean 工厂与循环依赖

在讨论三级缓存实现之前,我们先分析一个 Spring 中很经典的特点:依赖注入, 又被称为控制反转。这个特点使得当我们需要某个服务或者工具的时候, 可以通过注入来获取, 而非通过 new 进行新建。而由 Spring 所管理的实例, 基本都是单例

而依赖注入在种种好处之余则带来了一些经典问题, 比如循环依赖问题。 当服务 A 中存在变量服务 B, 而服务 B 中也存在服务 A 的时候, 循环依赖便有可能出现。当 A 服务中某个方法调用了 B 服务, 而 B 服务也对应的调用了 A 服务, 而由于 Spring 中的服务通常为单例模式, 因此对于服务 A 和服务 B 而言, 两者属于同一个实例, 因此方法会循环调用 A 和 B, 导致栈溢出。这就是触发了循环以来问题。

在解决循环依赖问题之前, 我们先思考一个问题:Spring是如何实现实例服务 A 和实例服务 B 这种嵌套的构造关系的?

二级缓存

一个思路:首先调用 A 的无参数构造器, 构造一个 A 的无参实例, 随后将该实例送入服务 B 来构建实例 B, 当实例 B 构建完成后, 再将实例 B 设置为实例 A 的参数。此时, 尽管 A 和 B 均需要对方作为自己的参数, 但是可以构建完成 A 和 B 的相互引用。

那么为了实现这个思路, 我们需要将服务实例化的部分拆为两个步骤:

  1. 构建毛坯房, 也就是通过无参构造器来构造一个无参数的实例类。
  2. 构建精装房, 对尚且没有参数的实例进行参数注入, 将对应的实例配置进去。

更进一步, 当第一次准备实例化服务 A 的时候, 我们需要先使用无参构造来构建一个实例 A, 并放入第一级缓存中, 随后再从第一级缓存中提取实例 A, 并对其设置对应的参数 B, 得到最终的实例 A, 放入最终的实例缓存中。

这时候, 我们其实已经构建了一个 二级缓存 来完成依赖注入。

但是这样也会导致循环依赖问题的出现, 而在服务存在无参构造的情况下, 且不做限制的情况下, 基本不存在破解循环依赖的方案, 因此 Spring 后续推荐使用参数构造器来实现注入, 这样可以将循环依赖的问题扼杀在摇篮。

另外, 循环依赖问题存在, 往往是由于代码架构、服务之间存在职责划分不清晰、服务链路混乱等问题导致的, 因此重新梳理代码架构逻辑, 并使用构造器注入来强制破解循环依赖是没有问题的。

面向AOP

二级缓存就可以完成依赖注入, 那么为什么还需要三级缓存呢?这就不得不提到 Spring 的另外一个特性:AOP, 又称面向切面编程。这个概念难以理解, 但举例时通常使用日志、安全管理等功能来举例, 当某个方法需要日志时, 我们固然可以在方法前后加上输入输出, 但是当所有的方法都需要日志时, 会造成霰弹式灾难, 因此 AOP 存在的主要原因是:解决不同方法中需要使用的相同基础功能而构建的。

这里可能需要进一步来讲解代理对象:简单理解的话, 代理对象是对原对象对一层包裹, 拥有和原始对象一样的输入和输出类型, 但是当参数进入时候, 会首先经过代理对象的操作, 当参数输出时, 会被代理对象进行后处理。

举例来说, 当我们要代理方法 int add(int x1, int x2)时, 代理的输入输出也都是int类型。而跟进一步, 代理操作是对输入进行自增, 对输出进行自减, 因此可以写成如下代码:

private int add(int x1, int x2){
  return x1 + x2;
}

public int proxyAdd(int x1, int x2){
    x1 += 1;
    x2 += 1;
    return add(x1, x2) - 2;
}

代码中proxyAdd拥有与原先对象一样的输出输出, 并完成了一定的操作, 是一个代理对象。当然在实际的应用中, 代理对象并非通过手动对每个函数操作进行实现的, 而是通过反射等方式进行自动代理。

代理的思路可以视为一种高级的抽象, 在不同的对象的外面包裹上一层统一的处理方法, 而不需要考虑这些对象之间存在什么差异。但是这给我们的二级缓存带来了一个关键问题:

  1. 当我们创建无参实例 A 的时候, 我们需要考虑实例 A 中的方法是否需要代理, 如果 A 需要代理, 那么我们在创建的时候就需要先考虑代理问题。

思考这一问题, 当我们创建无参实例 A 的时候, 我们可能不知道 A 是否需要代理, 但是我们应该明确当前手中有什么代理可以用。这样当 A 需要代理的时候, 我们才能将代理套在 A 的上面, 特别是当 A 需要不止一个代理的时候。这个, 就是第三级缓存

三级缓存

说来好玩, 虽然此时我称呼他为第三级缓存, 但是其实他的顺序应该是第一级, 因为当我们要构建无参数实例 A 的时候, 为了可以直接创建代理对象, 因此必须要先清楚代码中存在哪些代理, 所以在构造最终对象的两级缓存之前, 我们先要构造一个代理缓存, 才能进行下一步。

当构造完成所有的代理之后, 此时已经存在一个缓存用来存放代理对象, 然后我们再处理普通的服务 A, 为他构造对应的实例 A, 并套上代理, 随后再在 B 需要的时候, 将构造器来的实例 A 赋给对象 B, 再反过来将 B 赋予给 A。

因此总体来说, 三级缓存存在的意义, 并非为了解决循环依赖问题, 而且三级缓存也无法从根本上解决循环依赖。二级缓存就可以初步解决循环依赖的问题, 三级缓存的存在是为了解决代理对象的问题。

结语

另外需要注意的是, 本文所提出的三级缓存与 Spring 目前的三级缓存实现存在差异, 但是其核心思想是大体一致的, Spring 可能并没有一边扫描一边创建实例。在我印象中, Spring 应该是是将创建实例的方法存入缓存, 在整体结束后再进行创建实例的。

这篇文章写的其实很快, 整体可能不过30分钟, 没有太多图片和代码。所以请读者多多包涵。如果有哪里逻辑不够清晰, 请指出我改正。