深度分析Mybatis的一级、二级缓存

一、一级缓存的配置

MyBatis的全局配置文件中settings节点下配置:

<setting name="localCacheScope" value="SESSION"/>

 

默认情况下,启用了一级缓存,即本地的会话缓存,它仅仅对一个会话中的数据进行缓存。如果localCacheScope设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。

二、二级缓存的配置

MyBatis的全局配置文件中settings节点下配置:

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

 默认是true,全局开启二级缓存,但是如果真是要使用二级缓存必须配置每个Mapper文件,在<mapper>节点下加如下节点:

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

 eviction:缓存清除策略

可用的清除策略有:

LRU – 最近最少使用:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

在每个Mapper.xml配置文件中select、insert、update、delete语句中,也有对缓存更细粒度的配置

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">

flushCache: 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。

useCache :将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:true(对于 insert、update 和 delete 语句)。

默认情况下,语句会这样来配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

三、一级缓存的实现

我们先使用默认的配置,默认是开启一级缓存,缓存范围是Session,查询方法的示例代码如下:

        InputStream inputStream= Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        try(SqlSession sqlSession=sqlSessionFactory.openSession()){

            UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
            User user= userMapper.selectByPrimaryKey(9);
            System.out.println(user);

            user= userMapper.selectByPrimaryKey(9);
            System.out.println(user);

        }

我们先看sqlSessionFactory.openSession()这个方法:

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//创建事务 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建执行器 final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }

  这里我们主要关心的是configuration.newExecutor方法:

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
//这个cacheEnabled就是mybatis二级缓存的全局配置 if (cacheEnabled) { executor = new CachingExecutor(executor); }
//mybatis的插件链(暂不分析) executor = (Executor) interceptorChain.pluginAll(executor); return executor; }

  由上面的代码我们分析出如果不启用二级缓存,Executor 使用的是SimpleExecutor。然后我们回到前面的selectByPrimaryKey方法,调用过程为:

MapperProxy.invoke->MapperMethod.execute->DefaultSqlSession.selectOne->DefaultSqlSession.selectList->

最后到如下方法:

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
//这个MappedStatement就是根据相应Mapper.xml文件生成的配置信息,其中跟配置有关系的属性有:useCache = true,cache = null等,都跟二级缓存有关 MappedStatement ms = configuration.getMappedStatement(statement);
//这里调用的就是SimpleExecutor的父类BaseExecutor.query方法 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(); } }

  然后我们分析BaseExecutor.query方法:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//主要是得到执行的sql语句(把#{}转换成?等)和参数信息 BoundSql boundSql = ms.getBoundSql(parameter);
//生成缓存key,根据ms的id和sql语句、环境等很多条件组装成key
//1429489964:600437586:com.tang.mappers.UserMapper.selectByPrimaryKey:0:2147483647:select\n \n \n user_id, user_type, user_name, mobile, email, user_pass, nick_name, create_time\n \n \n from tb_user where user_id = ?:9:development CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }

  然后Query方法就是我们主要分析实现一级缓存的方法:  @Override

  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++;
//这里就会先从localCache查询是否存在缓存,这里的localCache是BaseExecutor的成员变量,类型是PerpetualCache,PerpetualCache的内部就是一个HashMap
  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();
//如果设置localCacheScope的值为STATEMENT,则每次执行完会清掉缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } 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;
//先存放值等于EXECUTION_PLACEHOLDER的对象到HashMap中 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally {
//查询完删除 localCache.removeObject(key); }
//重新存放从数据库查询到的值 localCache.putObject(key, list);
//这是sql有输出参数的情况,暂不考虑 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }

 最后返回到最开始的示例代码,执行第二次的selectByPrimaryKey方法,我们可以看到第二次查询直接从localCache.getObject(key)获取到对象返回。不过这还没完,在完成SqlSession的Close中Mybatis还会清掉这次会话产生的缓存。看DefaultSqlSession.close方法:

  @Override
  public void close() {
    try {
//这里调用的是SimpleExecutor的Close方法,最后调用的是基类的BaseExecutor的Close方法 executor.close(isCommitOrRollbackRequired(false)); closeCursors(); dirty = false; } finally { ErrorContext.instance().reset(); } }

  BaseExecutor的Close方法如下:

@Override
  public void close(boolean forceRollback) {
    try {
      try {
//回滚 rollback(forceRollback); } finally { if (transaction != null) { transaction.close(); } } } catch (SQLException e) { // Ignore. There's nothing that can be done at this point. log.warn("Unexpected exception on closing transaction. Cause: " + e); } finally { transaction = null; deferredLoads = null;
//这里清空一级缓存的引用 localCache = null; localOutputParameterCache = null; closed = true; } }

@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
//清空前面产生的一级缓存的数据
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}

  到这里这个一级缓存的实现过程分析完成。

 四、二级缓存的实现

 我们继续在前面示例中的UserMapper.xml文件添加二级缓存的配置:

<cache />

 只需要加上这句配置就好。

前面我们分析过sqlSessionFactory.openSession()这个方法,这里面之前创建的Executor类型是SimpleExecutor,当时配置好二级缓存后,类型就是CachingExecutor,然后我们根据一样的思路:

MapperProxy.invoke->MapperMethod.execute->DefaultSqlSession.selectOne->DefaultSqlSession.selectList->

进到CachingExecutor.query方法:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

 这个方法和之前分析一级缓存是一样的,主要看里面调用的query方法:

  @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);
//没有二级缓存则直接调用SimpleExecutor的查询方法 if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查询到数据后存入到二级临时缓存
//注:这里并没有直接放入到二级缓存,而是存放到临时缓存中,真正存入二级缓存的是在Session关闭时 tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } }
//没有配置缓存直接调用SimpleExecutor的查询方法
//delegate通过创建CachingExecutor的时候可以知道delegate就是SimpleExecutor,用的是装饰模式,包装类 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }

  这里就是二级缓存实现的核心逻辑了,但这里特别注意的是tcm.putObject这个方法,我们进入这个方法:

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

  然后再看如下方法:

  private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

  看到这里我们主要分析的是TransactionalCache这个事务缓存类:

public class TransactionalCache implements Cache {

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }


//在访问时,如果会话没关闭,是不会获取到二级缓存的 @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; } } @Override public void putObject(Object key, Object object) {
//添加二级缓存调用的tcm.putObject方法,实际只是添加在entriesToAddOnCommit这个容器中 entriesToAddOnCommit.put(key, object); } @Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); }
//在DefaultSqlSession调用close方法时内部会调用TransactionalCacheManager的commit方法,最后才会调用TransactionalCache的commit方法提交缓存 public void commit() { if (clearOnCommit) { delegate.clear(); }
//刷新二级临时缓存到二级缓存 flushPendingEntries(); reset(); } private void flushPendingEntries() {
//从临时缓存存放到二级缓存 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } }

  然后我们看下在一个DefaultSqlSession会话关闭时调用CachingExecutor的close方法:

  @Override
  public void close(boolean forceRollback) {
    try {
      //issues #499, #524 and #573
      if (forceRollback) {
        tcm.rollback();
      } else {
//实际调用的是TransactionalCacheManager的commit tcm.commit(); } } finally { delegate.close(forceRollback); } }

  TransactionalCacheManager的commit方法如下:

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

  最终调用了TransactionalCache的commit方法。

总结:二级缓存只有关闭了会话后,才能获取到。

 

posted @ 2019-11-24 11:47  myTang  阅读(201)  评论(0)    收藏  举报