Mybatis(三) 缓存

缓存

查询缓存主要是为了提升查询访问速度,即当用户执行一次查询后,会将该数据结果放到缓存中,当下次再执行此查询时就不会访问数据库了而是直接从缓存中获取该数据。 若是在缓存中找到了数据那叫作命中。sql

一级缓存

  • MyBatis的一级查询缓存(也叫做本地缓存)是基于org.apache.ibatis.cache.impl.PerpetualCache 类的 HashMap本地缓存,其做用域是SqlSession
  • 在同一个SqlSession中两次执行相同的 sql 查询语句,第一次执行完毕后,会将查询结果写入到缓存中,第二次会从缓存中直接获取数据,而再也不到数据库中进行查询,这样就减小了数据库的访问,从而提升查询效率。
  • 当一个 SqlSession 结束后,该 SqlSession 中的一级查询缓存也就不存在了。 myBatis 默认一级查询缓存是开启状态,且不能关闭
  • 增删改会清空缓存,不管是否commit
  • 当SqlSession关闭和提交时,会清空一级缓存

同一sqlSession 屡次查询同一SQL时会使用缓存

@Test
public void testLocalCache() throws Exception {
    SqlSession sqlSession = factory.openSession(); // 自动提交事务
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

    System.out.println(studentMapper.getStudentById(1));
    // 第二三次会从缓存中拿数据,不查数据库
    System.out.println(studentMapper.getStudentById(1));
    System.out.println(studentMapper.getStudentById(1));

    sqlSession.close();
}
复制代码

同一sqlSession 有增删改时会清空缓存

@Test
public void testLocalCacheClear() throws Exception {
    SqlSession sqlSession = factory.openSession(true); // 自动提交事务
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

    System.out.println(studentMapper.getStudentById(1));
    // 增删改会清空缓存
    System.out.println("增长了" + studentMapper.addStudent(buildStudent()) + "个学生");
    // 会从数据库查数据
    System.out.println(studentMapper.getStudentById(1));

    sqlSession.close();
}
复制代码

一级缓存实现

对SqlSession的操做mybatis内部都是经过Executor来执行的。Executor的生命周期和SqlSession是一致的。Mybatis在Executor中建立了一级缓存,基于PerpetualCache 类的 HashMap数据库

public class DefaultSqlSession implements SqlSession {

   private Configuration configuration;
   // 执行器
   private Executor executor;
   private boolean autoCommit;
   private boolean dirty;
   private List<Cursor<?>> cursorList;
}
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;
   protected PerpetualCache localOutputParameterCache;
   protected Configuration configuration;
   protected int queryStack;
   private boolean closed;
 
    protected BaseExecutor(Configuration configuration, Transaction transaction) { 
        this.configuration = configuration; this.transaction = transaction;
        this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
        this.closed = false; this.wrapperExecutor = this;
        //mybatis一级缓存,在建立SqlSession->Executor时候动态建立,随着sqlSession销毁而销毁
        this.localCache = new PerpetualCache("LocalCache");
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); 
    }
}
// 缓存实现类
public class PerpetualCache implements Cache {

    private String id;
    private Map<Object, Object> cache = new HashMap<Object, Object>();
    public PerpetualCache(String id) {
         this.id = id;
    }
}
复制代码
//SqlSession.selectList会调用此方法(一级缓存操做,老是先查询一级缓存,缓存中不存在再查询数据库)
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.");
    } 
    //先清一级缓存,再查询,但仅仅查询堆栈为0才清,为了处理递归调用 
    if (queryStack == 0 && ms.isFlushCacheRequired()) { 
        clearLocalCache(); 
    } 
    List<E> list; 
    try { 
        //加一,这样递归调用到上面的时候就不会再清局部缓存了
        queryStack++; 
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; 
        if (list != null) {
            //若是查到localCache缓存,处理
             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(); //清空延迟加载队列 
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        } 
    }
    return list; 
}

复制代码

