Spring + MySQL + Mybatis + Redis【二級緩存】執行流程分析

  • 一級緩存基於 PerpetualCache 的 HashMap 本地緩存,其存儲做用域爲 Session,當 Session flush 或 close 以後,該Session中的全部 Cache 就將清空。java

  • 二級緩存與一級緩存其機制相同,默認也是採用 PerpetualCache,HashMap存儲,不一樣在於其存儲做用域爲 Mapper(Namespace),而且可自定義存儲源,如 Ehcache、Hazelcast等。算法

  • 對於緩存數據更新機制,當某一個做用域(一級緩存Session/二級緩存Namespaces)的進行了 C/U/D 操做後,默認該做用域下全部 select 中的緩存將被clear。spring

  • MyBatis 的緩存採用了delegate機制 及 裝飾器模式設計,當put、get、remove時,其中會通過多層 delegate cache 處理,其Cache類別有:BaseCache(基礎緩存)、EvictionCache(排除算法緩存) 、DecoratorCache(裝飾器緩存):sql

    1. BaseCache :爲緩存數據最終存儲的處理類,默認爲 PerpetualCache,基於Map存儲;可自定義存儲處理,如基於EhCache、Memcached等;
    2. EvictionCache :當緩存數量達到必定大小後,將經過算法對緩存數據進行清除。默認採用 Lru 算法(LruCache),提供有 fifo 算法(FifoCache)等;
    3. DecoratorCache:緩存put/get處理先後的裝飾器,如使用 LoggingCache 輸出緩存命中日誌信息、使用 SerializedCache 對 Cache的數據 put或get 進行序列化及反序列化處理、當設置flushInterval(默認1/h)後,則使用 ScheduledCache 對緩存數據進行定時刷新等。
  • 通常緩存框架的數據結構基本上都是 Key-Value 方式存儲,MyBatis 對於其 CacheKey 的生成採起規則爲:數據庫

    [hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。apache

  • 對於併發 Read/Write 時緩存數據的同步問題,MyBatis 默認基於 JDK/concurrent中的ReadWriteLock,使用 ReentrantReadWriteLock 的實現,從而經過 Lock 機制防止在併發 Write Cache 過程當中線程安全問題。緩存

 

   測試 User user=userService.get(55); 安全

 

  1. 一般咱們在service層最終都會調用Mapper的接口方法,實現對數據庫的操做,本例中是經過id查詢user對象。
  2. 咱們知道Mapper是一個接口,接口是沒有對象的,更不能調用方法了,而咱們調用的實際上是mybatis框架的mapper動態代理對象MapperProxy,而MapperProxy中有封裝了配置信息的DefaultSqlSession中的Configuration。調用mapper方法的具體代碼以下。
  3. 在執行mapperMethod的execute的時候,不只傳遞了方法參數,還傳遞了sqlSession。在執行execute,實際上是經過判斷配置文件的操做類型,來調用sqlSession的對應方法的。本例中,因爲是select,而返回值不是list,因此下一步執行的是sqlSession的selectOne 
  4. selectOne其實調用了selectList,只不過是取了第一個。
  5. selectList通過層層調用,最終交給執行器執行。具體執行器的結構待會咱們會分析。注意這裏的ms參數,其實就是從Configration中獲得的一些配置信息,包括mapper文件裏的sql語句。具體代碼以下:
  6. 這裏的執行器execute,實際上是spring注入的。excute是一個接口,而到時候具體是哪一個execute執行,是看配置文件的。若是啓動用了Cache 才調用 CachingExecutor.query,反之則使用 BaseExcutor.query 進行數據庫查詢   
  7. 而咱們的一級緩存和二級緩存其實都是execute中的一種。接下來,咱們遍分析一下執行器(Executor)。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 2、Executor框架數據結構

 

 

解析器:結合mybatis-spring框架,讀取spring關於mybatis的配置文件。具體看是否開啓緩存(這裏指二級緩存),若是開啓,生成的執行器爲CachingExecutor。 mybatis

動態代理:實現調用mapper接口的時候執行mybatis邏輯

執行器:執行緩存處理邏輯。在這裏二級緩存和一級緩存有所區別。

 

 BatchExcutorReuseExcutor SimpleExcutor: 這幾個就沒什麼好說的了,繼承了 BaseExcutor 的實現其 doQuery、doUpdate 等方法,一樣都是採用 JDBC 對數據庫進行操做;三者區別在於,批量執行、重用 Statement 執行、普通方式執行。具體應用及場景在Mybatis 的文檔上都有詳細說明。 

CachingExecutor: 二級緩存執行器。我的以爲這裏設計的不錯,靈活地使用 delegate機制。其委託執行的類是 BaseExcutor。 當沒法從二級緩存獲取數據時,一樣須要從 DB 中進行查詢,因而在這裏能夠直接委託給 BaseExcutor 進行查詢。其大概流程爲: 

流程爲: 從二級緩存中進行查詢 -> [若是緩存中沒有,委託給 BaseExecutor] -> 進入一級緩存中查詢 -> [若是也沒有] -> 則執行 JDBC 查詢,其 query 代碼以下: 

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
     // 當前 Statement 是否啓用了二級緩存 
        if (cache != null) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, parameterObject, boundSql);
                List<E> list = (List)this.tcm.getObject(cache, key);
                if (list == null) {
            // 未找到緩存,很委託給 BaseExecutor 執行查詢  
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }
     // 沒有啓動用二級緩存,直接委託給 BaseExecutor 執行查詢 
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }



  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    // 將建立 cache key 委託給 BaseExecutor 建立 
        return this.delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    }

Cache 委託鏈構建
正如最開始的緩存概述所描述道,其緩存類的設計採用 裝飾模式,基於委託的調用機制。 
緩存實例構建: 
緩存實例的構建 ,Mybatis 在解析其 Mapper 配置文件時就已經將該實現初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 類中能夠看到: 

Java代碼   收藏代碼
private void cacheElement(XNode context) throws Exception {  
    if (context != null) {  
      // 基礎緩存類型  
      String type = context.getStringAttribute("type", "PERPETUAL");  
      Class typeClass = typeAliasRegistry.resolveAlias(type);  
      // 排除算法緩存類型  
      String eviction = context.getStringAttribute("eviction", "LRU");  
      Class evictionClass = typeAliasRegistry.resolveAlias(eviction);  
      // 緩存自動刷新時間  
      Long flushInterval = context.getLongAttribute("flushInterval");  
      // 緩存存儲實例引用的大小  
      Integer size = context.getIntAttribute("size");  
      // 是不是隻讀緩存  
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);  
      Properties props = context.getChildrenAsProperties();  
      // 初始化緩存實現  
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);  
    }  
  }  

 

如下是  useNewCache 方法實現: 

Java代碼   收藏代碼
public Cache useNewCache(Class typeClass,  
                           Class evictionClass,  
                           Long flushInterval,  
                           Integer size,  
                           boolean readWrite,  
                           Properties props) {  
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);  
    evictionClass = valueOrDefault(evictionClass, LruCache.class);  
    // 這裏構建 Cache 實例採用 Builder 模式,每個 Namespace 生成一個  Cache 實例  
    Cache cache = new CacheBuilder(currentNamespace)  
        // Builder 前設置一些從XML中解析過來的參數  
        .implementation(typeClass)  
        .addDecorator(evictionClass)  
        .clearInterval(flushInterval)  
        .size(size)  
        .readWrite(readWrite)  
        .properties(props)  
        // 再看下面的 build 方法實現  
        .build();  
    configuration.addCache(cache);  
    currentCache = cache;  
    return cache;  
}  
  
public Cache build() {  
    setDefaultImplementations();  
    // 建立基礎緩存實例  
    Cache cache = newBaseCacheInstance(implementation, id);  
    setCacheProperties(cache);  
    // 緩存排除算法初始化,並將其委託至基礎緩存中  
    for (Class<? extends Cache> decorator : decorators) {  
      cache = newCacheDecoratorInstance(decorator, cache);  
      setCacheProperties(cache);  
    }  
    // 標準裝飾器緩存設置,如LoggingCache之類,一樣將其委託至基礎緩存中  
    cache = setStandardDecorators(cache);  
    // 返回最終緩存的責任鏈對象  
    return cache;  
}  

 

最終生成後的緩存實例對象結構: 
 
可見,全部構建的緩存實例已經經過責任鏈方式將其串連在一塊兒,各 Cache 各負其責、依次調用,直到緩存數據被 Put 至 基礎緩存實例中存儲。 


 Cache 實例解剖
實例類:SynchronizedCache 
說   明:用於控制 ReadWriteLock,避免併發時所產生的線程安全問題。 
解   剖: 
對於 Lock 機制來講,其分爲 Read 和 Write 鎖,其 Read 鎖容許多個線程同時持有,而 Write 鎖,一次能被一個線程持有,若是當 Write 鎖沒有釋放,其它須要 Write 的線程只能等待其釋放才能去持有。 
其代碼實現:

 public synchronized void putObject(Object key, Object object) {
        this.delegate.putObject(key, object);
    }

    public synchronized Object getObject(Object key) {
        return this.delegate.getObject(key);
    }

 

 

