mybatis原始碼詳細解析(2)---- 一級,二級快取

Smile ~ 莫失莫離發表於2020-12-22

Mybatis快取的作用

每當我們使用 MyBatis 開啟一次和資料庫的會話,MyBatis 會建立出一個 SqlSession 物件表示一次資料庫會話。

在對資料庫的一次會話中,我們有可能會反覆地執行完全相同的查詢語句,如果不採取一些措施的話,每一次查詢都會查詢一次資料庫,而我們在極短的時間內做了完全相同的查詢,那麼它們的結果極有可能完全相同,由於查詢一次資料庫的代價很大,這有可能造成很大的資源浪費。

為了解決這一問題,減少資源的浪費,MyBatis會在表示會話的SqlSession物件中建立一個簡單的快取,將每次查詢到的結果結果快取起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從快取中直接將結果取出,返回給使用者,不需要再進行一次資料庫查詢了。

當初始化執行器的時候會預設建立快取器(CachingExecutor):

在這裡插入圖片描述

在這裡插入圖片描述
上一篇的程式中:

session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);

點開之後發現呼叫的是selectList();

在這裡插入圖片描述

繼續點進去

在這裡插入圖片描述
這裡是建立快取key, 這個 key 是一級快取 HashMap 的 key,由 MappedStatement 的 Id、SQL 的 offset、SQL 的 limit、SQL 本身以及 SQL 中的引數 Params 構成的:

Statement Id + Offset + Limmit + Sql + Params

在這裡插入圖片描述

驗證二級快取是否開啟,這裡沒有開啟,所以跳過if中的程式碼,執行delegate.query:

org.apache.ibatis.executor.CachingExecutor#query()

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) {
	//看是否需要清除cache(在xml中可以配置flushCache屬性決定何時清空cache)
        this.flushCacheIfRequired(ms);
//若開啟了cache且resultHandler 為空
        if (ms.isUseCache() && resultHandler == null) {
            this.ensureNoOutParams(ms, parameterObject, boundSql);
//從TransactionalCacheManager中取cache
            List<E> list = (List)this.tcm.getObject(cache, key);
//若取出來list是空的
            if (list == null) {
//查詢資料庫
                list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//將結果存入cache中
                this.tcm.putObject(cache, key, list);
            }

            return list;
        }
    }

    return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

查詢一級快取:

org.apache.ibatis.executor.BaseExecutor#query()

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 (this.closed) {  //一級快取預設開啟
        throw new ExecutorException("Executor was closed.");
    } else {
        if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
            this.clearLocalCache();
        }

        List list;
        try {
            ++this.queryStack;
            list = resultHandler == null ? (List)this.localCache.getObject(key) : null;  //第一次會從本地快取通過key獲取值
            if (list != null) { //如果不為空,則從本地獲取
                this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else { //如果為空,則查詢資料庫
                list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            --this.queryStack;
        }

        if (this.queryStack == 0) {
            Iterator var8 = this.deferredLoads.iterator();

            while(var8.hasNext()) {
                BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                deferredLoad.load();
            }
            this.deferredLoads.clear();
            if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                this.clearLocalCache();
            }
        }
        return list;
    }
}

因此,mybatis的快取是先驗證二級快取,再驗證一級快取

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER); //首先設定了一個預設值放入localCache
    List list;
    try {
        list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql); //執行查詢
    } finally {
        this.localCache.removeObject(key); //刪除原來的值
    }

    this.localCache.putObject(key, list); //將查詢到的值放到快取中
    if (ms.getStatementType() == StatementType.CALLABLE) {
        this.localOutputParameterCache.putObject(key, parameter);
    }
    return list;  //返回查詢的結果
}

這個方法,首先設定了一個預設值放入localCache,查詢完成後刪除key值,然後將查詢結果list放入快取localCache

所以當第二次執行查詢操作時,發現key值相同,就會到一級快取中去查詢,這樣就會出現日誌中查詢兩次,但是隻會執行一次資料庫操作的現象了。

並且當commit或者rollback的時候會清除快取,並且當執行insert、update、delete的時候也會清除快取。

相關原始碼:

org.apache.ibatis.executor.BaseExecutor#update

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        //刪除一級快取
        this.clearLocalCache();
        return this.doUpdate(ms, parameter);
    }
}


 org.apache.ibatis.executor.BaseExecutor#commit
 
 public void commit(boolean required) throws SQLException {
    if (this.closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
        //刪除一級快取
        this.clearLocalCache();
        this.flushStatements();
        if (required) {
            this.transaction.commit();
        }

    }
}

org.apache.ibatis.executor.BaseExecutor#rollback

public void rollback(boolean required) throws SQLException {
    if (!this.closed) {
        try {
            //刪除一級快取
            this.clearLocalCache();
            this.flushStatements(true);
        } finally {
            if (required) {
                this.transaction.rollback();
            }

        }
    }

}

區別:

儲存結構, 一級快取是存在記憶體 Map 中。二級快取儲存介質多樣,可在記憶體、硬碟中,需要進行序列化和反序列化;

範圍, 一級快取是 sqlSession 級別的快取,不同的sqlSession之間的快取是共享的,每個 SqlSession 都會建立一個 Executor,每個 Executor 都有一個一級快取 LocalCache,二級快取是namespace級別,跨 SqlSession 的;

失效場景, 一級、二級快取都是在執行插入、更新、刪除時會失效,需要重新從資料庫獲取,避免髒讀。另外一級快取不能用於分散式場景,二級快取需要使用 redis 來實現;

相關文章