localCache 缓存的key 为CacheKey对象 CacheKey:statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值apache

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    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);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }
复制代码

一级缓存生命周期总结

  • MyBatis在开启一个会话时,会建立一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
  • 若是SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
  • 若是SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,可是该对象仍可以使用;
  • SqlSession中执行了任何一个update操做(update()、delete()、insert()),都会清空PerpetualCache对象的数据,可是该对象能够继续使用;

二级缓存

  • MyBatis的二级缓存是mapper范围级别的
  • SqlSession关闭后才会将数据写到二级缓存区域
  • 增删改操做,不管是否进行提交commit(),均会清空一级、二级缓存
  • 二级缓存是默认开启的。

若是想要设置增删改操做的时候不清空二级缓存的话,能够在其insert或delete或update中添加属性flushCache=”false”,默认为 true。

<delete id="deleteStudent" flushCache="false">
    DELETE FROM t_student where id=#{id}
</delete>
复制代码

开启二级缓存

// mybatis-config.xml 中配置
<settings>
    <setting name="localCacheScope" value="SESSION"/>
    默认值为 true。即二级缓存默认是开启的
    <setting name="cacheEnabled" value="true"/>
</settings>

// 具体mapper.xml 中配置
<mapper namespace="cn.itcast.mybatis.mapper.UserMapper">
 
	<!-- 开启本mapper的namespace下的二级缓存
	type:指定cache接口的实现类的类型,mybatis默认使用PerpetualCache
	要和ehcache整合,须要配置type为ehcache实现cache接口的类型-->
	
	<cache />
	
    <!-- 下面的一些SQL语句暂时略 -->
</mapper>
复制代码

二级缓存的实现

  • 建立执行器 Configuration.newExecutor()方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    //确保ExecutorType不为空(defaultExecutorType有可能为空)
    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);
    }
    //重点在这里,若是启用二级缓存,返回Executor的Cache包装类对象
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
复制代码
  • CachingExecutor

静态代理模式。在CachingExecutor的全部操做都是经过调用内部的delegate对象执行的。缓存只应用于查询缓存

public class CachingExecutor implements Executor {

  private Executor delegate;
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
   @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    //是否须要更缓存
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }
   @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) {
          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);
  }
}
复制代码

不一样SqlSession,同一Mapper

  • SqlSession关闭后才会将数据写到二级缓存区域
@Test
public void testCacheWithCommitOrClose() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
    SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    //sqlSession1关闭后,会将sqlsession1中的数据写到二级缓存区域
    //不关闭的话不会写入二级缓存
    sqlSession1.close();
    
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
复制代码

  • sqlSession未关闭,不会将数据写到二级缓存区域
@Test
public void testCacheWithoutCommitOrClose() throws Exception {
    SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
    SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    //sqlSession未关闭,不会将数据写到二级缓存区域,会从数据库中查询
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
复制代码

二级缓存关闭

  • 全局关闭
<setting name="cacheEnabled" value="false"/>
复制代码
  • 局部关闭 局部关闭是只关闭某个select查询的二级缓存,在select标签中将属性useCache设置为false,那么就会关闭该select查询的二级缓存。
<select id="selectStudentById" useCache="false" resultMap="studentMapper">
    SELECT id,name,age,score,password FROM t_student where id=#{id}
</select>
复制代码

使用注意事项

  • 在一个命名空间下使用二级缓存 二级缓存对于不一样的命名空间namespace的数据是互不干扰的,假若多个namespace中对一个表进行操做的话,就会致使这不一样的namespace中的数据不一致的状况。bash

  • 在单表上使用二级缓存 在作关联关系查询时,就会发生多表的操做,此时有可能这些表存在于多个namespace中,这就会出现上一条内容出现的问题了。session

  • 查询多于修改时使用二级缓存 在查询操做远远多于增删改操做的状况下可使用二级缓存。由于任何增删改操做都将刷新二级缓存,对二级缓存的频繁刷新将下降系统性能。mybatis

相关文章
相关标签/搜索