1. 程式人生 > 其它 >mybaits原始碼分析(五) 一級快取、二級快取最詳細講解

mybaits原始碼分析(五) 一級快取、二級快取最詳細講解

技術標籤:mybaits一級快取、二級快取mybaits二級快取mybaits一級快取

mybaits原始碼分析(五) 一級快取、二級快取詳解

前言:上一篇講解了mybaits資料來源,這一篇講解一下mybaits一級快取、二級快取的基本使用,以及主要實現。

本篇主要分為下面幾個部分:

一級快取、二級快取的使用及測試

mybaits快取相關類的介紹

一級快取詳解

二級快取詳解

一、一級快取、二級快取的使用及測試

在mybaits中,一級快取是預設開啟的,快取的生命週期是sqlsession級別的,二級快取全域性配置是預設開啟的,但是需要另外在namespace中也開啟才可以使用二級快取,二級快取的生命週期是sqlsessionFactory的,快取操作的範圍是每個mapper對應一個cache(這也是為什麼需要在mapper配置的namespace中開啟才生效的原因)

1、在SqlMapper中加上下面配置, cacheEnabled負責開啟二級快取,logImpl負責列印sql(我們測試可以根據是否列印真實sql來判斷是否走了快取)

	<settings>
        <!--列印執行sql用
	  	<setting name="logImpl" value="STDOUT_LOGGING"/>
	  	<!-- 預設為true -->
	  	<setting name="cacheEnabled" value="true"/>
	</settings>

2、測試

	
	/**
	 * 一級快取測試 :
	 * 	測試前,需要加下面logImpl這個列印sql語句的配置
		 <settings>
		  	<setting name="logImpl" value="STDOUT_LOGGING"/> //  是為了列印sql語句用途
		  	<setting name="cacheEnabled" value="true"/> // 二級快取預設就是開啟的
		</settings>
		  測試結果,會發出一次sql語句,一級快取預設開啟,快取生命週期是SqlSession級別
	 */
	@Test
	public void test2() throws Exception {
		InputStream in = Resources.getResourceAsStream("custom/sqlMapConfig3.xml");
		SqlSessionFactory factory2 =  new SqlSessionFactoryBuilder().build(in);
		SqlSession openSession = factory2.openSession();
		UserMapper mapper = openSession.getMapper(UserMapper.class);
		User user1 = mapper.findUserById(40); // 會發出sql語句
		System.out.println(user1);
		
		User user2 = mapper.findUserById(40); // 會發出sql語句
		System.out.println(user2);
		
		openSession.close();
	}

	/**
	 * 二級快取測試
	 * 二級快取全域性配置預設開啟,但是需要每個名稱空間配置<cache></cache>,
	 * 即需要全域性和區域性同時配置,快取生命週期是SqlSessionFactory級別。
	 */
	@Test
	public void test3() throws Exception {
		InputStream in = Resources.getResourceAsStream("custom/sqlMapConfig3.xml");
		SqlSessionFactory factory2 =  new SqlSessionFactoryBuilder().build(in);
		SqlSession openSession = factory2.openSession();
		UserMapper mapper = openSession.getMapper(UserMapper.class);
		User user1 = mapper.findUserById(40);
		System.out.println(user1);
		
		openSession.close(); // 關閉session
		
		openSession = factory2.openSession();
		mapper = openSession.getMapper(UserMapper.class);
		
		User user3 = mapper.findUserById(40); // 二級快取全域性和區域性全部開啟才會列印sql
		System.out.println(user3);
	}
	

從測試結果可以看到,mybaits在沒有開啟二級快取的時候,一個sqlsession相同的查詢再次執行,在沒有close的情況下,會查一級快取,在二級快取開啟的情況下,如果close後,還是能夠使用快取(使用的是二級快取),快取機制是:第一次查詢,會先查二級快取,沒有找一級快取,再沒有查資料庫,資料庫查出來先放到一級快取,再放到二級快取,第二次查詢時,會先查二級快取,而二級快取有貨,就返回了。

二、mybaits快取相關類的介紹

mybaits一級快取二級快取他們的頂級介面都是一樣的,都是cache類,而預設mybaits的cache實現是一個hashMap的包裝,另外附帶很多對cache類實現的包裝。

1、下面先看看頂級介面的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();

}

