fengwbetter

目录

mybatis缓存机制

mybatis支持一、二级缓存来提高查询效率,能够正确的使用缓存的前提是熟悉mybatis的缓存实现原理;

众所周知,mybatis的sqlSession封装了对数据库的增删改查操作,但是每个SqlSession持有各自的Executor,真正的操作是委托给Executor操作的,而缓存功能也同样是交给了Executor实现;

Executor和缓存

下面看一段Configuration类创建执行器的代码:

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);
    }
    //如果开启了缓存则使用CachingExecutor装饰
    //cacheEnabled实际上是二级缓存开关,默认也是开启的
    //只是二级缓存需要额外的配置所有并不生效
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

mybatis可选配置的执行器有三种,分别是SimpleExecutor、ReuseExecutor和BatchExecutor,默认是SimpleExecutor;除此之外还有一个重要的执行器是CachingExecutor,根据名称即可推断它与缓存是相关的;看类图:

我们发现BaseExecutor和CachingExecutor实现了Executor接口,BaseExecutor是一个抽象类,它有三个子类(实际上还有一个ClosedExecutor)

一级缓存

mybatis一级缓存是在BaseExecutor中实现的,也相当于一级缓存是默认开启的;Cache对象是在BaseExecutor构造方法中创建的,因此一个Executor对应一个locaCache,下面看一下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++;
      //从一级缓存中取缓存(我们通常的查询中是不需要resultHandler的)
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //handleLocallyCachedOutputParameters这个只对存储过程有效
        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();
      }
      deferredLoads.clear(); // issue #601
      //如果一级缓存的范围是statement级别,则每次查询都清空一级缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // issue #482
      }
    }
    return list;
  }

因此,在不考虑二级缓存的情况下,每次查询都从一级缓存中取,如果没有命中缓存,则从数据库查询,并将查询结果加入缓存;这只是一级缓存的存取,接下来还要知道缓存何时失效,其实我们可以推测一下,如果数据库更新了,但是缓存并没有失效,那么缓存的数据就成了脏数据,所以缓存失效肯定和更新操作有关,但是这个更新就有范围了,是更新操作清除所有缓存(全局)?还是同一个SQLSession的更新操作清除当前SQLSession的缓存呢?

通过文档和源码我们知道LocalCacheScope有两个级别,分别是statement和session;从query方法已经知道statement级别每次查询都清除缓存,这也是一级缓存默认的级别;

那么session级别呢?
下面看BaseExecutor的update方法(SqlSesssion的insert、update、delete操作最后都会执行此方法):

 public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) throw new ExecutorException("Executor was closed.");
    //清除缓存
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

可以看到如果是session级别,在update操作的时候清除缓存;但是有两点要注意:

一、为什么叫做session级别?

同一个SqlSession持有同一个Executor,同一个Executor持有同一个LocalCache,clearLocalCache操作只是清除当前executor的本地缓存,因此session级别的缓存就是对同一个SqlSession生效。

二、缓存失效的时机

可以看到清除缓存是在doUpdate(真正的更新操作)操作之前执行的,也就是说doUpdate执行成功或失败、提交或者回滚 缓存都会失效;

小结

  • MyBatis一级缓存使用没有容量限制的HashMap,比较简陋;
  • statement级别的缓存每一次查询后清除;
  • session级别缓存在同一个SqlSession的insert、update、delete操作之前清除;
  • MyBatis的一级缓存最大是同一个SqlSession,在多个SqlSession环境下就会出现数据修改后缓存无法及时失效的情况产生脏数据;

二级缓存

前面我们知道二级缓存开启后Executor会使用CachingExecutor装饰;那就来看看它的query方法:

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) {
      //是否立即清除缓存,这个是statement标签中flushCache属性控制的,select标签默认false,其它标签默认true;
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        //关于存储过程暂不考虑
        //isUseCache()的值是statement标签中useCache配置的,默认为true
        ensureNoOutParams(ms, parameterObject, 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. Query must be not synchronized to prevent deadlocks
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

这里从查询缓存和加入缓存用的是tcm(TransactionalCacheManager)的getObject和putObject方法,稍稍看一下这个类:

  public class TransactionalCacheManager {
  //维护TransactionalCache 和 Cache 一对一的这样一个映射关系
  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);
  }
  //加入缓存(真正加入还要等commit)
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
   //省略一部分
   。。。。。。。
   
  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      //使用TransactionalCache装饰Cache
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

这里我们只需要知道关于缓存的操作最终还是委托给Cache类的,其它的暂不深入,回到CacheExecutor类,Cache对象是从MappedStatement(对应就是

相关文章: