MyBatis源碼分析(4)—— Cache構建以及應用

来源:http://www.cnblogs.com/jabnih/archive/2016/07/27/5711395.html
-Advertisement-
Play Games

@(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的。
判斷條件:

  1. Statement ID
  2. 結果範圍
  3. Sql
  4. 所有的入參

在上面的條件中,對於需要使用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範圍緩存失效時刻:
    1. SqlSession關閉,則會釋放緩存
    2. 提交或者回滾的時候會清空對應的一級緩存。
    3. 在更新操作的時候,則直接清空對應的一級緩存
    4. 手動調用清空緩存操作
  • 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,並沒有進行一些限制處理。

  1. 在MyBatis看來,SqlSession一般都是生命周期比較短的,當關閉的時候會釋放緩存,但是如果使用SqlSession多次進行查詢大量的數據時,會將數據緩存,那麼有可能會導致OOM記憶體溢出。

二級緩存

MyBatis雖然全局配置開啟緩存,但是還是取決於是否使用了<cache>標簽,如果使用了二級緩存,需要註意:

  1. 每個<cache>代表一個單獨的二級緩存,如果多個Mapper需要共用同一個二級緩存,則需要使用<cache-ref>
  2. 如果一個Mapper中查詢數據時,使用了多表聯查,則,當另一個Mapper更新相關數據時,如果沒有共用一個Cache,那麼下一次該Mapper查詢時,就會出現讀到臟數據。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 工廠方法模式適用於“大量的產品需要頻繁創建,並且具有同樣的介面時使用”。 一、普通工廠模式 對於實現了同一個介面的一些類進行統一實例的創建,把這個統一創建的工作交給一個類來實現,這個類就叫做工廠類。 Job介面 (定義了要做的事情) 1 public interface Job { 2 public ...
  • ...
  • 在 Delphi Berlin 10.1 IDE 里雖然提供了 Style 編輯功能(TStyleBook),但遍尋不到更換 Style 底圖功能,於是花了一點時間,寫了一個小工具: 編譯版本:Delphi Berlin 10.1 下載工具:[工具]OneStyleEdit_Style換圖工具_by ...
  • 本書介紹 你是不是對Django的學習感到迷茫?是不是對網上零星的教程感到絕望?是不是苦於沒有可以迅速上手的實例而發愁?如果你同我一樣有這些感受,那麼《Django.By.Example》這本書將是你指路的明燈。 本書基於Django1.8 + Python3.4的環境,裡面包含了個人博客、社交網站 ...
  • HttpSession HttpServletRequest.getSession() HibernateSessionFactory.getSession() HttpSession的生命周期是以上三者中最長的,存在於整次對話(瀏覽器關閉後被銷毀),用於存儲單個用戶在本次會話中所用到的數據。 Ht ...
  • 1.錯誤: 在eclipse中使用run->run on server的時候,選擇tomcat6會報錯誤:The server does not support version 3.0 of the J2EE Web module specification 如下所示: 2.原因: Tomcat 6 ...
  • R語言中的機器學習程式包主要如下所示: ...
  • learn python3   這是我初學Python時寫的一套Python基礎示常式序.主要基於廖雪峰老師的Python3教程和<<深入理解Python>>. 感謝! 下麵是這些示常式序的目錄總結:  Chapter1:容器/集合/C ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...