其中除PerpetualCache以外,其他的Cache實現都是使用了裝飾器模式,由底層的PerpetualCache完成實際的快取,並在此基礎上添加了其他功能。

SynchronizedCache:通過在get/put方式中加鎖,保證只有一個執行緒操作快取

FifoCacheLruCache:當快取到達上限時候,通過FIFO或者LRU(最早進入記憶體)策略刪除快取 (這二個可以在namespace開始<cache>時配置快取失效策略時用途)

ScheduledCache:在進行get/put/remove/getSize等操作前,判斷快取時間是否超過了設定的最長快取時間(預設是一小時),如果是則清空快取--即每隔一段時間清空一次快取

SoftCache/WeakCache:通過JVM的軟引用和弱引用來實現快取,當JVM記憶體不足時,會自動清理掉這些快取

TranscationCache: 事務化的包裝,即一次對此快取的操作,不會馬上更新到delegate快取中,會把操作分成移除和新增維護到map容器,待commit方法被呼叫,真實操作快取。

2、快取實現類的主要實現邏輯

1)PerpetualCache

public class PerpetualCache implements Cache {

  private String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

PerpetualCache 就是一個map快取,這個是mybaits預設的快取實現,一級快取使用的就是這個類,如果二級快取想使用第三方cache,都有現成的jar包可以使用,這裡不進行敘述。

2)FifoCache

FifoCache是一個實現快取先進先出的快取包裝,其實現原理是利用LinkedList先進先出的機制。

private final Cache delegate;
  private LinkedList<Object> keyList;
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<Object>();
    this.size = 1024;
  }

 public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);
  }
  // 主要實現就是新增快取的時候,順便新增到list中,這樣如果新增前,判斷list的尺寸大於size
  // 就移除list中的第一個,並且delegate快取也移除。
  private void cycleKeyList(Object key) {
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

3)LruCache

LruCache是實現了LRU淘汰機制的快取包裝,其主要原理就是利用,LinkList的三個引數構造。

new LinkedHashMap<Object, View>(size, 0.75f, true),其中第三個引數accessOrder的作用是如果元素被訪問的情況下,是否把元素新增到連結串列的尾部。結合LinkedHashMap的一個protected的removeEldestEntry方法,可以實現LRU(即最久沒訪問的移除)。

 	private MyCache delegate;
	private Map<Object, Object> keyMap;

	public LRUCache(MyCache delegate) {
		super();
		this.delegate = delegate;
		setSize(1024);
	}

	private void setSize(int initialCapacity) {
		keyMap = new LinkedHashMap<Object, Object>(initialCapacity, 0.75f, true) {
			private static final long serialVersionUID = 4267176411845948333L;
			protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
				boolean tooBig = size() > initialCapacity; // 大於尺寸
				if (tooBig) { // ture 移除delegate快取
					delegate.removeObject(eldest.getKey());
				}
				return tooBig; // 返回true會自動移除LinkHashMap的快取
			}
		};
	}


   // 其他操作的時候KeyMap進行同步

4)、SynchronizedCache

SynchronizedCache就是同步加鎖的一個包裝,這個就簡單了,即對快取狀態變更有依賴的實現方法全部加鎖。

@Override
	public synchronized void putObject(Object key, Object value) {
		delegate.putObject(key, value);
	}

	@Override
	public synchronized Object getObject(Object key) {
		return delegate.getObject(key);
	}

	@Override
	public synchronized Object removeObject(Object key) {
		return delegate.removeObject(key);
	}

5)、TranscationCache

TranscationCache的包裝就是內部維護了二個map,一個map裝remove快取的操作,一個map裝put快取的操作,待commit的時候,遍歷二個map,執行快取住的操作,如果reset的時候,就清空這二個臨時map。下面此cahce的成員變數,和二個內部類(這二個內部類就是包裝快取操作的)。

	/**
	 * 對需要新增元素的包裝,並且傳入了delegate快取,呼叫此commit就可以用delegate快取put進新增元素
	 */
	private static class AddEntry {
		private Object key;
		private Object value;
		private MyCache delegate;
		public AddEntry(Object key, Object value, MyCache delegate) {
			super();
			this.key = key;
			this.value = value;
			this.delegate = delegate;
		}
		public void commit() {
			this.delegate.putObject(key, value);
		}
	}

	/**
	 * 對需要移除的元素的包裝,並且傳入了delegate快取,呼叫此commit就可以用delegate移除元素。
	 */
	private static class RemoveEntry {
		private Object key;
		private MyCache delegate;
		public RemoveEntry(Object key, MyCache delegate) {
			super();
			this.key = key;
			this.delegate = delegate;
		}

		public void commit() {
			this.delegate.removeObject(key);
		}
	}
    // 包裝的快取
	private MyCache delegate;
    // 待新增元素的map
	private Map<Object, AddEntry> entriesToAddOnCommit;
	// 待移除元素的map
	private Map<Object, RemoveEntry> entriesToRemoveOnCommit;

