前言:
Mybatis為了提升性能,內置了本地緩存(也可以稱之為一級緩存),在mybatis-config.xml中可以設置localCacheScope中可以配置本地緩存的作用域,包含兩個值session和statement,其中session選項表示本地緩存在整個session都有效,而statement只能在一條語句中有效(這條語句有嵌套查詢--nested query/select)。
下面分析一下mybatis本地緩存的實現原理。
本地緩存是在Executor內部構建,Executor包含了三個實現類,SimpleExecutor,BatchExecutor以及CachingExecutor,其中CachingExecutor是開啟了二級緩存才會用到的,這里主要是SimpleExecutor和BatchExecutor,他們都實現了BaseExecutor,而BaseExecutor中正是進行了一級緩存的處理。
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache; // 一級緩存,實質就是一個HashMap<Object, Object>
protected PerpetualCache localOutputParameterCache; // 出參一級緩存,當statment為callable的時候使用
}
在BaseExecutor中定義了一個PerpetualCache類型的localCache屬性,用來保存一級緩存
而PerpetualCache類的主要功能如下:
public class PerpetualCache implements Cache {
private final String id; // 該緩存的id
private final Map<Object, Object> cache = new HashMap<>();
// ...其他一些獲取緩存數據、移除緩存數據的方法
}
其中包含了兩個屬性,id表示緩存的唯一標識,cache是一個HashMap類型的對象,里面存放所有已經緩存的數據
也就是是說Mybatis的一級緩存實質就是一個HashMap。
再回過頭看一看BaseExecutor中的一級緩存處理過程(下述中的代碼片段都是BaseExecutor類中的,不會再把類加上了):
- select添加緩存
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);
// 根據cachekey執行查詢
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
首先創建緩存鍵key,然后根據key再查詢。
下面代碼展示了根據key進行查詢的邏輯
@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;
if (list != null) { // 緩存中拿到了,處理輸出參數
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
先看第二個if--如果當前的查詢語句設置了清除緩存的屬性為true,那么就要把一級緩存清除
當然里面還需要滿足queryStack==0的條件,這個條件涉及到了嵌套查詢(nested select/query),如果是嵌套查詢的最外層查詢(第一個查詢),才進行緩存的清理動作,否則不進行。這里的queryStack是查詢的層級,取決於nested select的層數,例如一個Blog有一個Author,一個Author有一個Account,其中Author和Account都使用了嵌套查詢,並且不是延遲加載(fetchType設置),那么Author查詢的時候queryStack就會是1,Account查詢的時候queryStack為2。針對嵌套查詢這里就說這么多,后續會專門寫一篇嵌套查詢原理的文章,包括非延遲加載以及延遲加載的不同情況的處理方式。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
下面代碼片段展示了從緩存中取數據的邏輯
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
``
直接調用localCache的getObject方法,但是需要在resultHandler不為null的情況,因為如果查詢數據是傳入了ResultHandler,那么會返回null,數據由ResultHandler進行處理。
如果緩存中查到了數據,那么會處理緩存的出參(出參只有在MappedStatement類型為Callable時才會有,其他的STATEMENT/PREPAREDSTATMENT都沒有)
如果沒有查到數據,那么從數據庫中查詢
```java
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
數據查詢完了之后執行queryStack--操作。
進入queryFromDatabase方法進行分析:
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);
// 對於callable的statement來說,出參也需要緩存,而出參也是放在了入參中
// 因此這里緩存了入參
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
主要步驟:
- 先在一級緩存中設置一個占位符,EXECUTION_PLACEHOLDER,
此處代碼的作用就是為了防止嵌套查詢是查詢了相同的數據
舉個例子,一個Blog有一個Author,而Author中又嵌套了一個Blog,那么Blog還沒有放到緩存中,但是嵌套查詢現在查Author,Author中的Blog又是第一個Blog查詢的數據,這里放置一個占位符就是為了說明,這個Blog已經在查詢了,結果還沒出來而已,不要急,等結果出來了再進行配對。 - 執行子類的doQuery方法,查詢數據
- 刪除緩存占位、將查詢出的數據放入到緩存中。
- 如果此查詢語句是CALLABLE類型的,那么要把出參也緩存
以上四部做完之后從數據庫中查詢數據就結束了,其中第一步可能有些人還是很困惑,大家可以執行一些測試看一看。
再次將思路返回到query方法中,
if (queryStack == 0) { // 最外層的查詢已經結束
// 所有非延遲的嵌套查詢也已經查完了,那么就可以把嵌套查詢的結果放入到需要的對象中
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
當最外層查詢結束時,需要執行一些清理動作:
- 執行所有嵌套查詢的連接操作,上面例子中的Blog->Author->Blog,會把author中的Blog設置正確
- 清除嵌套查詢
- 如果當前語句的一級緩存作用域是statement的話,要把一級緩存清空
上面的第一步和第二部需要結合ResultSetHandler共同分析,后面分析嵌套查詢的時候再做詳細的介紹,這里大家心中有個了解即可。
至此,查詢過程的緩存處理就已經結束了
下面簡單看一下cleanLocalCache方法
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
也很簡單,就是把localCache和localOutputParameterCache置空。
接下來就分析update(其中insert/update/delete都統稱為update)時,一級緩存如何處理:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 先清除緩存
clearLocalCache();
// 使用子類的doUpdate方法
return doUpdate(ms, parameter);
}
``
先把緩存清空,然后調用子類的doUpdate執行具體的更新操作
另外事務的提交以及回滾都會清空以及緩存,代碼如下:
```java
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache(); // 清除緩存
flushStatements();
if (required) {
transaction.commit();
}
}
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache(); // 清理緩存
flushStatements();
if (required) {
transaction.commit();
}
}
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache(); // 清理緩存
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
因此,在一個sqlSession執行了commit或者rollback方法后,一級緩存已經沒有了數據,如果再次執行相同的查詢操作,那么會重新從數據庫中查詢。
一級緩存需要注意的事項:
在實際開發中,有可能對查詢數據進行一些操作,比如修改一些字段,或者一個列表中刪除/添加一些數據,再次執行相同的查詢,返回的不會是數據庫中的數據,而是經過修改的數據,因此最好不要對Mybatis返回的數據進行修改操作。