MyBatis源码解读6(3.2、MyBatis的缓存设计)

362 阅读10分钟

3.2、MyBatis的缓存设计

3.2.1、Cache接口

​ MyBatis在设计缓存的时候将缓存设计成了接口,那么我们知道,一旦被设计成了接口,那么意味着就有多种实现,我们要看源码的话就去搜Cache即可。

image-20241116121641719

​ 我们要注意有很多同名的Cache接口,要选择是MyBatis包下的。我们点进去后会发现是一个接口。

image-20241116121754182

​ 我们按住atrl+7可以快速查看这个接口所定义的方法。

image-20241116122041916

​ 来看看对应的方法是干啥的,其中对于我们来说最重要的两个方法是

image-20241116122429338

​ 这个两个方法通俗一点讲就是往缓存中放或者拿数据,也就是缓存最为核心的的功能。我们一个一个方法来解析:

  1. Object putObject(Object key, Object value):这个方法的作用是往缓存里存放数据,我们可以注意到这个方法里有两个参数,一个是key一个是value,这个我们很好理解,我们把对象放进缓存里,我们在拿的时候需要通过一个唯一标识来知道我们拿的是哪个对象,而这个key就是一个唯一标识。
  2. Object getObject(Object key):这个方法就是通过唯一标识key去缓存里拿到缓存对象。
  3. Object removeObject(Object key):这个方法的作用是通过唯一标识key将缓存里的对应对象移除。
  4. void clear():如果我们想一次性清除缓存里的所有对象,我们就可以使用这个方法。
  5. int getSize():获取缓存中到底存放了多少数据(即存放了多少个元素)
  6. default ReadWriteLock getReadWriteLock():最后一个方法稍微复杂一点,这个方法的目的是为缓存提供一个读写锁,以便在多线程环境下安全地访问缓存(==在高版本的MyBatis里已经把这个方法废弃了==)
  7. String getId():这个方法比较简单,后续我们的每一个mapperstatment都会有一个Cache,我们区别不同的缓存区域就通过这个唯一标识Id来区分。

3.2.2、Cahce存储结构

​ 我们可以通过查看Cache接口的方法我们可以发现,其实Cache接口的存储结构很像Java里面的Map,通过key、value来存储,map也有get、put方法,所以Cache的存储结构就有点类似于键值对的结构。

3.2.3、Cache实现类详解

​ 通过上述我们知道,其实Cache是一个接口,但是学过Java的都知道,光有接口还不行,Java是不允许对接口进行实例化的,我们还得有实现。那我们可以自己来简单实现一下这些方法,看看我们写的和人家框架源码写的有什么区别,有什么可以值得借鉴的地方。我们先写一个类来实现Cache接口,来看看需要实现哪些方法。

image-20241228145318072

​ 我们可以看到,前面五个是必须要实现的,而最后一个获取读写锁的方法是可以不实现的。

3.2.3.1、Cache核心方法的Map实现

​ 我们接着来分析,我们存储据肯定不止存储一条数据,肯定是多条数据,所以我们存储数据的结构肯定是数组、List、Set、Map这种可以存储多条数据的结构,而不是简简单单的String(基本变量),然后我们存储数据还需要有key、vcalue键值对的形式,那么很明显以上的结构只有map符合条件。,所以我们要把数据都存储到map里面,所以我们一开始就要定义好一个成员变量,key的类型为Object,value的类型为Object,用于存储数据。

image-20241228152025865

​ 这样我们就定义好了一个自己的Cache,这些Cache的方法底层都可以交给map来做实现。这样几个核心的方法我们都可以实现完成。

image-20241228152345431

​ 我们发现其实还漏了一个getId方法,因为Id是唯一的,所以我们可以通过类名来作为这个全局的唯一标识。这个是类的全限定名,如果害怕重复,我们可以加时间戳等方法来防止重复。

image-20250102214754685

