跳至主要內容

Mybatis缓存

xw大约 6 分钟MybatisMybatis

概述

MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。

缓存实现类

image-20221031161347317

  • BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
  • FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
  • LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。
  • LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节读者可参考LruCache类的源码。
  • ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
  • SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
  • SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
  • SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
  • TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。

一级缓存

MyBatis的一级缓存是SqlSession级别的缓存,在介绍MyBatis核心组件时,有提到过SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。

一级缓存在 org.apache.ibatis.executor.BaseExecutor实现,该类有两个属性,localCache用户缓存Mybatis查询结果。localOutputParameterCache属性用于缓存存储过程调用结果。

  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

MyBatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象(CacheKey对象决定了缓存的Key与哪些因素有关系)。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // mapper id
    cacheKey.update(ms.getId());
    // 偏移量
    cacheKey.update(rowBounds.getOffset());
    // 条数
    cacheKey.update(rowBounds.getLimit());
  
    //SQL语句
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    // 所有参数值
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    // 环境ID
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

从上面的代码可以看出,缓存Key的生成与下面这些因素有关:

  • Mapper Id,mapper命名空间和<select|update|insert|delete>标签的Id组成的全局限定名
  • 查询偏移量及查询条数
  • SQL语句及参数
  • MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值。

执行查询时,只有信息完全相同时,才会认为两次查询执行的是相同的SQL语句,缓存才会生效。以query方法为例:

image-20221031163356317

二级缓存

MyBatis二级缓存在默认情况下是关闭的,因此需要通过设置cacheEnabled参数值为true来开启二级缓存。二级缓存功能由CachingExecutor实现。CachingExecutor中有TransactionalCacheManager对象存储二级缓存,如下图所示:

image-20221031164816914

TransactionalCacheManager中维护着一个map存储二级缓存。

image-20221031164858243

查看CachingExecutor#query方法,实现逻辑如下:

image-20221031165247438

通过isFlushCacheRequired参数判断是否清空缓存。

image-20221031165500034

小结

MyBatis一级缓存是SqlSession级别的缓存,默认就是开启的,而且无法关闭;二级缓存需要在MyBatis主配置文件中通过设置cacheEnabled参数值来开启。一般来说不推荐使用缓存机制。总结如下:

  1. MyBatis一级缓存的生命周期和SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
  4. MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  5. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  6. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

参考