mybatis原始碼學習:一級快取和二級快取分析
阿新 • • 發佈:2020-04-26
[toc]
前文傳送門:[mybatis原始碼學習:從SqlSessionFactory到代理物件的生成](https://www.cnblogs.com/summerday152/p/12773121.html)
# 零、一級快取和二級快取的流程
> 以這裡的查詢語句為例。
## 一級快取總結
- 以下兩種情況會直接在一級快取中查詢資料
- 主配置檔案或對映檔案沒有配置二級快取開啟。
- 二級快取中不存在資料。
- 根據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中的值也就是儲存在快取中的物件。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175757601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
而通過裝飾器設計模式,將Cache的功能進行加強,在它的實現類中有著明顯的體現:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175811663.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
PerpetualCache:是最基礎的快取類,採用HashMap實現,同時一級快取使用的localCache就是該型別。
LruCache:Lru(least recently used),採用Lru演算法可以實現移除最長時間沒有使用的key/value。
SerializedCache:提供了序列化功能,將值序列化後存入快取,用於快取返回一份例項的Copy,保證執行緒安全。
LoggingCache:提供日誌功能,如果開啟debugEnabled為true,則列印快取命中日誌。
SynchronizedCache:同步的Cache,用synchronized關鍵字修飾所有方法。
> 下圖可以得知其執行鏈:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175830150.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
# 二、cache標籤解析原始碼
XMLMapperBuilder中的configurationElement負責解析mappers對映檔案中的標籤元素,其中有個cacheElement方法,負責解析cache標籤。
```java
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方法,這個方法的作用就是獲取指定的屬性值,如果沒有設定的話,就採用預設的值:
```java
public String getStringAttribute(String name, String def) {
//獲取name引數對應的屬性
String value = attributes.getProperty(name);
if (value == null) {
//如果沒有設定,預設為def
return def;
} else {
return value;
}
}
```
resolveAlias方法,從原始碼中我們就可以猜測,我們之前通過` `起別名其實也就是將裡面的內容解析,並存入map之中,而每次處理型別的時候,都比較的是小寫的形式,這也是我們起別名之後不用關心大小寫的原因。
```java
// throws class cast exception as well if types cannot be assigned
public Class resolveAlias(String string) {
try {
if (string == null) {
return null;
}
//首先將傳入的引數轉換為小寫形式
String key = string.toLowerCase(Locale.ENGLISH);
Class value;
//到TypeAliasRegistry維護的Map,TYPE_ALIASES中找有無對應的鍵
if (TYPE_ALIASES.containsKey(key)) {
//找到就直接返回:class類物件
value = (Class) TYPE_ALIASES.get(key);
} else {
//找不到就通過反射獲取一個
value = (Class) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
```
根據獲取的屬性,通過裝飾器模式,層層裝飾,最後建立了一個SynchronizedCache,並新增到configuration中。因此我們可以知道,一旦我們在對映檔案中設定了``,就會建立一個SynchronizedCache快取物件。
```java
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。
```java
@Override
public List 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物件的詳細資訊:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175903908.png)
CacheKey物件的CreateKey操作:
- 首先建立一個cachekey,預設hashcode=17,multiplier=37,count=0,updateList初始化。
- update操作:count++,對checksum,hashcode進行賦值,最後將引數新增到updatelist中。
```java
//根據傳入資訊,建立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 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. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)`)
```java
//主配置檔案已經開啟二級快取
@Override
public List 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 list = (List) tcm.getObject(cache, key);
if (list == null) {
//快取中沒有就執行查詢,BaseExecutor的query
list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//存入快取
tcm.putObject(cache, key, list); // issue #578 and #116
}
//如果快取中有,就直接返回
return list;
}
}
//對映檔案沒有開啟二級快取,需要進行查詢,delegate其實還是Executor物件
return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
```
> 除了select操作之外,其他的的操作都會清空二級快取。XMLStatementBuilder中配置屬性的時候:`boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);`
```java
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
//tcm後面會總結,清空二級快取
tcm.clear(cache);
}
}
```
# 四、二級快取TransactionCache
這裡學習一下二級快取涉及的快取類:TransactionCache,同樣也是基於裝飾者設計模式,對傳入的Cache進行裝飾,構建二級快取事務緩衝區:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175922957.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
CachingExecutor維護了一個TransactionCacheManager,即tcm,而這個tcm其實維護的就是一個key為Cache,value為TransactionCache包裝過的Cache。而`tcm.getObject(cache, key)`的意思我們可以通過以下原始碼得知:
```java
public Object getObject(Cache cache, CacheKey key) {
//將傳入的cache包裝為TransactionalCache,並根據key獲取值
return getTransactionalCache(cache).getObject(key);
}
```
需要注意的是,getObject方法中將會把獲取值的職責一路向後傳遞,直到最基礎的perpetualCache,根據cachekey獲取。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175945160.png)
最終獲取到的值,如果為null,就需要把key加入未命中條目的快取。
```java
@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中。
```java
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
```
也就是說,一定需要某種手段才能讓他真正地存入快取,沒錯了,commit是可以的:
```java
//CachingExecutor.java
@Override
public void commit(boolean required) throws SQLException {
//清除本地快取
delegate.commit(required);
//呼叫tcm.commit
tcm.commit();
}
```
最終呼叫的是TransactionCache的commit方法:
```java
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
```
最後的最後,我們可以看到將剛才的未命中和待提交的資料都進行了相應的處理,這才是最終影響二級快取中資料的操作,當然這中間也存在著職責鏈,就不贅述了。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419175959649.png)
當然,除了commit,close也是一樣的,因為最終呼叫的其實都是commit方法,同樣也會操作快取。
# 五、二級快取測試
```xml
```
```xml
```
```java
/**
* 測試二級快取
*/
@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));
}
```
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419180009239.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
二級快取實現了SqlSession之間快取資料的共享,是mapper對映級別的快取。
有時快取也會帶來資料讀取正確性的問題,如果資料更新頻繁,會導致從快取中讀取到的資料並不是最新的,可以關閉二級快取。
# 六、一級快取原始碼解析
主配置檔案或對映檔案沒有配置二級快取開啟,或者二級快取中不存在資料,最終都會執行BaseExecutor的query方法,如果queryStack為空或者不是select語句,就會先清空本地的快取。
```java
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
```
檢視本地快取(一級快取)是否有資料,如果有直接返回,如果沒有,則呼叫queryFromDatabase從資料庫中查詢。
```java
list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
//處理儲存過程
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//從資料庫中查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
```
判斷本地快取的級別是否為STATEMENT級別,如果是的話,清空快取,因此STATEMENT級別的一級快取無法共享localCache。
```java
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
```
# 七、測試一級快取
```java
/**
* 測試一級快取
*/
@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));
}
```
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200419180021970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
一級快取預設是sqlSession級別地快取,insert|delete|update|commit()和close()的操作的執行都會清空一級快取。
怎麼說呢,分析原始碼的過程讓我對Mybatis有了更加深刻的認識,可能有些理解還是沒有很到位,或許是經驗不足,很多東西還是浮於表面,但一翻debug下來,看到自己之前一個又一個的迷惑被非常確切地解開,真的爽!
[https://www.jianshu.com/p/c553169c5921](https://www.jianshu.com/p/c5531