​ 那么会有人说,我还想实现一下那个废弃了的方法getReadWriteLock(),这个比较简单,直接new一个他的实现类即可。

image-20250102215221981

​ 那为什么不直接new一个ReadWriteLock对象呢?我们点进去看就明白了,其实ReadWriteLock对象是一个接口,并不是一个对象,所以可以直接new他的实现类即可。

image-20250102215310386

image-20250102215355783

3.2.3.2、Cache核心方法的其他实现

​ 我们都知道Cache是一个接口,他的实现就可以是多种多样的,我们前面采用Map来实现,那么还有没有其他实现方式呢?

我们来分析一下,前面知道了能作为Cache的实现的要求有两点:

  1. 能存多个数据
  2. 是key-value的形式来存储数据

​ 我们很想当然就可以想到Redis来存储,我们就可以用类似Redis的这种数据库也可以,像Java其他的开源组件的Cache也可以实现,我们以Redis为例。

​ 要实现Java操作Redis肯定要引入Redis相关的客户端的依赖,我们用Jedis。我们首先在pom文件中引入这个依赖。

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

引入了依赖,我们接下来要写一个Jedis操作类,便于我们来操作Redis,我们这里只返回一个Jedis。

public class RedisUtils {

    public static Jedis getJedis() {
        String ip = "127.0.0.1";
        int port = 6379;
        return new Jedis(ip, port);
    }
}

那接下来就简单多了,但是有一个小问题,那就是我们的set方法的入参都是String类型,这可不行,我们的Cache的入参都是Object,我们可以把Object都转为Json数组即可解决这个小问题。但是这种方法不推荐,更推荐使用Jedis的set的重载方法,即我们可以把他序列化成byte数组。

image-20250102222524059

如果想要用序列化的方式,我们还需要引入commons-lang3这个包。

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.14.0</version>
        </dependency>

我们可以用lang3的SerializationUtils.serialize方法来将一个Object对象序列化成一个数组。我们一个方法一个方法来实现。

    @Override
    public void putObject(Object key, Object value) {
        Jedis jedis = RedisUtils.getJedis();
        jedis.set(SerializationUtils.serialize(key), SerializationUtils.serialize(value));
    }

get方法其实就是从Redis中取数据,这里其实还要进行判空操作,如果Redis中的数据不为空,我们才拿出来反序列化,防止报异常。

    @Override
    public Object getObject(Object key) {
        if (key == null) {
            Jedis jedis = RedisUtils.getJedis();
            byte[] ketBytes = jedis.get(SerializationUtils.serialize(key));
            return ketBytes == null ? SerializationUtils.deserialize(ketBytes) : null;
        } else {
            return null;
        }
    }

3.2.4、MyBatis对Cache实现

3.2.4.1、PerpetualCache

我们自己写了一下实现之后,那么MyBatis是如何实现的呢?我们去看一下Cache的接口可以发现他有这么多个实现类。

image-20250204151141157

我们可以看到,还是有我们自己写的那两个简单的实现类,撇去这两个不看的话,其他都是MyBatis原生为我们支持的实现类。我们自己一点可以发现,他其实主要是可以分两类,以包名来进行一个区分,其中大部分都是在org.apache.ibatis.cache.decorators这个包下,唯独只有一个是在 org.apache.ibatis.cache.impl这个包下。

image-20250204151237983

其中decorators是装饰器的意思,他本质上是23种设计模式中的一种装饰者设计模式,装饰器的作用就是为他的目标去增加一些额外的功能的,除了这些装饰器之外(非核心),只有一个是真正核心的实现类,那就是在org.apache.ibatis.cache.impl这个包下的PerpetualCache这个类。所以我们接下来去看一下PerpetualCache类的实现。我们来看看他的核心方法。

image-20250204151957706

我们可以看到,他这些核心方法的实现都是基于一个cache对象去实现的,而cache对象也正和我们实现的相类似,是一个HashMap对象。

除了以上这些核心方法他还有额外重写了equals和hashCode方法,可谓是很全面了。

