這是我參與8月更文挑戰的第6天,活動詳情查看:8月更文挑戰java
爲了提高持久層數據查詢的性能,MyBatis提供了一套緩存機制,根據緩存的做用範圍可分爲:一級緩存和二級緩存。一級緩存默認是開啓的,做用域爲SqlSession,也稱做【回話緩存/本地緩存】。二級緩存默認是關閉的,須要手動開啓,做用域爲namespace。本篇文章暫且不討論二級緩存,僅從源碼的角度分析一下MyBatis一級緩存的實現原理。 算法
咱們已經知道,SqlSession是MyBatis對外提供的,操做數據庫的惟一接口。當咱們從SqlSessionFactory打開一個新的回話時,一個新的SqlSession實例將被建立。SqlSession內置了一個Executor,它是MyBatis提供的操做數據庫的執行器,當咱們執行數據庫查詢時,最終會調用Executor.query()
方法,它在查詢數據庫前會先判斷是否命中一級緩存,若是命中就直接返回,不然才真的發起查詢操做。 數據庫
咱們直接看Executor.query()
方法,它首先會根據請求參數ParamMap解析出要執行的SQL語句BoundSql,而後建立緩存鍵CacheKey,而後調用重載方法。緩存
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 根據參數解析出要執行的SQL
BoundSql boundSql = ms.getBoundSql(parameter);
// 建立緩存鍵
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 執行查詢
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
複製代碼
所以咱們重點看query重載方法,它首先會向ErrorContext報告本身正在作查詢,而後判斷是否須要清空緩存,若是你在SQL節點配置了flushCache="true"
則不會使用一級緩存。以後就是嘗試從一級緩存中獲取結果,若是命中緩存則直接返回,不然調用queryFromDatabase
查詢數據庫,在queryFromDatabase
方法中,會再將查詢結果存入一級緩存。markdown
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
try {
// 試圖從一級緩存中獲取數據
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);
}
}
return list;
}
複製代碼
所以,咱們重點看localCache對象。 app
localCache是PerpetualCache類的實例,譯爲【永久緩存】,由於它不會主動刪除緩存,可是會在事務提交或執行更新方法時清空緩存。PerpetualCache是緩存接口Cache的子類,分析子類前,咱們應該先看它的接口。 ide
以下是Cache接口,它的職責很簡單,就是維護結果集緩存,對緩存提供了【增刪查】的API。源碼分析
public interface Cache {
// 獲取緩存惟一標識符
String getId();
// 添加緩存項
void putObject(Object key, Object value);
// 根據緩存鍵獲取緩存
Object getObject(Object key);
// 根據緩存鍵刪除緩存
Object removeObject(Object key);
// 清空緩存數據
void clear();
}
複製代碼
PerpetualCache的實現很是簡單, 內部使用一個HashMap容器來維護緩存,Key存放的是CacheKey,Value存放的是結果集,代碼就不貼了,以下是它的屬性。post
public class PerpetualCache implements Cache {
// 緩存惟一表示
private final String id;
// 使用HashMap做爲數據緩存的容器
private final Map<Object, Object> cache = new HashMap<>();
}
複製代碼
CacheKey是MyBatis提供的緩存鍵,爲何要爲緩存鍵單獨寫一個類呢?由於MyBatis判斷是否命中緩存,條件很是多,並非一個簡單的字符串就能夠搞定的,所以纔有了CacheKey。 性能
如何判斷查詢可否命中緩存?有哪些條件須要判斷呢?總結以下:
以上五個條件,必須同時知足,才能命中緩存。並且,像【填充的參數】這類數據是不固定的,所以CacheKey使用一個List
來存放這些條件,以下是它的屬性:
可否命中緩存,就看CacheKey是否相等了。爲了提高equals
方法的性能,避免每次挨個比較updateList,CacheKey使用hashcode
來保存哈希值,哈希值是根據每一個條件對象參與計算得出的,MyBatis提供了一套算法,儘量的讓哈希值分散。
調用update
方法能夠添加條件對象,源碼以下:
public void update(Object object) {
// 計算單個對象的哈希值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 條件對象個數遞增
count++;
// 累加校驗和
checksum += baseHashCode;
// 基礎哈希值乘以條件數量,使哈希值儘量分散
baseHashCode *= count;
// 最終哈希值,再乘以質數17,仍是爲了分散
hashcode = multiplier * hashcode + baseHashCode;
// 添加條件對象到List
updateList.add(object);
}
複製代碼
equals
方法用於判斷兩個CacheKey是否相等,爲了提高性能,會先進行一系列的簡單校驗,最後纔是按個匹配每一個條件對象。
@Override
public boolean equals(Object object) {
// 前置一系列校驗省略...
// 以上步驟,都是爲了提高equals的性能。最終仍是比較updateList的每一項
for (int i = 0; i < updateList.size(); i++) {
// 依次比較updateList各項條件
}
return true;
}
複製代碼
若是CacheKey相等,則表明命中緩存。
前面已經說過,調用query
方法時,首先會建立CacheKey,根據CacheKey判斷可否命中緩存,最後,咱們看一下CacheKey的建立過程。
createCacheKey
方法位於BaseExecutor
,CacheKey的建立過程並不複雜,就是實例化一個CacheKey對象,而後將須要匹配的條件調用update
方法保存下來。
上文已經說過判斷可否命中緩存的五大條件了,所以它須要MappedStatement獲取StatementID、須要parameterObject獲取請求參數、須要RowBounds獲取分頁信息、須要BoundSql獲取要執行的SQL。
/** * * @param ms 執行的SQL節點封裝對象 * @param parameterObject 參數,通常是ParamMap * @param rowBounds 分頁信息 * @param boundSql 綁定的SQL * @return */
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
// 實例化緩存鍵
CacheKey cacheKey = new CacheKey();
// StatementId要一致,必須調用的是同一個Mapper的同一個方法
cacheKey.update(ms.getId());
// 分頁信息,查詢的數據範圍要一致
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 執行的SQL要一致,動態SQL的緣由,SQL都不一致,確定不能命中緩存
cacheKey.update(boundSql.getSql());
/** * 除了上述基本的四項,還要匹配全部的參數。 * 針對同一個查詢方法,傳入的參數不一樣,確定也不能命中緩存。 * 下面是從參數ParamMap中獲取對應參數的過程。 */
cacheKey.update(參數);
// 校驗運行環境,查詢的數據源不一樣,也不能命中緩存。
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
複製代碼
CacheKey建立完畢後,就是從PerpetualCache中判斷是否已經存在緩存數據了,若是命中緩存就直接取緩存結果,避免查詢數據庫,提高查詢性能。
繼續看BaseExecutor的其餘代碼你會發現,PerpetualCache雖然本身不會主動清理緩存,可是隻要執行了update
語句、或者事務提交/回滾都會清空緩存。
一級緩存是基於SqlSession的,當一個會話被打開時,它會同時建立一個Executor,Executor內部持有一個PerpetualCache,PerpetualCache底層使用一個HashMap容器來維護緩存結果集。HashMap中Key存儲的是CacheKey,它是MyBatis提供的緩存鍵,由於判斷是否命中緩存涉及的條件很是多,所以CacheKey使用一個List來保存條件對象,只有當全部的條件都匹配時,才能命中緩存。
MyBatis的一級緩存其實仍是比較雞肋的,在多會話的場景下存在髒數據的問題,SessionA讀了一遍數據,SessionB修改了該數據,SessionA再去讀仍然是舊數據。不過,若是你使用Spring整合MyBatis就不用擔憂這個問題了。