下面是核心的實現

// 新增的時候,操作的是二個map,既然新增,那麼臨時remove的map需要remove這個key
	@Override
	public void putObject(Object key, Object value) {
		this.entriesToRemoveOnCommit.remove(key);
		this.entriesToAddOnCommit.put(key, new AddEntry(key, value, delegate));
	}

	@Override
	public Object getObject(Object key) {
		return this.delegate.getObject(key);
	}
    
	// 移除的時候,操作的是二個map,既然移除,那麼臨時add的map需要remove這個可以。
	@Override
	public Object removeObject(Object key) {
		this.entriesToAddOnCommit.remove(key);
		this.entriesToRemoveOnCommit.put(key, new RemoveEntry(key, delegate));
		return this.delegate.getObject(key); // 這裡是為了獲得返回值,注意不能用removeObject
	}

	@Override
	public void clear() {
		this.delegate.clear();
		reset();
	}
	
	// 提交
	public void commit() {
		delegate.clear(); // delegate移除
		for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
			entry.commit(); // 移除remove裡的元素
		}
		for (AddEntry entry : entriesToAddOnCommit.values()) {
			entry.commit(); // 新增add裡的元素
		}
		reset();
	}

	public void rollback() {
		reset();
	}

3、快取key的實現

mybaits快取的key是基於一個CacheKey的類實現的,其核心機制如下:

* 快取Key的實現機制:通過update方法,計算hashcode,並且新增到內部的list集合,判斷是否相同,也是依據內部list元素全部相同。
* 建立mybaits的CacheKey的機制:同樣語句、內部分頁引數offset、內部分頁引數limit、預編譯sql語句、引數對映。

/**
	 * CacheKey的組裝
	 */
	public static void main(String[] args) {
		String mappedStatementId = "MappedStatementId"; // 用字串描述mybaits的cachekey的元素。
		String rowBounds_getOffset = "rowBounds_getOffset";
		String rowBounds_getLimit = "rowBounds_getLimit";
		String buondSql_getSql = "buondSql_getSql";
		List<String> parameterMappings = new ArrayList<>();
		parameterMappings.add("param1");
		parameterMappings.add("param2");
		
		CacheKey cacheKey = new CacheKey(); // 建立CacheKey
		 
		cacheKey.update(mappedStatementId); // 新增元素到CacheKey
		cacheKey.update(rowBounds_getOffset);
		cacheKey.update(rowBounds_getLimit);
		cacheKey.update(buondSql_getSql);
		cacheKey.update(parameterMappings);
		System.out.println(cacheKey); 
	}

三、一級快取詳解

上面我們已經對快取的類介面和其實現已經瞭解了,現在可以探究下一級快取在mybaits中是怎麼使用的了。

說一級快取前,我們回顧下,sqlsession實際上是呼叫Executor進行操作的,而BaseExecutor是Executor的基本實現,它有其他幾個實現,我們用的是SimpleExecutor,另外還有一個

CachingExecutor是實現二級快取的。我們先從看建立session時的快取快取建立,然後BaseExecutor開始看一級快取。

1、快取建立過程(快取建立是在建立SqlSession的時候,我們直接從SqlSsession的openSessionFromDataSource看起)

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  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型別建立不同的Executor
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction); // 預設的Executor
    }
    if (cacheEnabled) { // 如果開啟全域性二級快取
      executor = new CachingExecutor(executor); // 就建立CachingExecutor
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

上面雖然是Executor的建立過程,實際上就是一級快取的建立過程,一級快取就是在Executor一個Cache的成員變數。二級快取是實現是由CachingExecutor對Executor的包裝,後續在詳細分析。

2、查詢入口 BaseExecutor.query開始分析

SqlSession的所有查詢都是呼叫的Executor的query方法實現,其實現類BaseExecutor的程式碼如下:

  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); // 快取key建立
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

