Mybatis源碼分析之Cache一級緩存原理(四)


以前的文章我已經基本講解到了SqlSessionFactory、SqlSession、Excutor以及Mpper執行SQL過程,下面我來了解下myabtis的緩存,java

它的緩存分爲一級緩存和二級緩存,本文咱們主要分析下一級緩存。sql


先看一個例子,代碼仍是以前(第一篇)的的demo數據庫

    public static void main(String[] args) throws Exception {
        SqlSessionFactory sessionFactory = null;
        String resource = "configuration.xml";
        sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource));
        SqlSession sqlSession = sessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.findUserById(1));
        System.out.println(userMapper.findUserById(1));
    }

上面代碼咱們執行了兩次userMapper.findUserById(1),結果以下圖緩存


從執行結果看,DB的查詢只有1次,那麼第二次的查詢結果是在怎麼來的呢?微信



一:什麼是一級緩存


   每當咱們使用MyBatis開啓一次和數據庫的會話,MyBatis會建立出一個SqlSession對象表示一次數據庫會話。 在對數據庫的一次會話中,咱們有可能會反覆地執行徹底相同的查詢語句,若是不採起一些措施的話,每一次查詢都會查詢一次數據庫,而咱們在極短的時間內作了徹底相同的查詢,那麼它們的結果極有可能徹底相同,因爲查詢一次數據庫的代價很大,這有可能形成很大的資源浪費。      session

爲了解決這一問題,減小資源的浪費,MyBatis會在表示會話的SqlSession對象中創建一個簡單的緩存,將每次查詢到的結果結果緩存起來,當下次查詢的時候,若是判斷先前有個徹底同樣的查詢,會直接從緩存中直接將結果取出,返回給用戶,不須要再進行一次數據庫查詢了。mybatis

     基本的流程示意圖以下:架構

     

     

     對於會話(Session)級別的數據緩存,咱們稱之爲一級數據緩存,簡稱一級緩存。app

     

     

二:如何執行緩存


    咱們知道mybatis的SQL執行最後是交給了Executor執行器來完成的,咱們看下BaseExecutor類的源碼ide

    

 @Override
     public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
       BoundSql boundSql = ms.getBoundSql(parameter);
       CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
       return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    
    
    
     @SuppressWarnings("unchecked")
     @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++;
         list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;//localCache 本地緩存
         if (list != null) {
           handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
         } else {
           list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);  //若是緩存沒有就走DB
         }
       } finally {
         queryStack--;
       }
       if (queryStack == 0) {
         for (DeferredLoad deferredLoad : deferredLoads) {
           deferredLoad.load();
         }
         // issue #601
         deferredLoads.clear();
         if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
           // issue #482
           clearLocalCache();
         }
       }
       return list;
     }
     
     
     
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);//清空現有緩存數據
    }
    localCache.putObject(key, list);//新的結果集存入緩存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }


經過上面的三個方法咱們基本已經看明白了緩存的使用,它的本地緩存使用的是PerpetualCache類,內部其實仍是一個HashMap,只是稍微作了封裝而已。

咱們再看下天的Key是如何生成的

 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);



  @Override
  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(Integer.valueOf(rowBounds.getOffset()));
    cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      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;
  }

它是經過statementId,params,rowBounds,BoundSql來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果。


咱們用一張圖來看清楚一級緩存的基本流程(本圖網上早來的,本身懶得畫了)


三:一級緩存生命週期


1. MyBatis在開啓一個數據庫會話時,會建立一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,

Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉。

2. 若是SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用。

3. 若是SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,可是該對象仍可以使用。

4.SqlSession中執行了任何一個update操做(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,可是該對象能夠繼續使用。

本文分享自微信公衆號 - JAVA高級架構(gaojijiagou)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索