Mybatis缓存源码详解

Mybatis缓存源码详解

示例代码地址:https://gitee.com/cq-laozhou/mybaits-source-code-analyzer

Mybatis内部使用2级缓存便于加速数据的查询,降低数据库的查询压力。

一级缓存 LocalCache

在Mybatis的执行流程中,首先去查找二级缓存,如果二级缓存中没有,再去一级缓存找,如果还没有,最后从数据库查询。

一级缓存示例

一级缓存逻辑相对简单,首先以一个简单的示例,来看看一级缓存带来的效果:

public void testLocalCacheSuccess(){
    SqlSession sqlSession = getSqlSession();
    SysUserMapper sysUserMapper = sqlSession.getMapper(SysUserMapper.class);
    SysUser sysUser = sysUserMapper.selectById(1L);
    SysUser sysUser1 = sysUserMapper.selectById(1L);
    //在相同的sqlSession生命周期中,同样的查询方法,同样的参数,则命中1级缓存,返回相同的对象。
    System.out.println(sysUser == sysUser1);
}

输出结果为true,同时只往DB发送了一次查询语句,因此一级缓存生效。

一级缓存默认是开启的,并且不能关闭,在同一次查询会话中如果出现相同的语句及参数,就会从缓存中取出不在走数据库查询。1级缓存只能作用于查询会话中 所以也叫做会话缓存。
不过想要一级缓存命中,需要满足下面的一系列的条件:

  1. 必须是相同的SqlSession,即相同的会话
  2. 必须是相同的Mapper类型(并不一定是相同的Mapper实例)
  3. 必须是相同的方法和参数(这个好理解,查的东西是一样的才能用缓存嘛)
  4. 查询语句的中间不能有写操作的语句,比如插入、更新、删除(这些写操作为让一级缓存全部清空)

具体案例见:
CacheTest的testLocalCacheWithDiffSqlSession、testLocalCacheWithDiffMapper、testLocalCacheWithDiffMethod、testLocalCacheWithDiffParameter、testLocalCacheWithWriteStatement等测试方法。

一级缓存实现源码

接下来,通过源码来分析下一级缓存的实现:

在前面的例子中,当执行sysUserMapper.selectById(1L);语句后,最终会调用到 DefaultSqlSession.selectList方法上来,至于是如何调过来的,后面解析整个执行流程中再分析,目前单独看下一级缓存的实现:

 @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
    	//从configuration中获取MappedStatement。configuration封装了mybaits配置信息,而每条sql语句封装在MappedStatement中。
      MappedStatement ms = configuration.getMappedStatement(statement);
      //调用执行器,进行查询操作。
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在构建DefaultSqlSession实例时,executor为CachingExecutor,并使用装饰者模式,内部默认包装了SimpleExecutor(至于怎么构建的,同理在执行流程中分析),如下图所示:

在这里插入图片描述

在CachingExecutor.query 方法中,封装了二级缓存的处理逻辑,可以简单的过下,在下面二级缓存解析中详细说明:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //获取二级缓存对象
    Cache cache = ms.getCache();
    
    //获取到了缓存对象
    if (cache != null) {
    	//如果有必要的话,先刷新二级缓存
      flushCacheIfRequired(ms);
      //开启了二级缓存
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        //从二级缓存中获取结果
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
        	//从缓存中没有获取到结果,则调用内部的被装饰的Executor执行查询。
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将查询结果放入临时缓存中(TransactionalCache.entriesToAddOnCommit)最终session在commit的时候在放入二级缓存。
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //返回查询结果。
        return list;
      }
    }
    
    //没有获取到二级缓存对象,直接调用内部的被装饰的Executor执行查询,相当于使用二级缓存。
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

调用内部的Executor执行查询,会调用SimpleExecutor.query方法,这儿又使用了模板方法模式,在父类BaseExecutor中封装了query的整体执行流程, 而在子类SimpleExecutor中,封装了某些步骤的具体算法。先分析下BaseExecutor.query方法:

@SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
   	//执行前的一些准备工作
   	...
    List<E> list;
    ...
    //从一级缓存localCache中获取结果
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
    	//获取到了,进行一些其他特殊的处理
    	....
    } else {
    	//没获取到,从DB中查询
    	list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    //一些其他处理  
    ...
    //返回结果
    return list;
  }

继续queryFromDatabase方法:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //先往localCache中放置一个占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
    	//这儿调用子类(即SimpleExecutor)的doQuery方法,执行正在的数据库查询。
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
    	//将localCache中的占位符移除
      localCache.removeObject(key);
    }
    //往localCache中放入查询结果
    localCache.putObject(key, list);
    ....
    return list;
  }