這個query主要是建立快取key(上面已經講過建立邏輯),和取出sql。再看query的呼叫的過載query方法。

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 
    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(); //處理迴圈引用?
      }
      deferredLoads.clear(); 
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // 如果是statement的本地快取,就直接清除!
      }
    }
    return list; // 取到返回
  }

在這個query方法,就是先取出本地快取localCache(這個就是PerpetualCache),如果找到就返回,沒有找到就查資料庫方法queryFromDatabase。另外一級快取可以配置成statement範圍,即每次查詢都會清除本地快取。我們再看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); // 查詢出來放入一級快取
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

另外如果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();
    return doUpdate(ms, parameter);
  }

四、二級快取的實現

上面說了二級快取主要是靠CachingExecutor的包裝,那麼我們直接分析這個類就可以瞭解二級快取了。

1、CachingExecutor成員和TransactionalCacheManager詳解

public class CachingExecutor implements Executor {

  private Executor delegate;
  private TransactionalCacheManager tcm = new TransactionalCacheManager(); // TransactionalCache的管理類

TransactionalCacheManager:用於管理CachingExecutor使用的二級快取物件,只定義了一個transactionalCaches欄位

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

它的key是CachingExecutor使用的二級快取物件,value是對應的TransactionalCache物件,下面看下它的實現。

public class TransactionalCacheManager {
  
  // 裝未包裝快取和包裝成Transaction快取的map對映
  private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  // 操作快取多了一個Cache引數,實際上是呼叫Transaction的對應方法
  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  
  // 全部快取commit
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
  // 全部快取rollback
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }
  // 建立一個TransactionalCache,並把原cache為key放入map維護
  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

2、二級快取快取邏輯

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, parameterObject, 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. Query must be not synchronized to prevent deadlocks
        }
        return list; // 有就返回
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

上面就是簡單的先查詢二級快取,如果有就返回,沒有就查BaseExcute的doquery (其是先查一級快取,找到返回,沒有查資料庫,查資料庫時放回到一級快取)。

其他更新、commit等操作會清除二級快取 (這個等於是一個簡單實現,等於如果只有查詢,二級快取是一直有效,如果有更新,就要清空所有的二級快取)。

另外:

二級快取的Cache,預設是每個namespace共享一個Cache的,這個Cache的實現,肯定也是用LRUCache等和SynchronizedCache包裝過的。

這個是在 Cache cache = ms.getCache(); 這個斷點檢視的,具體怎麼配置包裝,可到Mapper.xml解析的相關流程中分析。

五:一級、二級快取使用總結

1、一級快取配置:

從程式碼可以看出,一級快取是預設開啟的,並沒有任何設定或判斷語句控制是否執行一級快取查詢、新增操作。所以,無法關閉掉一級快取。

2、二級快取配置:

二級快取的配置有三個地方:

a、全域性快取開關,mybatis-config.xml

<settings><setting name="cacheEnabled" value="true"/></settings>

b、各個namespace下的二級快取例項 mapper.xml

<cache/>或引用其它namespace的快取<cache-ref namespace="com.someone.application.data.SomeMapper"/>

c、<select>節點中配置useCache屬性

預設為true,設定false時,二級快取針對該條select語句不會生效

3、快取範圍

a、一級快取範圍

一級快取的範圍是可以配置的:

<settings><setting name="localCacheScope" value="STATEMENT"/></settings>

範圍選項有:SESSION和STATEMENT,預設值為SESSION

SESSION:這種情況下會快取一個會話(SqlSession)中執行的所有查詢

STATEMENT:本地會話僅用在語句執行上,在完成一次查詢後就會清空掉一級快取。

b、二級快取範圍

二級快取範圍是namespace,即同一個namespace下的多個SqlSession共享同一個快取

4、使用建議

不建議使用二級快取,二級快取是名稱空間範圍共享的,生命週期雖然是sqlsesisonFacotry,但是任何更新都會清空所有二級快取,另外二級快取在連表查詢時也是存在問題的,(比如你連線了不是此名稱空間的表,那個表的資料被更改,此名稱空間的二級快取是不知道的),以及其他一些問題,所以不建議使用。

一級快取在一些極端情況下,可能存在髒資料,使用建議一個是改成STATEMENT範圍,另外一個就是不要在業務邏輯上用一個sqlsession重複的查詢同樣的一個語句。

end!