1. 程式人生 > 程式設計 >Mybatis之快取分析

Mybatis之快取分析

前言

快取可以說是提升效能的標配,作業系統,cpu,各種各樣的框架我們總能看到快取的身影,當然Mybatis也不例外,Mybatis提供了強大的快取功能,分別有一級快取和二級快取,接下來我們來做一一介紹。

快取配置

在深入之前我們先看看Mybatis都提供了哪些快取的配置,方便開發者使用,可以大致歸為三類配置,下面分別詳細說明:

1.setting配置

setting配置中包含了相關快取的配置有:cacheEnabled和localCacheScope;
cacheEnabled:全域性地開啟或關閉配置檔案中的所有對映器已經配置的任何快取,預設值true;
localCacheScope:MyBatis 利用本地快取機制(Local Cache)防止迴圈引用(circular references)和加速重複巢狀查詢。 預設值為 SESSION,這種情況下會快取一個會話中執行的所有查詢。 若設定值為 STATEMENT,本地會話僅用在語句執行上,對相同 SqlSession 的不同呼叫將不會共享資料,預設為session。

2.statement配置

XML對映檔案包括select標籤和insert/update/delete標籤兩類;select標籤包括flushCache和useCache,另外三個只有useCache:
flushCache:將其設定為true後,只要語句被呼叫,都會導致本地快取和二級快取被清空,預設值:false;
useCache:將其設定為true後,將會導致本條語句的結果被二級快取快取起來,預設值:對select元素為true。

3.cache標籤

XML對映檔案可以包含cache和cache-ref兩類標籤:
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 元素來引用另一個快取。

快取測試

1.預設配置

預設是沒有開啟二級快取的,只有一級快取,並且快取範圍是SESSION,flushCache為false語句被呼叫,不會導致本地快取;

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中配置,同樣執行以上的程式,結果如下:

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