image-20250204152145062

看完了核心的,我们来看看装饰器到底是如何让PerpetualCache更强大了呢?我们同样也是分组来看。

3.2.4.2、FifoCache和LruCache

FifoCache和LruCache增强了PerpetualCache的换出功能,我们知道缓存其实本质上是用内存来存储数据,我们不可能无限制的使用内存,内存永远会有满的那天,所以就必须要有换出算法,其实这里也就对应着常见的两种换出算法:FIFO(先入先出)和LRU(最少使用),其中LruCache为默认的装饰器,意味着换出算法默认就是LRU。

那么MyBatis是如何进行增强的呢?我们可以写一段测试代码来看看

    public static void main(String[] args) {
        PerpetualCache perpetualCache = new PerpetualCache("Lin");

        // 为perpetualCache增强LRU换出算法功能
        LruCache lruCache = new LruCache(perpetualCache);

        // 为lruCache增强日志功能
        LoggingCache loggingCache = new LoggingCache(lruCache);
    }

我们可以看到,我们可以用套娃的写法一层一层往上套,不断为perpetualCache增强新的功能,

3.2.4.3、LoggingCache

刚刚我们也用到了LoggingCache,我们接下来就看看这个LoggingCache,他是为PerpetualCache增强了日志功能。

3.2.4.4、BlockingCache

BlockingCache为PerpetualCache增强了阻塞的功能,使得同一时间只有一个现成拿着key去缓存中找数据,实际上他就帮我们解决了并发访问的问题。

3.2.4.5、ScheduledCache

ScheduledCache可以帮我们自动刷新缓存,当上次清空的时间大于指定的时间的时候,他就会帮我们刷新一次缓存,意味着ScheduledCache我们可以设置时间间隔,有了ScheduledCache的存在就可以尽可能的避免脏数据的产生。

3.2.4.6、SerializedCache

SerializedCache可以帮我们自动完成key、value的序列化和反序列化的操作,他的作用其实就有点类似于我们之前写的这行代码:

SerializationUtils.serialize(key)

以及反序列化的代码:

SerializationUtils.deserialize(ketBytes)

3.2.4.7、SoftCache和WeakCache

SoftCache为PerpetualCache增强软引用的功能,它使用软引用(Soft Reference)来存储缓存项,以便在内存不足时,这些缓存项可以被垃圾回收器回收。

WeakCache为PerpetualCache增强弱引用的功能,它使用弱引用(Weak Reference)来存储缓存项,以便在内存不足时,这些缓存项可以被垃圾回收器回收。

3.2.4.8、SynchronizedCache

SynchronizedCache为PerpetualCache增强同步的功能,它使用同步机制来确保对缓存的访问是线程安全的。

3.2.4.9、TransactionalCache

SynchronizedCache为PerpetualCache增强事务的功能,当我们只有在事务操作成功的时候才会把对应的数据放置在缓存中,换句话说,如果事务操作失败了,或者是没有事务的话,他是绝对不对去操作缓存的。

3.2.4.10、设计模式的区分

我们知道,MyBatis中大量使用了装饰器设计模式,目的是为目标拓展额外的功能,但是我们可以想到一个设计模式——代理设计模式,代理设计模式也是为目标(原始对象)增加额外的功能,也可以看成代理设计模式也是为目标拓展额外功能,从概念上来讲,代理设计模式和装饰器设计模式是很类似的,而且他们的类图也都是一样的,那么我们到底如何选择呢?或者说如何做区分呢?

装饰器拓展的功能是他的本职功能,也就是他的核心功能,而代理设计模式为原始对象增强的是额外功能,不属于本职核心功能,属于锦上添花,比如说他做的是Cache功能,那么装饰器装饰的就只是Cache的功能。简单来说就是:

  1. 装饰器增加的是核心功能,他和被装饰的对象做的事同一件事
  2. 代理设计模式增加的是额外功能,和被代理的对象做的不是同一件事