前文傳送門:mybatis原始碼學習:從SqlSessionFactory到代理物件的生成
零、一級快取和二級快取的流程
以這裡的查詢語句為例。
一級快取總結
-
以下兩種情況會直接在一級快取中查詢資料
- 主配置檔案或對映檔案沒有配置二級快取開啟。
- 二級快取中不存在資料。
-
根據statetment,生成一個CacheKey。
-
判斷是否需要清空本地快取。
-
根據cachekey從localCache中獲取資料。
-
如果快取未命中,走接下來三步並向下
- 從資料庫查詢結果。
- 將cachekey:資料存入localcache中。
- 將資料返回。
-
如果快取命中,直接從快取中獲取資料。
-
localCache的範圍如果為statement,清空一級快取。
二級快取總結
-
判斷主配置檔案是否設定了enabledCache,預設是開啟的,建立CachingExecutor。
-
根據statetment,生成一個CacheKey。
-
判斷對映檔案中是否有cache標籤,如果沒有則跳過以下針對二級快取的操作,從一級快取中查,查不到就從資料庫中查。
-
否則即開啟了二級快取,獲取cache。
-
判斷是否需要清空二級快取。
-
判斷該語句是否需要使用二級快取isUserCache。
-
如果二級快取命中,則直接返回該資料。
-
如果二級快取未命中,則將cachekey存入未命中set,然後進行一下的操作:
- 從一級快取中查,如果命中就返回,沒有命中就從資料庫中查。
- 將查到的資料返回,並將cachekey和資料(物件的拷貝)存入待加入二級快取的map中。
-
最後commit和close操作都會使二級快取真正地更新。
一、快取介面Cache及其實現類
快取類的頂級介面Cache,裡面定義了加入資料到快取,從快取中獲取資料,清楚快取等操作,通常mybatis會將namespace作為id,將CacheKey作為Map中的鍵,而map中的值也就是儲存在快取中的物件。
而通過裝飾器設計模式,將Cache的功能進行加強,在它的實現類中有著明顯的體現:
PerpetualCache:是最基礎的快取類,採用HashMap實現,同時一級快取使用的localCache就是該型別。
LruCache:Lru(least recently used),採用Lru演算法可以實現移除最長時間沒有使用的key/value。
SerializedCache:提供了序列化功能,將值序列化後存入快取,用於快取返回一份例項的Copy,保證執行緒安全。
LoggingCache:提供日誌功能,如果開啟debugEnabled為true,則列印快取命中日誌。
SynchronizedCache:同步的Cache,用synchronized關鍵字修飾所有方法。
下圖可以得知其執行鏈:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache
二、cache標籤解析原始碼
XMLMapperBuilder中的configurationElement負責解析mappers對映檔案中的標籤元素,其中有個cacheElement方法,負責解析cache標籤。
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//獲取type屬性,預設為perpetual
String type = context.getStringAttribute("type", "PERPETUAL");
//獲取type類物件
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//獲取eviction策略,預設為lru,即最近最少使用,移除最長時間不被使用的物件
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
//獲取flushInterval重新整理間隔
Long flushInterval = context.getLongAttribute("flushInterval");
//獲取size引用數目
Integer size = context.getIntAttribute("size");
//獲取是否只讀
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
//獲取是否blocking
boolean blocking = context.getBooleanAttribute("blocking", false);
//這一步是另外一種設定cache的方式,即cache子元素中用property,name,value定義
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
getStringAttribute方法,這個方法的作用就是獲取指定的屬性值,如果沒有設定的話,就採用預設的值:
public String getStringAttribute(String name, String def) {
//獲取name引數對應的屬性
String value = attributes.getProperty(name);
if (value == null) {
//如果沒有設定,預設為def
return def;
} else {
return value;
}
}
resolveAlias方法,從原始碼中我們就可以猜測,我們之前通過</typeAliases>
起別名其實也就是將裡面的內容解析,並存入map之中,而每次處理型別的時候,都比較的是小寫的形式,這也是我們起別名之後不用關心大小寫的原因。
// throws class cast exception as well if types cannot be assigned
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
//首先將傳入的引數轉換為小寫形式
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
//到TypeAliasRegistry維護的Map,TYPE_ALIASES中找有無對應的鍵
if (TYPE_ALIASES.containsKey(key)) {
//找到就直接返回:class類物件
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
//找不到就通過反射獲取一個
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
根據獲取的屬性,通過裝飾器模式,層層裝飾,最後建立了一個SynchronizedCache,並新增到configuration中。因此我們可以知道,一旦我們在對映檔案中設定了<cache>
,就會建立一個SynchronizedCache快取物件。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//把當前的namespace當作快取的id
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();
//將cache加入configuration
configuration.addCache(cache);
currentCache = cache;
return cache;
}
三、CacheKey快取項的key
預設情況下,enabledCache的全域性設定是開啟的,所以Executor會建立一個CachingExecutor,以查詢為例,當執行Executor實現類的時候,會獲取boundsql,並根據當前資訊建立快取項的key。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//從MappedStatement中獲取boundsql
BoundSql boundSql = ms.getBoundSql(parameterObject);
//Cachekey類表示快取項的key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
每一個SqlSession中持有了自己的Executor,每一個Executor中有一個Local Cache。當使用者發起查詢時,Mybatis會根據當前執行的MappedStatement生成一個key,去Local Cache中查詢,如果快取命中的話,返回。如果快取沒有命中的話,則寫入Local Cache,最後返回結果給使用者。
boundsql物件的詳細資訊:
CacheKey物件的CreateKey操作:
- 首先建立一個cachekey,預設hashcode=17,multiplier=37,count=0,updateList初始化。
- update操作:count++,對checksum,hashcode進行賦值,最後將引數新增到updatelist中。
//根據傳入資訊,建立chachekey
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
//執行器關閉就丟擲異常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//建立一個cachekey,預設hashcode=17,multiplier=37,count=0,updateList初始化
CacheKey cacheKey = new CacheKey();
//新增操作:sql的id,邏輯分頁偏移量,邏輯分頁起始量,sql語句。
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//引數名
String propertyName = parameterMapping.getProperty();
//根據引數名獲取值
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//新增引數值
cacheKey.update(value);
}
}
//新增environment的id名,如果它不為空的話
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
//返回cachekey
return cacheKey;
}
所以快取項的key最後表示為:hashcode:checknum:遍歷updateList,以:間隔
。
2020122321:657338105:com.smday.dao.IUserDao.findById:0:2147483647:select * from user where id = ?:41:mysql
。
接著,呼叫同類中的query方法,針對是否開啟二級快取做不同的決斷。(需要注意的是,這一部分是建立在cacheEnabled設定為true的前提下,當然預設是true。如果為false,Executor將會建立BaseExecutor,並不會判斷mappers對映檔案中二級快取是否存在,而是直接執行delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)
)
//主配置檔案已經開啟二級快取
@Override
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) {
//如果cache不為空,且需要清快取的話(insert|update|delete),執行tcm.clear(cache);
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) {
//快取中沒有就執行查詢,BaseExecutor的query
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//存入快取
tcm.putObject(cache, key, list); // issue #578 and #116
}
//如果快取中有,就直接返回
return list;
}
}
//對映檔案沒有開啟二級快取,需要進行查詢,delegate其實還是Executor物件
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
除了select操作之外,其他的的操作都會清空二級快取。XMLStatementBuilder中配置屬性的時候:
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
//tcm後面會總結,清空二級快取
tcm.clear(cache);
}
}
四、二級快取TransactionCache
這裡學習一下二級快取涉及的快取類:TransactionCache,同樣也是基於裝飾者設計模式,對傳入的Cache進行裝飾,構建二級快取事務緩衝區:
CachingExecutor維護了一個TransactionCacheManager,即tcm,而這個tcm其實維護的就是一個key為Cache,value為TransactionCache包裝過的Cache。而tcm.getObject(cache, key)
的意思我們可以通過以下原始碼得知:
public Object getObject(Cache cache, CacheKey key) {
//將傳入的cache包裝為TransactionalCache,並根據key獲取值
return getTransactionalCache(cache).getObject(key);
}
需要注意的是,getObject方法中將會把獲取值的職責一路向後傳遞,直到最基礎的perpetualCache,根據cachekey獲取。
最終獲取到的值,如果為null,就需要把key加入未命中條目的快取。
@Override
public Object getObject(Object key) {
//根據職責一路向後傳遞
Object object = delegate.getObject(key);
if (object == null) {
//沒找到值就將key存入未命中的set
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
如果快取中沒有找到,將會從資料庫中查詢,查詢到之後,將會進行新增操作,也就是:tcm.putObject(cache, key, list);
。我們可以發現,其實它並沒有直接將資料加入快取,而是將資料新增進待提交的map中。
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
也就是說,一定需要某種手段才能讓他真正地存入快取,沒錯了,commit是可以的:
//CachingExecutor.java
@Override
public void commit(boolean required) throws SQLException {
//清除本地快取
delegate.commit(required);
//呼叫tcm.commit
tcm.commit();
}
最終呼叫的是TransactionCache的commit方法:
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
最後的最後,我們可以看到將剛才的未命中和待提交的資料都進行了相應的處理,這才是最終影響二級快取中資料的操作,當然這中間也存在著職責鏈,就不贅述了。
當然,除了commit,close也是一樣的,因為最終呼叫的其實都是commit方法,同樣也會操作快取。
五、二級快取測試
<!-- 開啟全域性配置 -->
<settings>
<!--全域性開啟快取配置,是預設開啟的-->
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 對映配置檔案 -->
<!--開啟user支援二級快取-->
<cache></cache>
<select id="findById" resultType="user" useCache="true" >
select * from user where id = #{id}
</select>
/**
* 測試二級快取
*/
@Test
public void testFirstLevelCache2(){
SqlSession sqlSession1 = factory.openSession();
IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);
User user1 = userDao1.findById(41);
System.out.printf("==> %s\n", user1);
sqlSession1.commit();
//sqlSession1.close();
SqlSession sqlSession2 = factory.openSession();
IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);
User user2 = userDao2.findById(41);
System.out.printf("==> %s\n", user2);
sqlSession2.close();
System.out.println("user1 == user2:"+(user1 == user2));
SqlSession sqlSession3 = factory.openSession();
IUserDao userDao3 = sqlSession3.getMapper(IUserDao.class);
User user3 = userDao3.findById(41);
System.out.printf("==> %s\n", user3);
sqlSession2.close();
System.out.println("user2 == user3:"+(user2 == user3));
}
二級快取實現了SqlSession之間快取資料的共享,是mapper對映級別的快取。
有時快取也會帶來資料讀取正確性的問題,如果資料更新頻繁,會導致從快取中讀取到的資料並不是最新的,可以關閉二級快取。
六、一級快取原始碼解析
主配置檔案或對映檔案沒有配置二級快取開啟,或者二級快取中不存在資料,最終都會執行BaseExecutor的query方法,如果queryStack為空或者不是select語句,就會先清空本地的快取。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
檢視本地快取(一級快取)是否有資料,如果有直接返回,如果沒有,則呼叫queryFromDatabase從資料庫中查詢。
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);
}
判斷本地快取的級別是否為STATEMENT級別,如果是的話,清空快取,因此STATEMENT級別的一級快取無法共享localCache。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
七、測試一級快取
/**
* 測試一級快取
*/
@Test
public void testFirstLevelCache1(){
SqlSession sqlSession1 = factory.openSession();
IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);
User user1 = userDao1.findById(41);
System.out.printf("==> %s\n", user1);
IUserDao userDao2 = sqlSession1.getMapper(IUserDao.class);
User user2 = userDao2.findById(41);
System.out.printf("==> %s\n", user2);
sqlSession1.close();
System.out.println("user1 == user2:"+(user1 == user2));
}
一級快取預設是sqlSession級別地快取,insert|delete|update|commit()和close()的操作的執行都會清空一級快取。
怎麼說呢,分析原始碼的過程讓我對Mybatis有了更加深刻的認識,可能有些理解還是沒有很到位,或許是經驗不足,很多東西還是浮於表面,但一翻debug下來,看到自己之前一個又一個的迷惑被非常確切地解開,真的爽!