3

吃透Mybatis源码-缓存的理解(三)

 2 years ago
source link: https://blog.csdn.net/u014494148/article/details/122313499
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

吃透Mybatis源码-缓存的理解(三)

来来来,给俏如来扎起。感谢老铁们对俏如来的支持,2021一路有你,2022我们继续加油!你的肯定是我最大的动力

博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢


对于Mybatis的缓存在上一章节《吃透Mybatis源码-Mybatis执行流程》我们有提到一部分,这篇文章我们对将详细分析一下Mybatis的一级缓存和二级缓存。

市面上流行的ORM框架都支持缓存,不管是Hibernate还是Mybatis都支持一级缓存和二级缓存,目的是把数据缓存到JVM内存中,减少和数据库的交互来提高查询速度。同时MyBatis还可以整合三方缓存技术。

Mybatis一级缓默认开启,是SqlSession级别的,也就是说需要同一个SqlSession执行同样的SQL和参数才有可能命中缓存。如:
在这里插入图片描述
同一个SqlSession执行同一个SQL,发现控制台日志只执行了一次SQL记录,说明第二次查询是走缓存了。但是要注意的是,当SqlSession执行了delete,update,insert语句后,缓存会被清除。

那么一级缓存在哪儿呢?下面给大家介绍一个类。
在这里插入图片描述
Mybatis中提供的缓存都是Cache的实现类,但是真正实现缓存的是PerpetualCache,其中维护了一个Map<Object, Object> cache = new HashMap<Object, Object>() 结构来缓存数据。其他的缓存类采用了装饰模式对PerpetualCache做增强。比如:LruCache 在PerpetualCache 的基础上增加了最近最少使用的缓存清楚策略,当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use)。代码如下

public class LruCache implements Cache {
	//对 PerpetualCache 做装饰
  private final Cache delegate;

下面对其他的缓存类做一个介绍

  • PerpetualCache : 基础缓存类
  • LruCache : LRU 策略的缓存 当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use),eviction=“LRU”(默 认)
  • FifoCache : FIFO 策略的缓存 当缓存到达上限时候,删除最先入队的缓存,配置eviction=“FIFO”
  • SoftCache WeakCache :带清理策略的缓存 通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 SoftReference 和 WeakReference
  • SynchronizedCache : 同步缓存 基于 synchronized 关键字实现,解决并发问题
  • ScheduledCache : 定时调度的缓存,在进行 get/put/remove/getSize 等操作前,判断 缓存时间是否超过了设置的最长缓存时间(默认是 一小时),如果是则清空缓存–即每隔一段时间清 空一次缓存
  • SerializedCache :支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化
  • TransactionalCache :事务缓存,在二级缓存中使用,可一次存入多个缓存,移除多个缓存 。通过TransactionalCacheManager 中用 Map 维护对应关系。

一级缓存到底存储在哪儿?

一级缓存在SimpleExecutor 的父类 BaseExecutor 执行器中,如下

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  //一级缓存
  protected PerpetualCache localCache;

PerpetualCache缓存类源码如下

public class PerpetualCache implements Cache {

