Mybatis(三) 快取

進階的小碼農發表於2018-09-11

快取

查詢快取主要是為了提高查詢訪問速度,即當使用者執行一次查詢後,會將該資料結果放到快取中,當下次再執行此查詢時就不會訪問資料庫了而是直接從快取中獲取該資料。 如果在快取中找到了資料那叫做命中。

一級快取

  • 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();
}
複製程式碼

Mybatis(三) 快取

同一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();
}
複製程式碼

Mybatis(三) 快取

一級快取實現

對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的引數值

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(),均會清空一級、二級快取
  • 二級快取是預設開啟的。

Mybatis(三) 快取
如果想要設定增刪改操作的時候不清空二級快取的話,可以在其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));
}
複製程式碼

Mybatis(三) 快取

  • 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));
}
複製程式碼

Mybatis(三) 快取

二級快取關閉

  • 全域性關閉
<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中的資料不一致的情況。

  • 在單表上使用二級快取 在做關聯關係查詢時,就會發生多表的操作,此時有可能這些表存在於多個namespace中,這就會出現上一條內容出現的問題了。

  • 查詢多於修改時使用二級快取 在查詢操作遠遠多於增刪改操作的情況下可以使用二級快取。因為任何增刪改操作都將重新整理二級快取,對二級快取的頻繁重新整理將降低系統效能。

相關文章