SimpleExecutor.doQuery方法在执行流程中详解,现在只需要知道,它就是真正执行sql语句获取结果的。

这个localCache是PerpetualCache这种类型的cache,其实现非常简单,内部用了一个map来装缓存数据的

public class PerpetualCache implements Cache {

  private String id;
	
	//缓存数据存放在这个map中。
  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

	//放入缓存
  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

	//从缓存中获取
  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

	//移除缓存
  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

	//清空缓存
  @Override
  public void clear() {
    cache.clear();
  }

现在我们可以回答下,要想享受一级缓存带来的福利,为什么要满足下面这些条件了?

  1. 为什么必须是相同的SqlSession,即相同的会话

    这是因为localCache对象是封装在SimpleExecutor对象中,这个有被DefaultSqlSession引用,因此localCache的活动范围和生命周期与Sqlsession一致,不同的sqlsession拥有不同的localCache对象。

  2. 为什么必须是相同的Mapper类型?

  3. 为什么必须是相同的方法和参数?

    这两个问题,涉及到缓存key,看下缓存key是如何生成的, 在BaseExecutor.createCacheKey方法中:

    CacheKey cacheKey = new CacheKey();
        cacheKey.update(ms.getId());  //语句ID,包括mapper的namespace和方法名称
        cacheKey.update(rowBounds.getOffset());  //分页的偏移量
        cacheKey.update(rowBounds.getLimit());		//分页的限制
        cacheKey.update(boundSql.getSql());			//sql语句
        ...
        for (ParameterMapping parameterMapping : parameterMappings) {
         		//value为参数值
            cacheKey.update(value);  //所有参数值
          }
        }
        if (configuration.getEnvironment() != null) {
          //环境ID
          cacheKey.update(configuration.getEnvironment().getId());  //环境ID
        }
        return cacheKey;
    

    可以看出,缓存key的构建由一系列的变量控制,其中有任何一个不一样,生成的key就不同,那么自然就不会命中缓存了。这种是必须要有相同的mapper(其实要相同的namespace),相同的方法,相同的参数,相同的sql语句,分页参数也要一样,如果有环境的话,环境也要一样。

  4. 查询语句的中间不能有写操作的语句,比如插入、更新、删除(这些写操作为让一级缓存全部清空)

    为了回答这个问题,需要看下执行写操作时,会发生什么?首先,先明确一点,DefaultSqlSession暴露出来的insert、update、和delete方法,实际上内部都是调用的update方法,比如:

    //插入方法
    public int insert(String statement, Object parameter) {
      return update(statement, parameter);
    }
    
    //删除方法
    public int delete(String statement) {
    	return update(statement, null);
    }
    

    而update方法滴调用时是Executor的update方法:

    public int update(String statement, Object parameter) {
        try {
          dirty = true;
          MappedStatement ms = configuration.getMappedStatement(statement);
          return executor.update(ms, wrapCollection(parameter));
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    
    

    先忽略二级缓存(CachingExecutor),那么直接分析BaseExecutor的update方法:

    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ....
        //清空一级缓存LocalCache
        clearLocalCache();
        //在执行子类的(SimpleExecutor)的doUpdate方法,执行真正的数据库更新。
        return doUpdate(ms, parameter);
      }
    
    

    可以看出,update方法一进来,二话不说,直接将一级缓存全部清空,所以在两次查询中间,夹杂着写操作,那么后面的查询是永不了缓存的。

二级缓存 Cache

一级缓存让我们在同一个SqlSession中,对多次执行同一个查询时,不用重复查询数据库,提高了查询效率。而二级缓存可让我们在跨SqlSession中命中缓存,提高查询效率,降低数据库查询压力。

二级缓存示例

同样以一个示例来体验下二级缓存:

@Test
public void testL2Cache(){
    SqlSession sqlSession = getSqlSession();
    try {
        SysRoleMapper mapper = sqlSession.getMapper(SysRoleMapper.class);
        SysRole sysRole1 = mapper.selectById2(1);  //第一次查询,结果放入一级缓存中
        SysRole sysRole2 = mapper.selectById2(1);		//同一个sqlsession的第二次查询,此时二级缓存还没有,会走到一级缓存中,并命中
        assertEquals(sysRole1, sysRole2); //L1Cache.
    }finally {
        sqlSession.close();		//将一级缓存中的结果,放入到二级缓存中。
    }

    sqlSession = getSqlSession();  //开启另外一个sqlsession
    try {
        SysRoleMapper mapper = sqlSession.getMapper(SysRoleMapper.class);
        SysRole sysRole3 = mapper.selectById2(1);  //直接命中2级缓存
        System.out.println(sysRole3.getRoleName());
        SysRole sysRole4 = mapper.selectById2(1);   //再次命中2级缓存
        assertEquals(sysRole3, sysRole4);
    }finally {
        sqlSession.close();
    }
}

注意,二级缓存默认是没有的,需要配置上才能使用,xml配置和注解配置是一致的,这儿以xml为例:

<!ELEMENT cache (property*)>  -- 子元素用于配置缓存实现类所需要的属性
<!ATTLIST cache
type CDATA #IMPLIED						-- 缓存具体实现类,默认为PerpetualCache
eviction CDATA #IMPLIED				-- 缓存逐出策略,默认为Lru,即最近最少使用。
flushInterval CDATA #IMPLIED	-- 缓存过期时间,默认为0,即不过期。
size CDATA #IMPLIED						-- 缓存大小,默认1024。注意该配置不是指缓存的对象数量,而是查询次数。
readOnly CDATA #IMPLIED				-- 是否只读。默认false,即缓存实例获取后,会不会去修改。mybatis会根据此配置来决定返回给你的对象是否是个新对象(通过反序列化给你一个全新的对象,你对这个对象的任何操作不会污染到缓存)
blocking CDATA #IMPLIED				-- 是否阻塞,默认false。用于解决缓存击穿时,对数据库带来瞬时压力问题。
>

比如,示例的配置:

 <cache eviction="FIFO" size="512" readOnly="true"/>

二级缓存实现源码

二级缓存的实现相对于一级缓存来说,有些许复杂,不过掌握了其核心思想和脉络后,也比较清晰。

在前面一级缓存的分析过程中,我们简单的看了下二级缓存的处理流程,位于CachingExecutor.query方法中

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //获取二级缓存对象
    Cache cache = ms.getCache();
    
    //获取到了缓存对象
    if (cache != null) {
    	//如果有必要的话,先刷新二级缓存
      flushCacheIfRequired(ms);
      //开启了二级缓存
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        //从二级缓存中获取结果
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
        	//从缓存中没有获取到结果,则调用内部的被装饰的Executor执行查询。
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将查询结果放入临时缓存中(TransactionalCache.entriesToAddOnCommit)最终session在commit的时候在放入二级缓存。
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //返回查询结果。
        return list;
      }
    }
    
    //没有获取到二级缓存对象,直接调用内部的被装饰的Executor执行查询,相当于使用二级缓存。
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

