@(MyBatis)[Cache] MyBatis源碼分析——Cache構建以及應用 SqlSession使用緩存流程 如果開啟了二級緩存,而Executor會使用CachingExecutor來裝飾,添加緩存功能,該CachingExecutor會從MappedStatement中獲取對應的Cac ...
@(MyBatis)[Cache]
MyBatis源碼分析——Cache構建以及應用
SqlSession使用緩存流程
如果開啟了二級緩存,而Executor會使用CachingExecutor來裝飾,添加緩存功能,該CachingExecutor會從MappedStatement中獲取對應的Cache來使用。(註:MappedStatement中有保存相關聯的Cache)
在使用SqlSession向DB查詢數據時,如果開啟了二級緩存,則會優先從二級緩存中獲取數據,沒有命中的話才會去查詢一級緩存,此時,一級緩存也沒有命中,則才會真正的去資料庫查詢數據。
沒有命中緩存
下圖為開啟了二級緩存的查詢數據時序圖,其中忽略了二級緩存事務的處理(見下麵二級緩存詳細說明)。
命中二級緩存
命中一級緩存
緩存鍵,CacheKey
下麵為CacheKey的主要核心代碼,省略了部分代碼。在MyBatis中,是通過幾個條件來判斷是否同一條Sql的。
判斷條件:
- Statement ID
- 結果範圍
- Sql
- 所有的入參
在上面的條件中,對於需要使用JDBC查詢出相同結果的來說,需要是同一條Sql以及該Sql的入參條件。
在查詢數據之前,會先創建CacheKey,在BaseExecutor.createCacheKey
中實現:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor was closed.");
CacheKey cacheKey = new CacheKey();
// StatementId, 即用於映射Mapper中的具體Sql的ID
cacheKey.update(ms.getId());
// 結果集範圍,在資料庫查詢出來的結果中進行過濾,並非是物理分頁。
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 具體執行的Sql
cacheKey.update(boundSql.getSql());
// 入參變數值
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
ParameterMapping parameterMapping = parameterMappings.get(i);
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);
}
}
return cacheKey;
}
CacheKey的實現:
這裡將所有需要判斷相等的條件都放入List中,並且更新這些條件計算出校驗值和hashCode,這是為了加快比較的速度。因為只有在校驗值以及HashCode相等的情況下,才會去逐一地判斷每個條件是否相等。
public class CacheKey implements Cloneable, Serializable {
// 預設擴展因數
private static final int DEFAULT_MULTIPLYER = 37;
// 預設HashCdoe基值
private static final int DEFAULT_HASHCODE = 17;
private 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>();
}
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
// 如果對象為數組,則根據每個數組的元素來進行計算
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
doUpdate(object);
}
}
// 計算HashCode和checksum
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
// 擴展因數*當前的哈希值 + 對象的哈希值*擴大倍數
hashcode = multiplier * hashcode + baseHashCode;
// 添加到對比條件中
updateList.add(object);
}
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;
// 只有上面的檢驗條件都相等的情況下,才對每個條件逐一對比
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null)
return false;
} else {
if (!thisObject.equals(thatObject))
return false;
}
}
return true;
}
}
一級緩存
一級緩存直接採用PerpetualCache
來實現,預設為SESSION範圍
刷新時機
- SESSION範圍緩存失效時刻:
- SqlSession關閉,則會釋放緩存
- 提交或者回滾的時候會清空對應的一級緩存。
- 在更新操作的時候,則直接清空對應的一級緩存
- 手動調用清空緩存操作
- STATEMENT範圍刷新緩存
無論是查詢還是更新,在執行完Sql的時候都會清空對應的一級緩存。
二級緩存
在MyBatis中,Cache都通過CachingExecutor內的TransactionalCacheManager來管理Cache,每個Cache都會使用TransactionalCache來裝飾,即緩存是事務性質的,需要手動通過commit或者SqlSession的close來實現真正的將執行結果反應到Cache中,因為二級緩存是屬於全局的,會有可能涉及到多個Cache的添加或者刪除操作。
構建二級緩存
MapperBuilderAssistant.useNewCache
調用構造CacheBuilder
來構建Cache,並且將構造出來的cache註入到MappedStatement
中。CacheBuilder
以Builder設計模式實現,而緩存的功能添加則是通過裝飾者模式來實現。
下麵為CacheBuilder
構建Cache的部分代碼:
public Cache build() {
// 設置預設底層實現Cache,預設如果沒有提供則為PerpetualCache
setDefaultImplementations();
// 創建基類,用於最底層的Cache實現
Cache cache = newBaseCacheInstance(implementation, id);
// 設置Cache屬性
setCacheProperties(cache);
// 只有PerpetualCache才使用裝飾類添加功能,自定義的Cache不添加
if (PerpetualCache.class.equals(cache.getClass())) {
// 使用裝飾類包裝
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 設置給定的裝飾類
cache = setStandardDecorators(cache);
}
return cache;
}
// 根據給定的Cache以及待裝飾實例,創建裝飾類
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
try {
return cacheConstructor.newInstance(base);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
}
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果開啟了定時,則使用ScheduledCache裝飾
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// 讀寫功能,則需要序列化裝飾
if (readWrite) {
cache = new SerializedCache(cache);
}
// 預設會有日誌以及同步
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
二級緩存刷新時機示例
配置:
關閉一級cache,僅僅開啟二級Cache
<setting name="localCacheScope" value="STATEMENT"/>
不手動commit
public static void main(String args[]) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(true);
ProductMapper productMapper = session.getMapper(ProductMapper.class);
productMapper.queryAll();
productMapper.queryAll();
}
輸出結果:
可以看到,當沒有手動提交,並且是同一個session時,前一次執行的結果並沒有刷到緩存,兩次緩存的命中率均為0
2016-07-26 11:04:53 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.0
2016-07-26 11:04:53 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Preparing: select * from products
2016-07-26 11:04:53 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Parameters:
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
<== Total: 14
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.0
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Preparing: select * from products
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Parameters:
2016-07-26 11:04:54 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
<== Total: 14
手動commit
比上面多了一步,手動commit,刷新到緩存。
// 註:此處關閉了一級cache,僅僅開啟了二級cache
public static void main(String args[]) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 此處設置了自動提交,但是那是JDBC中Connection的自動提交
SqlSession session = sqlSessionFactory.openSession(true);
ProductMapper productMapper = session.getMapper(ProductMapper.class);
List<Product> list = productMapper.queryAll();
// 這裡比上面多操作一步,手動提交
session.commit();
productMapper.queryAll();
productMapper.queryAll();
}
輸出結果:
可以看到下麵的二級Cache命中率,第一次沒有數據,故為0,第二次命中,變為0.5
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.0
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Preparing: select * from products
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
==> Parameters:
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug()]:
<== Total: 14
2016-07-26 11:05:18 [DEBUG]-[Thread: main]-[org.apache.ibatis.cache.decorators.LoggingCache.getObject()]:
Cache Hit Ratio [com.jabnih.analysis.mybatis.mapper.ProductMapper]: 0.5
MyBatis緩存使用註意點
在使用緩存的時候,需要註意如果數據緩存在本地,另一個系統修改資料庫時,會出現臟數據問題。
一級緩存
Myatis的一級緩存預設為SESSION,而且由於底層採用PerpetualCache
來實現,該類直接使用HashMap
,並沒有進行一些限制處理。
- 在MyBatis看來,SqlSession一般都是生命周期比較短的,當關閉的時候會釋放緩存,但是如果使用SqlSession多次進行查詢大量的數據時,會將數據緩存,那麼有可能會導致OOM記憶體溢出。
二級緩存
MyBatis雖然全局配置開啟緩存,但是還是取決於是否使用了<cache>
標簽,如果使用了二級緩存,需要註意:
- 每個
<cache>
代表一個單獨的二級緩存,如果多個Mapper需要共用同一個二級緩存,則需要使用<cache-ref>
- 如果一個Mapper中查詢數據時,使用了多表聯查,則,當另一個Mapper更新相關數據時,如果沒有共用一個Cache,那麼下一次該Mapper查詢時,就會出現讀到臟數據。