  private final String id;
  //缓存
  private Map<Object, Object> cache = new HashMap<Object, Object>();

那么一级缓存在什么时候创建的?

在 BaseExecutor 中的构造器中创建了一级缓存,而执行器Executor 是保存在SqlSession中的,也就是说当创建SqlSession的时候,就会创建 SimpleExecutor,而在SimpleExecutor的构造器中会调用BaseExecutor的构造器来创建一级缓存。见:org.apache.ibatis.executor.SimpleExecutor#SimpleExecutor

public class SimpleExecutor extends BaseExecutor {
	//执行器构造器
  public SimpleExecutor(Configuration configuration, Transaction transaction) {
  	//调用父类构造器
    super(configuration, transaction);
  }

下面是 BaseExecutor 的执行器 org.apache.ibatis.executor.BaseExecutor#BaseExecutor

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  //一级缓存
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;


  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
    //创建一级缓存
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

一级缓存怎么存储的?

一级缓存是在执行查询的时候会先走二级缓存,二级缓存么有就会走一级缓存,以及缓存没有就会走数据库查询,然后放入一级缓存和二级缓存。我们来看一下源码流程 ,见:org.apache.ibatis.executor.CachingExecutor#query

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //构建缓存的Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    //执行查询
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

这里在尝试构建Cachekey ,cachekey时由:MappedStatement的id(如:cn.xx.xx.xxMapper.selectByid) ,分页,Sql,参数值一起构建而成的,一级二级缓存都是如此。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //开启了二级缓存才会存在Cache  
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //走二级缓存查询数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //二级缓存没有,走数据库查询数据
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //写入二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

这里我们看到,在执行org.apache.ibatis.executor.CachingExecutor#query 查询的时候会先走二级缓存,二级缓存没有会继续调用 org.apache.ibatis.executor.BaseExecutor#query 查询,而BaseExecutor#query会尝试先走一级缓存

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //【重要】走一级缓存获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      //如果一级缓存中没有,走数据库查询数据
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

上面代码会先走一级缓存拿数据,如果一级缓存没有,就走数据库获取数据,然后加入一级缓存org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //走数据库查询数据
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //把数据写入一级缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

到这里我们就看到了一级缓存和二级缓存的执行流程,注意的是:先执行二级缓存再执行一级缓存。

这里画一个一级缓存的图
在这里插入图片描述

第一步:二级缓存需要在mybatis-config.xml 配置中开启,如下

<setting name="cacheEnabled" value="true"/>

当然其实该配置默认是开启的,也就是默认会使用 CachingExecutor 装饰基本的执行器。
第二步骤:需要在mapper.xml中配置 < cache/>如下

<mapper namespace="cn.whale.mapper.StudentMapper">
	<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
		 size="1024" 
		 eviction="LRU" 
		 flushInterval="120000" 
		 readOnly="false"/> 
...省略...

解释一下上面的配置,首先<cache/> 是在某个mapper.xml中指定的,也就是说二级缓存作用于当前的namespace.

  • type : 代表的是使用什么类型的缓存,只要是实现了 Cache 接口的实现类都可以
  • size :缓存的个数,默认是1024 个对象
  • eviction : 缓存剔除策略 ,LRU – 最近最少使用的:移除最长时间不被使用的对象(默认);FIFO – 先进先出:按对象进入缓存的顺序来移除它们 ;SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象;WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
  • flushInterval :定时自动清空缓存间隔 自动刷新时间,单位 ms,未配置时只有调用时刷新
  • readOnly :缓存时候只读
  • blocking :是否使用可重入锁实现 缓存的并发控制 true,会使用 BlockingCache 对 Cache 进行装饰 默认 false

Mapper.xml 配置了之后,select()会被缓存。update()、delete()、insert() 会刷新缓存,下面是测试案例
在这里插入图片描述

可以看到,这里使用了2个SqlSesion 2次执行了相同的SQL,参数相同,看控制台日志只执行了一次SQL,说明是命中的二级缓存。因为满足条件:同一个 namespace下的相同的SQL被执行,尽管使用的SqlSession不是同一个。

但是你可能注意到一个细节,就是session.commit() 为什么要提交事务呢?这就要说到二级缓存的存储结构了,如果不执行commit是不会写入二级缓存的。在 CachingExecutor 中有一个属性private final TransactionalCacheManager tcm = new TransactionalCacheManager(); 看名字肯能够看出二级缓存和事务有关系。结构如下

public class CachingExecutor implements Executor {

  private final Executor delegate;
  //二级缓存,通过TransactionalCacheManager来管理
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 中维护了一个 HashMap<Cache, TransactionalCache>()

public class TransactionalCacheManager {
  //二级缓存的HashMap
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

在TransactionCache中维护了一个 Map<Object, Object> entriesToAddOnCommit;

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  //二级缓存临时存储
  private final Map<Object, Object> entriesToAddOnCommit;

  ...省略...
  //写入二级缓存
  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

当执行查询的时候,从数据库查询出来数据回写入TransactionalCache的entriesToAddOnCommit中,我们来看一下二级缓存写入的流程,见:org.apache.ibatis.executor.CachingExecutor#query

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //如果mapper.xml配置了 <cache/> 就会创建 Cache
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //从二级缓存获取
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //写入二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果mapper.xml配置了 就会创建 Cache,Cache不为null,才会走到二级缓存的流程,此时代码来到org.apache.ibatis.cache.TransactionalCacheManager#putObject

public class TransactionalCacheManager {
  //存储二级缓存
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