其具體原理能夠看看 jdk concurrent 中的 ReadWriteLock 實現。 


實例類:LoggingCache 
說   明:用於日誌記錄處理,主要輸出緩存命中率信息。 
解   剖: 
說到緩存命中信息的統計,只有在 get 的時候才須要統計命中率: 

Java代碼   收藏代碼
public Object getObject(Object key) {  
    requests++; // 每調用一次該方法,則獲取次數+1  
    final Object value = delegate.getObject(key);  
    if (value != null) {  // 命中! 命中+1  
      hits++;  
    }  
    if (log.isDebugEnabled()) {  
      // 輸出命中率。計算方法爲: hits / requets 則爲命中率  
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  
    }  
    return value;  
} 

 

實例類:SerializedCache 
說   明:向緩存中 put 或 get 數據時的序列化及反序列化處理。 
解   剖: 
序列化在Java裏面已是最基礎的東西了,這裏也沒有什麼特殊之處: 

Java代碼   收藏代碼
public void putObject(Object key, Object object) {  
     // PO 類須要實現 Serializable 接口  
    if (object == null || object instanceof Serializable) {  
      delegate.putObject(key, serialize((Serializable) object));   
    } else {  
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);  
    }  
  }  
  
  public Object getObject(Object key) {  
    Object object = delegate.getObject(key);  
    // 獲取數據時對 二進制數據進行反序列化  
    return object == null ? null : deserialize((byte[]) object);  
  }  

 

其 serialize 及 deserialize 代碼: 

 private byte[] serialize(Serializable value) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(value);
            oos.flush();
            oos.close();
            return bos.toByteArray();
        } catch (Exception var4) {
            throw new CacheException("Error serializing object.  Cause: " + var4, var4);
        }
    }

    private Serializable deserialize(byte[] value) {
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream(value);
            ObjectInputStream ois = new SerializedCache.CustomObjectInputStream(bis);
            Serializable result = (Serializable)ois.readObject();
            ois.close();
            return result;
        } catch (Exception var5) {
            throw new CacheException("Error deserializing object.  Cause: " + var5, var5);
        }
    }

 



實例類:LruCache 
說   明:最近最少使用的:移除最長時間不被使用的對象,基於LRU算法。 
解   剖: 
這裏的 LRU 算法基於 LinkedHashMap 覆蓋其 removeEldestEntry 方法實現。好象以前看過 XMemcached 的 LRU 算法也是這樣實現的。 
初始化 LinkedHashMap,默認爲大小爲 1024 個元素: 

Java代碼   收藏代碼
public LruCache(Cache delegate) {  
    this.delegate = delegate;  
    setSize(1024); // 設置 map 默認大小  
}  
public void setSize(final int size) {  
    // 設置其 capacity 爲size, 其 factor 爲.75F  
    keyMap = new LinkedHashMap(size, .75F, true) {  
      // 覆蓋該方法,當每次往該map 中put 時數據時,如該方法返回 True,便移除該map中使用最少的Entry  
      // 其參數  eldest 爲當前最老的  Entry  
      protected boolean removeEldestEntry(Map.Entry eldest) {  
        boolean tooBig = size() > size;  
        if (tooBig) {  
          eldestKey = eldest.getKey(); //記錄當前最老的緩存數據的 Key 值,由於要委託給下一個 Cache 實現刪除  
        }  
        return tooBig;  
      }  
    };  
  }  
  
public void putObject(Object key, Object value) {  
    delegate.putObject(key, value);  
    cycleKeyList(key);  // 每次 put 後,調用移除最老的 key  
}  
// 看看當前實現是否有 eldestKey, 有的話就調用 removeObject ,將該key從cache中移除  
private void cycleKeyList(Object key) {  
    keyMap.put(key, key); // 存儲當前 put 到cache中的 key 值  
    if (eldestKey != null) {  
      delegate.removeObject(eldestKey);  
      eldestKey = null;  
    }  
  }  
  
public Object getObject(Object key) {  
    keyMap.get(key); // 便於 該 Map 統計 get該key的次數  
    return delegate.getObject(key);  
  } 

 

實例類:PerpetualCache 
說   明:這個比較簡單,直接經過一個 HashMap 來存儲緩存數據。因此沒什麼說的,直接看下面的 MemcachedCache 吧。 

相關文章
相關標籤/搜索