Mybatis一二級緩存的理解

    頻繁的數據庫操做是很是耗費性能的(主要是由於對於DB而言,數據是持久化在磁盤中的,所以查詢操做須要經過IO,IO操做速度相比內存操做速度慢了好幾個量級),尤爲是對於一些相同的查詢語句,徹底能夠把查詢結果存儲起來,下次查詢一樣的內容的時候直接從內存中獲取數據便可,這樣在某些場景下能夠大大提高查詢效率。算法

  • MyBatis的緩存分爲兩種:

  1. 一級緩存,一級緩存是SqlSession級別的緩存,對於相同的查詢,會從緩存中返回結果而不是查詢數據庫
  2. 二級緩存,二級緩存是Mapper級別的緩存,定義在Mapper文件的<cache>標籤中並須要開啓此緩存,多個Mapper文件能夠共用一個緩存,依賴<cache-ref>標籤配置

    

SqlSession>DefaultSqlSession(selectList)>this.executor.query>Executor(一級緩存BaseExecutor, 二級緩存CachingExecutor)sql

CacheKey判斷兩次查詢條件是否一致。數據庫

 
一級緩存:/executor/BaseExecutor.class
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;
                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 i$ = this.deferredLoads.iterator();
​
                while(i$.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();
                    deferredLoad.load();
                }
​
                this.deferredLoads.clear();
                if(this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }
​
            return list;
        }
    }

 

一級緩存三個結論:

  1. MyBatis的一級緩存是SqlSession級別的,可是它並不定義在SqlSessio接口的實現類DefaultSqlSession中,而是定義在DefaultSqlSession的成員變量Executor中,Executor是在openSession的時候被實例化出來的,它的默認實現爲SimpleExecutor
  2. MyBatis中的一級緩存,與有沒有配置無關,只要SqlSession存在,MyBastis一級緩存就存在,localCache的類型是PerpetualCache,它其實很簡單,一個id屬性+一個HashMap屬性而已,id是一個名爲"localCache"的字符串,HashMap用於存儲數據,Key爲CacheKey,Value爲查詢結果
  3. MyBatis的一級緩存查詢的時候默認都是會先嚐試從一級緩存中獲取數據的,可是咱們看第6行的代碼作了一個判斷,ms.isFlushCacheRequired(),即想每次查詢都走DB也行,將<select>標籤中的flushCache屬性設置爲true便可,這意味着每次查詢的時候都會清理一遍PerpetualCache,PerpetualCache中沒數據,天然只能走DB。增刪改沒有一級緩存。
 

//緩存判斷查詢條件是否一致:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());//判斷id屬性是否相同
            cacheKey.update(Integer.valueOf(rowBounds.getOffset()));//判斷Offset屬性是否相同
            cacheKey.update(Integer.valueOf(rowBounds.getLimit()));//判斷Limit屬性是否相同
            cacheKey.update(boundSql.getSql());//判斷sql屬性是否相同
            List parameterMappings = boundSql.getParameterMappings();//後面都是判斷參數是否相同
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
​
            for(int i = 0; i < parameterMappings.size(); ++i) {
                ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                if(parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    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 = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
​
                    cacheKey.update(value);
                }
            }
​
            return cacheKey;
        }
    }

 

 
 
 

一級緩存從四組共五個條件判斷兩次查詢是相同的:

  1. <select>標籤所在的Mapper的Namespace+<select>標籤的id屬性
  2. RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認爲0,limit默認爲Integer.MAX_VALUE
  3. <select>標籤中定義的sql語句
  4. 輸入參數的具體參數值,一個int值就update一個int,一個String值就update一個String,一個List就輪詢裏面的每一個元素進行update

即只要兩次查詢知足以上三個條件且沒有定義flushCache="true",那麼第二次查詢會直接從MyBatis一級緩存PerpetualCache中返回數據,而不會走DB。緩存

假如定義了MyBatis二級緩存,那麼MyBatis二級緩存讀取優先級高於MyBatis一級緩存

MyBatis二級緩存的生命週期即整個應用的生命週期,應用不結束,定義的二級緩存都會存在在內存中。mybatis

從這個角度考慮,爲了不MyBatis二級緩存中數據量過大致使內存溢出,MyBatis在配置文件中給咱們增長了不少配置例如size(緩存大小)、flushInterval(緩存清理時間間隔)、eviction(數據淘汰算法)來保證緩存中存儲的數據不至於太過龐大。app

 

二級緩存:/executor/CachingExecutor.class

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();//Cache是從MappedStatement中獲取到的,而MappedStatement又和每個<insert>、<delete>、<update>、<select>綁定並在MyBatis啓動的時候存入Configuration中:
        if(cache != null) {
            this.flushCacheIfRequired(ms);//根據flushCache=true或者flushCache=false判斷是否要清理二級緩存
            if(ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, parameterObject, boundSql);//保證MyBatis二級緩存不會存儲存儲過程的結果
                List list = (List)this.tcm.getObject(cache, key);//tcm裝飾器模式,建立一個事物緩存TranactionalCache,持有Cache接口,Cache接口的實現類就是根據咱們在Mapper文件中配置的<cache>建立的Cache實例
                if(list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }//若是沒有從MyBatis二級緩存中拿到數據,那麼就會查一次數據庫,而後放到MyBatis二級緩存中去
return list;
            }
        }
//優先讀取以上二級緩存,query方法優先讀取默認實現的一級緩存。
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
 

 

 
      
 
 

MyBatis支持三種類型的二級緩存:性能

  • MyBatis默認的緩存,type爲空,Cache爲PerpetualCache
  • 自定義緩存
  • 第三方緩存
 
開啓二級緩存:
<settings>//mybatis.cfg.xml
        <!-- 開啓二級緩存  默認值爲true -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
<mapper namespace="cn.sxt.vo.user.mapper">//mapper.xml
    <!-- 開啓本mapper namespace下的二級緩存 -->
    <cache></cache>
 
 
存在問題:
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;

對於tableA與tableB的操做定義在兩個Mapper中,分別叫作MapperA與MapperB,即它們屬於兩個命名空間,若是此時啓用緩存:ui

  1. MapperA中執行上述sql語句查詢這6個字段
  2. tableB更新了col1與col2兩個字段
  3. MapperA再次執行上述sql語句查詢這6個字段(前提是沒有執行過任何insert、delete、update操做)

此時問題就來了,即便第(2)步tableB更新了col1與col2兩個字段,第(3)步MapperA走二級緩存查詢到的這6個字段依然是原來的這6個字段的值,由於咱們從CacheKey的3組條件來看:this

  1. <select>標籤所在的Mapper的Namespace+<select>標籤的id屬性
  2. RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認爲0,limit默認爲Integer.MAX_VALUE
  3. <select>標籤中定義的sql語句

對於MapperA來講,其中的任何一個條件都沒有變化,天然會將原結果返回。spa

這個問題對於MyBatis的二級緩存來講是一個無解的問題,所以使用MyBatis二級緩存有一個前提:

必須保證全部的增刪改查都在同一個命名空間下才行

相關文章
相關標籤/搜索