对于二级缓存来说,关键的代码就是从二级缓存获取结果tcm.getObject(cache, key);以及将结果保存到二级缓存中tcm.putObject(cache, key, list);

在进入关键的代码之前,先热下身,了解下二级缓存的范围和生命周期,在mybatis框架初始化时,会解析配置并生成Configuration对象,该对象是单例的,应用级的生命周期。其有一属性 为protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");,这个map会在初始化过程中被填充,以namespace为key,而value为一系列的装饰者装饰过后的配置的cache实现类实例。

核心代码为,MapperBuilderAssistant.useNewCache方法逻辑:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    //构建一个新的Cache
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    //放到全局的configuration中,也就是上面说的那个map,key就是currentNamespace。
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

看下CacheBuilder.build方法:

public Cache build() {
		//默认使用PerpetualCache。
    setDefaultImplementations();
    //反射实例化具体的实现
    Cache cache = newBaseCacheInstance(implementation, id);
    //反射设置cache的相关属性
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    //如果实现类是PerpetualCache,使用一系列的装饰者装饰。注意如果是自定义的cache实现,不会被装饰。
    if (PerpetualCache.class.equals(cache.getClass())) {
    	//使用指定的装饰者装饰,比如示例中定义的eviction="FIFO"配置,这儿会使用fifocache来装饰
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      //使用标准的装饰者装饰
      cache = setStandardDecorators(cache);
    } 
    
    //返回装饰好的cache对象。
    return cache;
  }

那么标准的装饰者,又装饰些啥上去了? 看下CacheBuilder.setStandardDecorators方法:

