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方式中加鎖,保證只有一個執行緒操作快取
FifoCache、LruCache:當快取到達上限時候,通過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解析的相關流程中分析。
五:一級、二級快取使用總結
從程式碼可以看出,一級快取是預設開啟的,並沒有任何設定或判斷語句控制是否執行一級快取查詢、新增操作。所以,無法關閉掉一級快取。
二級快取的配置有三個地方:
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語句不會生效
a、一級快取範圍
一級快取的範圍是可以配置的:
<settings><setting name="localCacheScope" value="STATEMENT"/></settings>
範圍選項有:SESSION和STATEMENT,預設值為SESSION
SESSION:這種情況下會快取一個會話(SqlSession)中執行的所有查詢
STATEMENT:本地會話僅用在語句執行上,在完成一次查詢後就會清空掉一級快取。
b、二級快取範圍
二級快取範圍是namespace,即同一個namespace下的多個SqlSession共享同一個快取
4、使用建議
不建議使用二級快取,二級快取是名稱空間範圍共享的,生命週期雖然是sqlsesisonFacotry,但是任何更新都會清空所有二級快取,另外二級快取在連表查詢時也是存在問題的,(比如你連線了不是此名稱空間的表,那個表的資料被更改,此名稱空間的二級快取是不知道的),以及其他一些問題,所以不建議使用。
一級快取在一些極端情況下,可能存在髒資料,使用建議一個是改成STATEMENT範圍,另外一個就是不要在業務邏輯上用一個sqlsession重複的查詢同樣的一個語句。
end!