mybatis的一級和二級緩存原理及源碼解析

一、作用:

一級緩存:默認開啓,對於同一個SqlSession會話下,參數和SQL語句完全一樣時,第一次查詢的結果會放入緩存,之後的查詢將從緩存中獲取,只要當前會話有寫操作,緩存會被清空。

二級緩存:默認關閉,對於同一個namespace下,參數和SQL語句完全一樣時,第一次查詢的結果會放入緩存,之後的查詢將從緩存中獲取,但只要當前namespace有寫操作,當前的namespace會被清空緩存會被清空。

二、原理

流程圖(圖片來源於https://www.cnblogs.com/happyflyingpig/p/7739749.html)
在這裏插入圖片描述

執行流程:

1、Mapper接口執行方法時,對於一級緩存會忽略CachingExecutor對象,由具體的Executor調用queryFromDatabase查詢數據庫,並將查詢結果保存到本地緩存(一級緩存)

2、在相同的SqlSession會話下,再次執行相同參數、方法時,Executor調用handleLocallyCachedOutputParameters獲取本地緩存(一級緩存)而不查詢數據庫。

3、對於二級緩存,在執行方法時,由CachingExecutor對象先根據namespace獲取Cache對象,如果Cache存在,則直接返回。由於CachingExecutor內部對Executor的封裝(delegate),如果不存在則由Executor(delegate)調用queryFromDatabase查詢數據庫,查詢的

具體實現:

1、Mapper接口執行方法時,由MapperProxy代理對象執行

public class MapperProxy<T> implements InvocationHandler, Serializable {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ...
            // 獲取(緩存)方法,再調用MapperMethodInvoker執行方法
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
    
    // methodCache爲方法緩存,此處是如果方法緩存沒有,則添加該方法的MapperMethodInvoker實現處理
    private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
            return methodCache.computeIfAbsent(method, m -> {
                ...
                // 此處放出幾個關鍵對象,均爲MapperMethodInvoker實現類
                return new DefaultMethodInvoker(getMethodHandleJava8(method));
                return new DefaultMethodInvoker(getMethodHandleJava9(method));
                return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            });
    }
}

2、執行MapperMethod方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
	  ...
      case SELECT: {
          // 關鍵代碼,MapperMethod方法實際由SqlSession調用實現
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
      }
      case FLUSH:
      default:
    }
    return result;
  }

3、由CachingExecutor對象執行查詢

// 先獲取MappedStatement(sql的具體信息),再由執行器執行查詢,此處的執行器爲CachingExecutor
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

// 此處的Executor爲什麼是CachingExecutor呢,其它的爲什麼是BaseExecutor?
// 原因是啓動時會去加載配置信息,查看對應的xml有無配置Cache元素,是否開啓了二級緩存,具體代碼如下
// 3.1:由SqlSessionFactoryBuilder的build方法中用XMLConfigBuilder加載配置文件
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    	...
        // 關鍵代碼,parser.parse()則解析配置文件
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    }
}
// parser.parse()具體的解析
private void parseConfiguration(XNode root) {
    // ...
    settingsElement(settings);
    // ...
}
// 設置開啓緩存
private void settingsElement(Properties props) {
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
}

// 3.2:由SqlSessionFactory調用openSession()獲取SqlSession時,獲取執行器時,指定爲CachingExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 3.2.1:
    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);
    }
    // 3.2.2:由於上述初始化時開啓了緩存,所以此處是true,則將3.2.1處初始化的executor作爲參數封裝在CachingExecutor中,在內部爲一個delegate變量中,該變量類型爲Executor
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

具體查詢:

// 該方法由CachingExecutor調用的查詢,先去獲取Cache緩存,如果開啓了二級緩存,有數據則之間返回,無數據則調用內部的Executor delegate去查詢
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

4、Executor執行機查詢數據

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
	// ...
    // 關鍵代碼,由執行機查詢之前,先去查看是否由本地緩存,本地緩存Cache實現類爲PerpetualCache對象
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
        // ...
    } else {
        // 無本地緩存則查詢數據庫
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    // ...
}

5、查詢數據庫

// 查詢數據庫
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
    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;
}

三、區別:

一級緩存:是SqlSession級別,對於commit、close、寫操作後,緩存都會清空

二級緩存:是namespace級別,如果有多個namespace,對同一個表進行操作,可能出現數據不一致的情況。如果讀多寫少、單表操作可以選擇開啓二級緩存

參考資料

https://blog.csdn.net/qq_35688140/article/details/89848539

https://www.cnblogs.com/happyflyingpig/p/7739749.html

https://blog.csdn.net/u012489412/article/details/86712331

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章