private Cache setStandardDecorators(Cache cache) {
   
   MetaObject metaCache = SystemMetaObject.forObject(cache);
   if (size != null && metaCache.hasSetter("size")) {
   	metaCache.setValue("size", size);
   }
   //配置了clearInterval,则使用ScheduledCache装饰一波
   if (clearInterval != null) {
     cache = new ScheduledCache(cache);
     ((ScheduledCache) cache).setClearInterval(clearInterval);
   }
   //如果可读写(非只读),则使用SerializedCache装饰一波
   if (readWrite) {
   	cache = new SerializedCache(cache);
   }
   //使用LoggingCache装饰一波
   cache = new LoggingCache(cache);
   //再使用SynchronizedCache装饰一波
   cache = new SynchronizedCache(cache);
   
   //如果blocking为true,则使用BlockingCache装饰。
   if (blocking) {
   		cache = new BlockingCache(cache);
   }
   return cache;
    ..
 }

二级缓存的这块实现算是它的精髓了,如果想了解装饰者设计模式,可参考 https://blog.csdn.net/gruelxsp/category_9399300.html 中的 装饰者模式 说明。

比如,对于示例的配置,包装出来的cache长这个样子:

在这里插入图片描述

好了,热身结束,回到 从二级缓存获取结果tcm.getObject(cache, key);以及将结果保存到二级缓存中tcm.putObject(cache, key, list);这两个操作上来。

首先,tcm是TransactionalCacheManager示例,它在实例化CachingExecutor时,会示例化出来

private TransactionalCacheManager tcm = new TransactionalCacheManager();

而TransactionalCacheManager实现非常简单,其主要作用管理当前这个执行器中会用到的二级缓存,并用TransactionalCache这个装饰一波,代码如下:

public class TransactionalCacheManager {
	
	//内部使用TransactionalCache包装过的map。
  private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }
	
	//如果map中有了就直接用,没有的话就new一个TransactionalCache来装饰。
  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

因此tcm.getObject(cache, key);调动实际上会调用TransactionalCache的getObject方法:

@Override
  public Object getObject(Object key) {
  
 		//直接从被代理的cahe中获取(也就是前面分析的configuration中的对应的cache)
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

tcm.putObject(cache, key, list);这个方法会调用到TransactionalCache的putObject方法:

//这儿紧紧是放到了TransactionalCache的entriesToAddOnCommit的map中,并不是真正的放到二级缓存中
public void putObject(Object key, Object object) {
	entriesToAddOnCommit.put(key, object);
}

那么,何时会放到真正的二级缓存中呢?

在SqlSession执行commit或者close方法是,会调用CachingExecutor的commit或者close方法

public void commit(boolean force) {
    ...
    executor.commit(isCommitOrRollbackRequired(force));
    ...
}

public void close() {
  ...
    executor.close(isCommitOrRollbackRequired(false));
  ...
}

CachingExecutor的commit或者close方法,会调用tcm.commit();方法:

public void commit() {
  for (TransactionalCache txCache : transactionalCaches.values()) {
  	txCache.commit();
  }
}

最终会调用到 TransactionalCache 的 flushPendingEntries 方法,这儿才是真正的将加过放到二级缓存中。

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    ...
  }

为什么不是在前面就放到二级缓存中,而是要使用TransactionalCache来中间做一层临时存储,而在sqlsession关闭或者提交时才将临时存储中的结果放到二级缓存中?

这个问题的答案就可在回滚逻辑中看出点端倪,以前面同样的思路分析,sqlsession的rollback会调用CachingExecutor的rollback方法,然后调用tcm.rollback();,最终会调用到TransactionalCache的方法:

private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear(); //清空临时存储结果。
    entriesMissedInCache.clear();
  }

可以看到,在rollback这种情况下,是不会往二级缓存中存放查询结果的。这个也好理解,如果回滚的情况下也往二级缓存存放的话,会存在其他sqlsession查询时命中查询结果,而这个查询结果是应该被回滚的,造成脏读。

比如:

在sqlsession1中:执行一条更新语句—> 查询该条结果 --> 放入二级缓存 --> 回滚

在sqlsession2中: 查询同样的记录—> 命中缓存 --> 返回结果(而这个结果在数据库中是被回滚的。

最后,当执行update方法时,即所有的写操作时,和一级缓存一样,会清空对应namespace的二级缓存,源码见CachingExecutor.update方法:

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

flushCacheIfRequired方法逻辑:

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {   //isFlushCacheRequired,非select方法默认为true。 
      tcm.clear(cache); //清空二级缓存。
    }
  }

到此,完整分析了Mybatis的缓存实现,整体还是容易理解,其精髓是多处使用了装饰者模式,让每个类的职责更加清晰,实现也更容易。

当然,本文没有详细分析具体的cache和这些装饰者的具体实现,这些可以留给你自己去分析下。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值