	public void putObject(Cache cache, CacheKey key, Object value) {
	//通过cache为key拿到 TransactionalCache ,把数据put进去
    getTransactionalCache(cache).putObject(key, value);
  }

存储数据的是TransactionalCache ,见org.apache.ibatis.cache.decorators.TransactionalCache#putObject

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //正在的二级缓存存储位置
  private final Cache delegate;
  private boolean clearOnCommit;
  //临时的二级缓存存储位置
  private final Map<Object, Object> entriesToAddOnCommit;

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

我们看到,数据写到了 TransactionalCache#entriesToAddOnCommit 一个Map中。只有在执行commit的时候数据才会真正写入二级缓存。

我们来看下SqlSession.commit方法是如何触发二级缓存真正的写入的,见:org.apache.ibatis.session.defaults.DefaultSqlSession#commit()

  @Override
  public void commit() {
    commit(false);
  }

  @Override
  public void commit(boolean force) {
    try {
    //调用执行器提交事务
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

代码来到org.apache.ibatis.executor.CachingExecutor#commit

@Override
  public void commit(boolean required) throws SQLException {
    //提交事务
    delegate.commit(required);
    //调用org.apache.ibatis.cache.TransactionalCacheManager#commit提交事务
    tcm.commit();
  }

代码来到org.apache.ibatis.cache.TransactionalCacheManager#commit

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      //调用 TransactionalCache#commit
      txCache.commit();
    }
  }

代码来到org.apache.ibatis.cache.decorators.TransactionalCache#commit

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //真正的二级缓存存储位置,本质是一个 PerpetualCache
  private final Cache delegate;
  //临时存储二级缓存
  private final Map<Object, Object> entriesToAddOnCommit;
  
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    //这里在写入缓存,保存到TransactionalCache中的delegate字段,本质是一个PerpetualCache
    flushPendingEntries();
    //把entriesToAddOnCommit清除掉
    reset();
  }
  
  private void flushPendingEntries() {
	    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
	      //从entriesToAddOnCommit中拿到临时的缓存数据,写入缓存,最终会写入PerpetualCache#cache字段中
	      delegate.putObject(entry.getKey(), entry.getValue());
	    }
	    for (Object entry : entriesMissedInCache) {
	      if (!entriesToAddOnCommit.containsKey(entry)) {
	        delegate.putObject(entry, null);
	      }
	    }
   }
	
	private void reset() {
	    clearOnCommit = false;
	    //清除entriesToAddOnCommit
	    entriesToAddOnCommit.clear();
	    entriesMissedInCache.clear();
  }

所以我们总结一下二级缓存的写入流程,二级缓存通过 TransactionalCacheManager中的一个Map<Cache, TransactionalCache>管理的,当执行query查询处数据的时候,会把数据写入TransactionalCache中的 Map<Object, Object> entriesToAddOnCommit 中临时存储。当执行commit的时候才会把entriesToAddOnCommit中的数据写入TransactionalCache中的 Cache delegate ,其本质和一级缓存一样,也是一个 PerpetualCache

当我们做第二次query的时候会尝试通过 TransactionalCacheManager#getObject 从二级缓存获取数据

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  //获取二级缓存
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

然后会从 TransactionalCache中的delegate中获取缓存

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
 //二级缓存
  private final Cache delegate;
  ...省略...
  
  @Override
  public Object getObject(Object key) {
    // issue #116
    //从二级缓存获取数据
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

所以记得,二级缓存一定要commit才会起作用。下面花了一个一级缓存和二级缓存的结构图
在这里插入图片描述

三方缓存框架

除了使用Mybatis自带的缓存,也可以使用第三方缓存方式,比如:比如 ehcache 和 redis 下面以Redis为例 ,首先导入mybatis整合redis的依赖

<dependency>
	 <groupId>org.mybatis.caches</groupId>
	 <artifactId>mybatis-redis</artifactId> 
	 <version>1.0.0-beta2</version> 
 </dependency>

第二步骤:在mapper.xml配置缓存

<cache type="org.mybatis.caches.redis.RedisCache" 
	eviction="FIFO" 
	flushInterval="60000" 
	size="512" readOnly="true"/>

这里type使用了RedisCache,RedisCache也是实现了Cache接口的,接着我们需要配置Redis的链接属性,默认RedisCache类会读取名字为 : redis.properties 的配置文件

host=127.0.0.1
password=123456
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

再次执行测试代码,查看Redis效果如下
在这里插入图片描述
博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK