Mybatis之緩存分析

前言

緩存能夠說是提高性能的標配,操做系統,cpu,各類各樣的框架咱們總能看到緩存的身影,固然Mybatis也不例外,Mybatis提供了強大的緩存功能,分別有一級緩存和二級緩存,接下來咱們來作一一介紹。java

緩存配置

在深刻以前咱們先看看Mybatis都提供了哪些緩存的配置,方便開發者使用,能夠大體歸爲三類配置,下面分別詳細說明:git

1.setting配置

setting配置中包含了相關緩存的配置有:cacheEnabled和localCacheScope;
cacheEnabled:全局地開啓或關閉配置文件中的全部映射器已經配置的任何緩存,默認值true;
localCacheScope:MyBatis 利用本地緩存機制(Local Cache)防止循環引用(circular references)和加速重複嵌套查詢。 默認值爲 SESSION,這種狀況下會緩存一個會話中執行的全部查詢。 若設置值爲 STATEMENT,本地會話僅用在語句執行上,對相同 SqlSession 的不一樣調用將不會共享數據,默認爲session。github

2.statement配置

XML映射文件包括select標籤和insert/update/delete標籤兩類;select標籤包括flushCache和useCache,另外三個只有useCache:
flushCache:將其設置爲true後,只要語句被調用,都會致使本地緩存和二級緩存被清空,默認值:false;
useCache:將其設置爲true後,將會致使本條語句的結果被二級緩存緩存起來,默認值:對select元素爲true。redis

3.cache標籤

XML映射文件能夠包含cache和cache-ref兩類標籤:
cache:對給定命名空間的緩存配置,要啓用全局的二級緩存,只須要在你的SQL映射文件中添加一行<cache/>,固然裏面也包含一些自定義的屬性,以下完整的配置:sql

<cache 
        blocking="true" 
        eviction="FIFO" 
        flushInterval="60000"
        readOnly="true" 
        size="512" 
        type="org.apache.ibatis.cache.impl.PerpetualCache">
    </cache>

eviction:清除策略常見的有:LRU,FIFO,SOFT,WEAK;默認的清除策略是LRU;
flushInterval:(刷新間隔)屬性能夠被設置爲任意的正整數,設置的值應該是一個以毫秒爲單位的合理時間量。 默認狀況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新;
size:(引用數目)屬性能夠被設置爲任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。默認值是 1024;
readOnly:(只讀)屬性能夠被設置爲 true 或 false。只讀的緩存會給全部調用者返回緩存對象的相同實例。 所以這些對象不能被修改。這就提供了可觀的性能提高。而可讀寫的緩存會(經過序列化)返回緩存對象的拷貝。 速度上會慢一些,可是更安全,所以默認值是 false;
blocking:當在緩存中找不到元素時,它設置對緩存鍵的鎖定;這樣其餘線程將等待此元素被填充,而不是命中數據庫;
type:指定緩存器類型,能夠自定義緩存;數據庫

cache-ref:對某一命名空間的語句,只會使用該命名空間的緩存進行緩存或刷新。 但你可能會想要在多個命名空間中共享相同的緩存配置和實例。要實現這種需求,你可使用 cache-ref 元素來引用另外一個緩存。apache

緩存測試

1.默認配置

默認是沒有開啓二級緩存的,只有一級緩存,而且緩存範圍是SESSION,flushCache爲false語句被調用,不會致使本地緩存;segmentfault

public static void main(String[] args) throws IOException {
        String resource = "mybatis-config-sourceCode.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper<Blog> mapper = session.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlog(160));
            // 默認開啓一級緩存,在參數和sql相同的狀況下,只執行一次sql
            System.out.println(mapper.selectBlog(160));
        } finally {
            session.close();
        }
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper<Blog> mapper = session2.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlog(160));
        } finally {
            session.close();
        }
    }

分別建立了2個session,第一個session連續查詢了兩次,第二session查詢了一次結果以下:緩存

com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@13c9d689

由於開啓了一級緩存而且緩存範圍是SESSION,因此session1的兩次查詢返回同一個對象;而不一樣的session2返回了不一樣的對象;安全

2.flushCache爲true

一樣執行以上的程序,結果以下:

com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@13c9d689
com.mybatis.vo.Blog@4afcd809

由於設置了只要語句被調用,都會致使本地緩存,因此獲取的對象都是不一樣的;

3.localCacheScope設置爲STATEMENT

一樣執行以上的程序,結果以下:

com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@13c9d689
com.mybatis.vo.Blog@4afcd809

設置值爲STATEMENT,本地會話僅用在語句執行上,對相同SqlSession的不一樣調用將不會共享數據,因此獲取的對象都是不一樣的;

4.cacheEnabled設置爲false

一樣執行以上的程序,結果以下:

com.mybatis.vo.Blog@6771beb3
com.mybatis.vo.Blog@6771beb3
com.mybatis.vo.Blog@411f53a0

能夠發現此配置對一級緩存並不起做用,只做用於二級緩存;

5.配置cache標籤

在xxMapper.xml中配置<cache />,一樣執行以上的程序,結果以下:

com.mybatis.vo.Blog@292b08d6
com.mybatis.vo.Blog@292b08d6
com.mybatis.vo.Blog@24313fcc

爲何已經設置了二級緩存,獲取的對象仍是不同;主要緣由是cache中默認的readOnly屬性爲false,也就是說會返回緩存對象的拷貝,全部這裏對象不一致,但其實並無再次查詢數據庫;再次設置readOnly屬性以下所示:

<cache readOnly="true"/>

再次運行,能夠發現全部對象都是同一個,結果以下:

com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@6c6cb480

注:須要注意的是若是設置readOnly=true須要注意其餘線程修改此對象值,進而影響當前線程的對象值,由於全部線程都是共享的同一個對象,若是設置爲false那麼其餘線程獲取的對象都是拷貝,不會影響當前線程數據。
映射語句文件中的全部insert、update和delete語句會刷新緩存,在session2查詢以前執行更新操做以下:

SqlSession session21 = sqlSessionFactory.openSession();
try {
    BlogMapper mapper = session21.getMapper(BlogMapper.class);
    Blog blog = new Blog();
    blog.setId(158);
    blog.setTitle("hello java new");
    mapper.updateBlog(blog);
    session21.commit();
} finally {
    session21.close();
}

再次運行,緩存已經被清除,獲取新的對象,結果以下:

com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@4b2bac3f

6.配置cache屬性blocking="true"

blocking=true的狀況下其餘線程將等待此元素被填充,而不是命中數據庫;能夠簡單作個測試,首先不設置blocking,而後分別建立兩個線程分別查詢selectBlog:

new Thread(new Runnable() {
        @Override
        public void run() {
              ...selectBlog...
       }
}).start();
new Thread(new Runnable() {
        @Override
        public void run() {
              ...selectBlog...
        }
}).start();

結果以下,每一個線程都查詢了一次數據庫,這樣若是是很費時的sql,起不到緩存的做用:

com.mybatis.vo.Blog@4eebc002
com.mybatis.vo.Blog@354d6d02

注:若是以上查詢出來是兩個相同的結果,能夠增長相關查詢的sql時間,這樣效果更加明顯;修改配置設置blocking="true",配置以下:

<cache readOnly="true" blocking="true" />

再次運行結果以下:

com.mybatis.vo.Blog@f9ecfca
com.mybatis.vo.Blog@f9ecfca

能夠發現對象是同一個,說明只查詢了一次數據庫;

緩存分析

上節中對一級緩存和二級緩存經過實例測試的方式,詳細結束瞭如何使用,以及注意點;本節從源碼入手,更加深刻的瞭解mybatis的緩存機制;

1.緩存類型

Mybatis提供了緩存接口類Cache,具體以下所示:

public interface Cache {
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  ReadWriteLock getReadWriteLock();
}

提供了put,get,remove操做,同時還提供了清除,獲取大小,獲取讀寫鎖功能;基於此接口Mybatis提供了多個實現類,具體以下圖所示:
image.png
FifoCache,LruCache,SoftCache,WeakCache:這四個是能夠在cache標籤裏面配置的策略eviction默認爲LruCache;

  • LRU– 最近最少使用:移除最長時間不被使用的對象。
  • FIFO– 先進先出:按對象進入緩存的順序來移除它們。
  • SOFT– 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。
  • WEAK– 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。

其餘的緩存是不能看成緩存策略來配置的,他們主要被用來看成以上四種策略的補充,配合四種策略使用的:
BlockingCache:在咱們設置blocking="true"時會自動使用此緩存,用來防止多個線程同時執行相同sql,查詢屢次數據庫的問題;
LoggingCache:爲緩存提供日誌功能的;
SerializedCache:當緩存有讀寫功能的時候,提供序列化功能;
ScheduledCache:若是配置了刷新間隔flushInterval,提供檢查是否到刷新時間;
SynchronizedCache:提供同步功能synchronized關鍵字;
PerpetualCache:提供緩存最基本,最純粹的功能,內置HashMap保存數據;能夠說以上配置的四種策略都由此類提供保存功能;一級緩存就是直接使用此類;
TransactionalCache:提供事務管理機制;

2.一級緩存

Mybatis默認開啓一級緩存,使用的是PerpetualCache做爲緩存工具類,內部就是一個最簡單的HashMap,使用CacheKey做爲Map的key,value就是查詢處理的數據;相關功能能夠參考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 (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;
  }

首先斷定是否開啓了flushCache開關,以前說過若是開啓了開關,則每次查詢都會清除緩存,可是這裏加了另一個條件,必須queryStack==0的狀況下;從名字能夠大體猜想出就是查詢的堆棧深度,查詢以前+1,結束以後-1;爲何須要等於0,大概猜想一下多是爲了防止在查詢的過程當中,有其餘線程進來直接把緩存給清掉了;
而後把queryStack+1,而且只有在沒有設置resultHandler的狀況下才會從本地緩存裏面獲取值,不然不會從緩存獲取,直接查詢數據庫;結束時queryStack-1,本次查詢結束,queryStack歸0;
最後一樣是在queryStack==0的狀況下處理延遲加載,以及緩存範圍若是是STATEMENT,則清除緩存數據;
總結一下:一級緩存是默認開啓的,也沒有開關對其進行關閉,惟一的兩個參數分別是localCacheScope和flushCache用來控制刪除緩存,固然session關閉的時候也會清除緩存;另一個問題就是爲何本地緩存沒有引入刪除策略好比lru等,可能仍是由於session的生命週期比較短,關閉session便可刪除緩存。

3.二級緩存

上面咱們介紹到cacheEnabled==true的狀況下才會開啓二級緩存,默認爲true;在configuration中會建立Executor,以下所示:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    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);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

executorType在configuration中設置,以下所示:

<!-- 執行器類型:SIMPLE,REUSE.BATCH -->
<setting name="defaultExecutorType" value="SIMPLE" />

若是沒有設置executorType,默認爲ExecutorType.SIMPLE;能夠看到建立完Executor以後會判斷cacheEnabled是否爲true,只有爲true纔會建立CachingExecutor,此類是專門用來處理二級緩存的;固然並非設置了cacheEnabled就開啓了二級緩存,還必須設置cache標籤,否則一樣不會開啓二級緩存;具體看CachingExecutor中的查詢功能:

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) {
      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.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

首先獲取緩存實現類,若是沒有配置cache標籤,這裏獲取的實現類就爲null,因此就直接查詢數據庫;若是不爲null,則先判斷是否開啓了flushCache功能,能夠發現此功能不只用在一級緩存,一樣用在二級緩存,若是設置爲則直接清除緩存;
接下來會判斷select標籤是否開啓了useCache功能,默認是開啓的;同時還須要沒有設置resultHandler,這一點和本地緩存同樣;
最後就是查詢數據而後放入緩存中,這裏並無直接用獲取的Cache實現類去get/put操做,而是外層有一個包裝緩存類TransactionalCache,也就是默認開啓了事務;

下面看一下是如何獲取Cache的,這個主要和xxxMapper.xml中配置的cache標籤有關;Cache實現類在MapperBuilderAssistant中實現,具體以下:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

能夠發現這裏設置的參數基本和cache標籤裏面設置的一致:
implementation:對應cache標籤中的type,若是沒有設置默認爲PerpetualCache;
addDecorator:對應cache標籤中的eviction,也就是清除策略,默認是LruCache;
clearInterval:對應cache標籤中的flushInterval,默認狀況是不設置,也就是沒有刷新間隔;
readWrite:對應cache標籤中的readOnly,默認爲false,支持讀寫功能;
blocking:對應cache標籤中的blocking,默認爲false;

最後執行build方法,具體代碼以下:

public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }
  
    private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

首先newBaseCacheInstance,這裏默認的implementation其實就是PerpetualCache,固然如何這裏指定了自定義的緩存類型,就直接返回用戶自定義的類型了;若是沒有指定那麼繼續往下會newCacheDecoratorInstance,這裏的decorators就是配置的eviction,默認是LruCache,同時包含了默認的PerpetualCache;
而後執行setStandardDecorators方法,這個方法其實就是判斷用戶是否配置了相關的參數好比:flushInterval,readOnly,blocking等,每一個新的緩存實例都會包含原來的實例,相似裝飾者模式;具體每一個緩存實例這裏就不過多介紹了,反正就是每一個實現一個功能,最後就是把全部功能過濾一遍,有點像過濾器;

自定義緩存

從上面的內容中咱們能夠知道,能夠在cache標籤中設置type類型,這裏其實就能夠指定自定義的緩存類型了;而且咱們在分析二級緩存源碼的時候若是type類型不是PerpetualCache實現類,那麼就不會有下面的setStandardDecorators,直接返回用戶自定義的緩存,不少功能就沒有了,因此自定義緩存仍是要當心謹慎;
固然簡單實現一個自定義的緩存仍是比較簡單的,實現接口Cache便可;好比咱們經常使用的redis,Memcached,EhCache等作緩存,其實也能夠經過擴展做爲Mybatis的二級緩存,Mybatis官方也提供了實現:二級緩存擴展,咱們只須要引入jar便可:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-memcached</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

引入相關jar包之後,只須要在Cache標籤中配置type類型便可:

type="org.mybatis.caches.memcached.MemcachedCache"
type="org.mybatis.caches.redis.RedisCache"
type="org.mybatis.caches.ehcache.EhcacheCache"

總結

本文首先介紹了Mybatis緩存的相關配置項,一一介紹;而後經過改變各類參數進行一一驗證,並從源碼層面進行分析重點分析了一級緩存,二級緩存;最後介紹了自定義緩存,以及官方提供的一下擴展緩存實現。

示例代碼地址

Github

相關文章
相關標籤/搜索