一、作用:
一級緩存:默認開啓,對於同一個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