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

相關文章
相關標籤/搜索