MyBatis框架原理3:快取
如果開啟了二級快取,資料查詢執行過程就是首先從二級快取中查詢,如果未命中則從一級快取中查詢,如果也未命中則從資料庫中查詢。MyBatis的一級和二級快取都是基於Cache介面的實現,下面先來看看Cache介面和其各種實現類。
Cache介面及常用裝飾器
public interface Cache { String getId(); //快取中新增資料,key為生成的CacheKey,value為查詢結果 void putObject(Object key, Object value); //查詢 Object getObject(Object key); //刪除 Object removeObject(Object key); //清空快取 void clear(); //獲取快取數量 int getSize(); //獲取讀寫鎖 ReadWriteLock getReadWriteLock(); }
Cache介面位於MyBatis的cache包下,定義了快取的基本方法,其實現類採用了裝飾器模式,通過實現類的組裝,可以實現操控快取的功能。cache包結構如下:
- PerpetualCache是Cache介面的實現類,通過內部的HashMap來對快取進行基本的操作,通常配合裝飾器類一起使用。
- BlockingCache裝飾器:保證只有一個執行緒到資料庫中查詢指定key的資料,如果該執行緒在BlockingCache中未查詢到資料,就獲取key對應的鎖,阻塞其他查詢這個key的執行緒,通過其內部ConcurrentHashMap來實現,原始碼如下:
public class BlockingCache implements Cache { //阻塞時長 private long timeout; private final Cache delegate; //key和ReentrantLock物件一一對應 private final ConcurrentHashMap<Object, ReentrantLock> locks; @Override public Object getObject(Object key) { //獲取key的鎖 acquireLock(key); //根據key查詢 Object value = delegate.getObject(key); //如果命中快取,釋放鎖,未命中則繼續持有鎖 if (value != null) { releaseLock(key); } return value; } @Override //從資料庫獲取結果後,將結果放入BlockingCache,然後釋放鎖 public void putObject(Object key, Object value) { try { delegate.putObject(key, value); } finally { releaseLock(key); } } ...
- FifoCache裝飾器: 先入先出規則刪除最早的快取,通過其內部的Deque實現。
- LruCache裝飾器: 刪除最近使用最少的快取, 通過內部的LinkedHashMap實現。
- SynchronizedCache裝飾器:同步Cache。
- LoggingCache裝飾器: 提供日誌功能,記錄和輸出快取命中率。
- SerializedCache裝飾器:序列化功能。
CacheKey
CacheKey物件是用來確認快取項的唯一標識,由其內部ArrayList新增的所有物件來確認兩個CacheKey是否相同,通常ArrayList內將新增MappedStatement的id,SQL語句,使用者傳遞給SQL語句的引數以及查詢結果集範圍RowBounds等,CacheKey原始碼如下:
public class CacheKey implements Cloneable, Serializable { ... private final int multiplier; private int hashcode; private long checksum; private int count; private List<Object> updateList; public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<Object>(); } //向updateLis中新增物件 public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); } @Override //重寫equals方法判斷CacheKey是否相同 public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } //比較updateList中每一項 for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; } }
一級快取
一級快取是session級別快取,只存在當前會話中,在沒有任何配置下,MyBatis預設開啟一級快取,當一個SqlSession第一次執行SQL語句和引數查詢時,將生成的CacheKey和查詢結果放入快取中,下一次通過相同的SQL語句和引數查詢時,就會從快取中獲取,當進行更新或者插入操作時,一級快取會進行清空。在上一篇中說到,MayBatis進行一級快取查詢和寫入是由BaseExecutor執行的,原始碼如下:
-
初始化快取:
一級快取是Cache介面的PerpetualCache實現類物件
public abstract class BaseExecutor implements Executor { ... protected PerpetualCache localCache; protected PerpetualCache localOutputParameterCache; protected Configuration configuration; protected int queryStack; private boolean closed; protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>(); //一級快取初始化 this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; } ...
-
生成CacheKey
key在CachingExecutor中生成,CacheKey的updateList中放入了MappedStatement,傳入SQL的引數,結果集範圍rowBounds和boundSql:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
- 將查詢結果和CacheKey放入快取:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; //快取中放入CacheKey和佔位符 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { //在資料庫中查詢操作 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } //快取中放入CacheKey和結果集 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } //返回結果 return list; }
- 再次執行相同查詢條件時從快取獲取結果:
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; }
- 更新操作時清空快取:
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); }
通過以下程式碼驗證下,分別開兩個session進行相同的查詢,第一個session查詢兩次:
public void testSelect() { SqlSession sqlSession = sqlSessionFactory.openSession(); User user = sqlSession.selectOne("findUserById", 1); System.out.println(user); User user2 = sqlSession.selectOne("findUserById", 1); System.out.println(user2); sqlSession.close(); System.out.println("sqlSession closed!==================================="); //新建會話 SqlSession sqlSession2 = sqlSessionFactory.openSession(); User user3 = sqlSession2.selectOne("findUserById", 1); System.out.println(user3); sqlSession2.close(); }
把日誌設定為DEBUG級別得到執行日誌:
DEBUG [main] - ==>Preparing: SELECT * FROM user WHERE id = ? DEBUG [main] - ==> Parameters: 1(Integer) DEBUG [main] - <==Total: 1 User [id=1, username=小明, birthday=null, sex=1, address=四川成都] User [id=1, username=小明, birthday=null, sex=1, address=四川成都] DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d] DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d] DEBUG [main] - Returned connection 369241501 to pool. sqlSession closed!=================================== DEBUG [main] - Opening JDBC Connection DEBUG [main] - Checked out connection 369241501 from pool. DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d] DEBUG [main] - ==>Preparing: SELECT * FROM user WHERE id = ? DEBUG [main] - ==> Parameters: 1(Integer) DEBUG [main] - <==Total: 1 User [id=1, username=小明, birthday=null, sex=1, address=四川成都] DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d] DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d] DEBUG [main] - Returned connection 369241501 to pool.
第一次會話中,雖然查詢了兩次id為1的使用者,但是隻執行了一次SQL,關閉會話後開啟一次新的會話,再次查詢id為1的使用者,SQL再次執行,說明了一級快取只存在SqlSession中,不同SqlSession不能共享。
二級快取
二級快取是Mapper級別快取,也就是同一Mapper下不同的session共享二級快取區域。
只需要在XML對映檔案中增加cache標籤或cache-ref標籤標籤就可以開啟二級快取,cache-ref標籤配置的是共享其指定Mapper的二級快取區域。具體配置資訊如下:
- blocking : 是否使用阻塞快取
- readOnly : 是否只讀
- eviction: 快取策略,可指定Cache介面下裝飾器類FifoCache、LruCache、SoftCache和WeakCache
- flushInterval : 自動重新整理快取時間
- size : 設定快取個數
- type : 設定快取型別,用於自定義快取類,預設為PerpetualCache
二級快取是在MyBatis的解析配置檔案時初始化,在XMLMapperBuilder中將快取配置解析:
private void cacheElement(XNode context) throws Exception { if (context != null) { //指定預設型別為PerpetualCache String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); //預設快取策略為LruCache String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); //委託builderAssistant構建二級快取 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
構建過程:
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) //設定快取型別,預設為PerpetualCache .implementation(valueOrDefault(typeClass, PerpetualCache.class)) //設定快取策略,預設使用LruCache裝飾器 .addDecorator(valueOrDefault(evictionClass, LruCache.class)) //設定重新整理時間 .clearInterval(flushInterval) //設定大小 .size(size) //設定是否只讀 .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; }
最終得到預設的二級快取物件結構為:

CachingExecutor將初始化的Cache物件用TransactionalCache包裝後放入TransactionalCacheManager的Map中,下面程式碼中的tcm就是TransactionalCacheManager物件,CachingExecutor執行二級快取操作過程:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //從Configuration的MappedStatement中獲取二級快取 Cache cache = ms.getCache(); if (cache != null) { //判斷是否需要重新整理快取,SELECT不重新整理,INSERT|UPDATE|DELETE重新整理快取 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") //從二級快取中獲取資料 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { //委託BaseExecutor查詢 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); }
通過之前一級快取的例子驗證二級快取,只需要在UserMapper對映檔案中加入cache標籤,並且讓相關POJO類實現java.io.Serializable介面,執行得到日誌:
DEBUG [main] - ==>Preparing: SELECT * FROM user WHERE id = ? DEBUG [main] - ==> Parameters: 1(Integer) DEBUG [main] - <==Total: 1 User [id=1, username=小明, birthday=null, sex=1, address=四川成都] DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0 User [id=1, username=小明, birthday=null, sex=1, address=四川成都] DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f] DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f] DEBUG [main] - Returned connection 1543974463 to pool. sqlSession closed!=================================== DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333 User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
不同session查詢同一條記錄時,總共只執行了一次SQL語句,並且日誌打印出了快取的命中率,這時候不同session已經共享了二級快取區域。
[1]: https://www.cnblogs.com/abcboy/p/9656302.html