MyBatis一級緩存源碼分析

這是我參與8月更文挑戰的第6天,活動詳情查看:8月更文挑戰java


1. 前言

爲了提高持久層數據查詢的性能,MyBatis提供了一套緩存機制,根據緩存的做用範圍可分爲:一級緩存和二級緩存。一級緩存默認是開啓的,做用域爲SqlSession,也稱做【回話緩存/本地緩存】。二級緩存默認是關閉的,須要手動開啓,做用域爲namespace。本篇文章暫且不討論二級緩存,僅從源碼的角度分析一下MyBatis一級緩存的實現原理。 ​算法

咱們已經知道,SqlSession是MyBatis對外提供的,操做數據庫的惟一接口。當咱們從SqlSessionFactory打開一個新的回話時,一個新的SqlSession實例將被建立。SqlSession內置了一個Executor,它是MyBatis提供的操做數據庫的執行器,當咱們執行數據庫查詢時,最終會調用Executor.query()方法,它在查詢數據庫前會先判斷是否命中一級緩存,若是命中就直接返回,不然才真的發起查詢操做。 ​數據庫

2. 源碼分析

咱們直接看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

2.1 PerpetualCache

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<>();
}
複製代碼

2.2 CacheKey

CacheKey是MyBatis提供的緩存鍵,爲何要爲緩存鍵單獨寫一個類呢?由於MyBatis判斷是否命中緩存,條件很是多,並非一個簡單的字符串就能夠搞定的,所以纔有了CacheKey。 ​性能

如何判斷查詢可否命中緩存?有哪些條件須要判斷呢?總結以下:

  1. StatementID要相同,必須執行的是同一個接口的同一個方法。
  2. RowBounds要相同,查詢的數據範圍必須一致。
  3. 執行的SQL語句要相同。
  4. 預編譯的SQL填充的參數要相同。
  5. 查詢的數據源要相同,即EnvironmentID相同。

以上五個條件,必須同時知足,才能命中緩存。並且,像【填充的參數】這類數據是不固定的,所以CacheKey使用一個List來存放這些條件,以下是它的屬性:

  1. DEFAULT_MULTIPLIER:默認參與哈希值計算的倍數因子。
  2. DEFAULT_HASHCODE:默認哈希值。
  3. multiplier:與哈希值計算的倍數因子,默認37。
  4. hashcode:哈希碼,提升equals的效率。
  5. checksum:校驗和,哈希值的和。
  6. count:updateList元素的個數。
  7. updateList:條件列表,必須知足全部條件,才能命中緩存。

可否命中緩存,就看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相等,則表明命中緩存。 ​

2.3 建立緩存鍵

前面已經說過,調用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語句、或者事務提交/回滾都會清空緩存。

3. 總結

一級緩存是基於SqlSession的,當一個會話被打開時,它會同時建立一個Executor,Executor內部持有一個PerpetualCache,PerpetualCache底層使用一個HashMap容器來維護緩存結果集。HashMap中Key存儲的是CacheKey,它是MyBatis提供的緩存鍵,由於判斷是否命中緩存涉及的條件很是多,所以CacheKey使用一個List來保存條件對象,只有當全部的條件都匹配時,才能命中緩存。 ​

MyBatis的一級緩存其實仍是比較雞肋的,在多會話的場景下存在髒數據的問題,SessionA讀了一遍數據,SessionB修改了該數據,SessionA再去讀仍然是舊數據。不過,若是你使用Spring整合MyBatis就不用擔憂這個問題了。

相關文章
相關標籤/搜索