myBatis原始碼解析-二級快取的實現方式

超人小冰發表於2020-09-16

1. 前言

前面近一個月去寫自己的mybatis框架了,對mybatis原始碼分析止步不前,此文繼續前面的文章。開始分析mybatis一,二級快取的實現。
附上自己的專案github地址:https://github.com/xbcrh/simple-ibatis 

對mybatis感興趣的同學可關注下,全手寫的一個orm框架,實現了sql的基本功能和物件關係對映。
廢話不說,開始解析mybatis快取原始碼實現。

2. mybatis中快取的實現方式

見mybatis原始碼包 org.apache.ibatis.cache

2.1 mybatis快取實現介面類:cache

public interface Cache {
  // 獲取快取的ID
  String getId();
  // 放入快取
  void putObject(Object key, Object value);
  // 從快取中獲取
  Object getObject(Object key);
  // 移除快取
  Object removeObject(Object key);
  // 清除快取
  void clear();
  // 獲取快取大小
  int getSize(); 
  // 獲取鎖
  ReadWriteLock getReadWriteLock();
}

mybatis自定義了快取介面類,提供了基本的快取增刪改查的操作。在此基礎上,提供了基礎快取實現類PerpetualCache。原始碼如下:

2.2 mybatis快取基本實現類:PerpetualCache

public class PerpetualCache implements Cache {

  // 快取的ID
  private String id;
  // 使用HashMap充當快取(老套路,快取底層實現基本都是map)
  private Map<Object, Object> cache = new HashMap<Object, Object>();
  // 唯一構造方法(即快取必須有ID)
  public PerpetualCache(String id) {
    this.id = id;
  }
  // 獲取快取的唯一ID
  public String getId() {
    return id;
  }
  // 獲取快取的大小,實際就是hashmap的大小
  public int getSize() {
    return cache.size();
  }
  // 放入快取,實際就是放入hashmap
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }
  // 從快取獲取,實際就是從hashmap中獲取
  public Object getObject(Object key) {
    return cache.get(key);
  }
  // 從快取移除
  public Object removeObject(Object key) {
    return cache.remove(key);
  }
  // hashmap清除資料方法
  public void clear() {
    cache.clear();
  }
  // 暫時沒有其實現
  public ReadWriteLock getReadWriteLock() {
    return null;
  }
  // 快取是否相同
  public boolean equals(Object o) {
    if (getId() == null) throw new CacheException("Cache instances require an ID.");
    if (this == o) return true; // 快取本身,肯定相同
    if (!(o instanceof Cache)) return false; // 沒有實現cache類,直接返回false

    Cache otherCache = (Cache) o; // 強制轉換為cache
    return getId().equals(otherCache.getId()); // 直接比較ID是否相等
  }
  // 獲取hashCode
  public int hashCode() {
    if (getId() == null) throw new CacheException("Cache instances require an ID.");
    return getId().hashCode();
  }

}

PerpetualCache 類其實是對HashMap的封裝,通過對map的put和get等操作實現快取的存取等功能。mybatis中除了基本的快取實現類外還提供了一系列的裝飾類(此處是用到裝飾者模式),此處拿較為重要的裝飾類LruCache進行分析。

2.3 Lru淘汰策略實現分析

Lru是一種快取淘汰策略,其核心思想是”如果資料最近被訪問過,那麼將來被訪問的機率也更高“,LruCache 是基於LinkedHashMap實現,LinkedHashMap繼承自HashMap,來分析下為什麼LinkedHashMap可以當做Lru快取實現。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

LinkedHashMap繼承HashMap類,實際上就是對HashMap的一個封裝。

// 內部維護了一個自定義的Entry,整合HashMap中的node類
static class Entry<K,V> extends HashMap.Node<K,V> {
        // linkedHashmap用來連線節點的欄位,根據這兩個欄位可查詢按順序插入的節點
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

構造方法見如下:

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        // 呼叫HashMap的構造方法
        super(initialCapacity, loadFactor);
        // 訪問順序維護,預設false不開啟
        this.accessOrder = accessOrder;
    }  

引入兩種圖來理解HashMap與LinkedHashMap

 

 

 以上是HashMap的結構,採用拉鍊法解決衝突。LinkedHashMap在HashMap基礎上增加了一個雙向連結串列來表示節點插入順序。

 

 

 



如上,節點上多出的紅色和藍色箭頭代表了Entry中的before和after。在put元素時,會自動在尾節點後加上該元素,維持雙向連結串列。瞭解LinkedHashMap結構後,在看看究竟什麼是維護節點的訪問順序。先說結論,當開啟accessOrder後,在對元素進行get操作時,會將該元素放在雙向連結串列的隊尾節點。原始碼如下:

public V get(Object key) {
        Node<K,V> e;
       // 呼叫HashMap的getNode方法,獲取元素
        if ((e = getNode(hash(key), key)) == null)
            return null;
       // 預設為false,如果開啟維護連結串列訪問順序,執行如下方法
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }


// 方法實現(將e放入尾節點處)
void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 當節點不是雙向連結串列的尾節點時
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 將待調整的e節點賦值給p
           
            p.after = null;
            if (b == null) // 說明e為頭節點,將老e的下一節點值為頭節點
                head = a;
            else
                b.after = a;// 否則,e的上一節點直接指向e的下一節點
            if (a != null)
                a.before = b; // e的下一節點的上節點為e的上一節點
            else
                last = b;  
            if (last == null)
                head = p;  
            else {
                p.before = last;   // last和p互相連線
                last.after = p;
            }
            tail = p;   // 將雙向連結串列的尾節點指向p
            ++modCount; // 修改次數加以
        }
    }

程式碼很簡單,如上面的圖,我訪問了節點值為3的節點,那木經過get操作後,結構變成如下:

 

 

 經過如上分析我們知道,如果限制雙向連結串列的長度,每次刪除頭節點的值,就變為一個lru的淘汰策略了。舉個例子,我想限制雙向連結串列的長度為3,依次put 1 2 3,連結串列為 1 -> 2 -> 3,訪問元素2,連結串列變為 1 -> 3-> 2,然後put 4 ,發現連結串列長度超過3了,淘汰1,連結串列變為3 -> 2 ->4;

那木linkedHashMap是怎樣知道自定義的限制策略,看程式碼,因為LinkedHashMap中沒有提供自己的put方法,是直接呼叫的HashMap的put方法,檢視hashMap程式碼如下:

// hashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 看這個方法
        afterNodeInsertion(evict);
        return null;
    }

// linkedHashMap重寫了此方法

 void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // removeEldestEntry預設返回fasle
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            // 移除雙向連結串列中的頭指標元素
            removeNode(hash(key), key, null, false, true);
        }
    }

原來只需要重新實現removeEldestEntry就可以自定義實現lru功能了。瞭解基本的lru原理後,開始分析LruCache。

2.4 快取包裝類 - LruCache

public class LruCache implements Cache {
  // 被裝飾的快取類,即真實的快取類,提供真正的快取能力
  private final Cache delegate;
  // 內部維護的一個linkedHashMap,用來實現LRU功能
  private Map<Object, Object> keyMap;
  // 待淘汰的快取元素
  private Object eldestKey;
  // 唯一構造方法
  public LruCache(Cache delegate) {
    this.delegate = delegate; // 被裝飾的快取類
    setSize(1024); // 設定快取大小
  }
  ....
 }

經分析,LruCache還是個裝飾類。內部除了維護真正的Cache外,還維護了一個LinkedHashMap,用來實現Lru功能,檢視其構造方法。

// 唯一構造方法
  public LruCache(Cache delegate) {
    this.delegate = delegate; // 被裝飾的快取類
    setSize(1024); // 設定快取大小
  }
  
   // setSize()是構造方法中方法
  public void setSize(final int size) {
    // 初始化keyMap
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;
      // 什麼時候自動刪除快取元素,此處是根據當快取數量超過指定的數量,在LinkedHashMap內部刪除元素
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          // 將待刪除元素賦值給eldestKey,後續會根據此值是否為空在真實快取中刪除
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

和上文分析一樣,重寫了removeEldestEntry方法。此方法返回一個boolean值,當快取的大小超過自定義大小,返回true,此時linkedHashMap中會自動刪除eldest元素。在真實快取cache中也將此元素刪除。保持真實cache和linkedHashMap元素一致。其實就是用linkedHashMap的lru特性來保證cache也具有此lru特性。
分析put方法和get方法驗證此結論.。

@Override
  public Object getObject(Object key) {
    keyMap.get(key); // 觸發linkedHashMap中get方法,將key對應的元素放入隊尾
    return delegate.getObject(key); // 呼叫真實的快取get方法
  }
  
  // 放入快取時,除了在真實快取中放一份外,還會在LinkedHashMap中放一份
   @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    // 呼叫LinkedHashMap的方法
    cycleKeyList(key);
  }
  
  private void cycleKeyList(Object key) {
    // linkedHashMap中put,會觸發removeEldestEntry方法,如果快取大小超過指定大小,則將雙向連結串列對頭值賦值給eldestKey
    keyMap.put(key, key); 
    // 檢查eldestKey是否為空。不為空,則代表此元素是淘汰的元素了,需要在真實快取中刪除。
    if (eldestKey != null) {
      // 真實快取中刪除
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

介紹完Cache基本實現後,開始分析mybatis中一級快取

3. mybatis一級快取使用原始碼分析

此處是僅介紹mybatis的實現,沒有涉及到與Spring整合,先介紹mybatis最基本的sql執行語法。預設大家掌握了SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession用法。後面我會寫一篇部落格分析SQL在mybatis中執行的過程,會介紹到這些基礎知識。

InputStream inputStream = Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml"); // 構建位元組流
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();   // 構建SqlSessionFactoryBuilder
SqlSessionFactory factory = builder.build(inputStream);  // 構建SqlSessionFactory

SqlSession sqlSession = factory.openSession(); // 生成SqlSession
List<SysUser> userList = sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser"); // 執行SysUserMapper類的getSysUser方法

前文構建SqlSession的內容大家感興趣可自行檢視,此處僅分析執行過程。檢視selectList方法,mybatis中sqlSession的預設實現為DefaultSqlSession

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
  // 每個mapper檔案會解析生成一個MappedStatement
  MappedStatement ms = configuration.getMappedStatement(statement);
  // 呼叫真實的查詢方法,此處是呼叫executor的方法。executor採用了裝飾者模式,若該mapper檔案未啟用二級快取,則預設為BaseExecutor。
  // 若該mapper檔案啟用了二級快取,則使用的是CachingExecutor
  List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  return result;
} catch (Exception e) {
  throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
} finally {
  ErrorContext.instance().reset();
}
}

因為此處使用的是裝飾者模式,BaseExecutor是最基礎的執行器,使用了一級快取,CachingExecutor是對BaseExecutor進行一次封裝,若開啟二級快取開關,在使用一級快取前,先使用二級快取。後文介紹二級快取會分析這兩個Executor生成地方。先分析BaseExecutor的一級快取實現。

// BaseExecutor.java
/**
   * 查詢,並建立好CacheKey物件
   * @param ms Mapper.xml檔案的select,delete,update,insert這些DML標籤的封裝類
   * @param parameter 引數物件
   * @param rowBounds Mybatis的分頁物件
   * @param resultHandler 結果處理器物件
   */
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter); // 獲取boundSql物件
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);  // 生成快取KEY
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 執行如下方法
 }

  @SuppressWarnings("unchecked")
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) throw new ExecutorException("Executor was closed.");
    //如果將flushCacheRequired為true,則會在執行器執行之前就清空本地一級快取
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++; // 請求堆疊加一
      // 如果此次查詢的resultHandler為null(預設為null),則嘗試從本地快取中獲取已經快取的的結果
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
        //如果查到localCache快取,處理localOutputParameterCache,即對儲存過程的sql進行特殊處理
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 從資料庫中查詢,並將結果放入到localCache
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      // 請求堆疊減一
      queryStack--;
    }
    if (queryStack == 0) {
      // 載入延遲載入List
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      deferredLoads.clear(); // issue #601
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // issue #482
      }
    }
    return list;
  }
 
 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER); // 先放置一個佔位符
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);  // 從資料庫中查詢
    } finally {
      localCache.removeObject(key); // 移除佔位符
    }
    localCache.putObject(key, list); // 放入快取
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);  // 若是儲存過程,則放入儲存過程快取中
    }
    return list; // 返回查詢結果
}

mybatis一級快取很好理解,對於同一個SqlSession物件(即同一個Executor),執行同一條語句時,BaseExecutor會先從自己的快取中查詢,是否存在此條語句的結果,若能找到,則直接返回(暫且忽略儲存過程處理)。若沒有找到,則查詢資料庫,將結果放入此快取,供下次使用。mybatis預設開啟一級快取。

4. mybatis二級快取使用原始碼分析

4.1 配置方式

在全域性配置檔案中mybatis-config.xml中加入如下設定

<settings>    
<setting name="cacheEnabled" value="true"/>
</settings>

在具體mapper.xml中配置<cache/>標籤或者<cache-ref/>標籤

<cache></cache>或者<cache-ref/>

或者採用註解配置方式,在mapper.java檔案上配置註解

@CacheNamespace 或者 @CacheNamespaceRef

4.1 mybatis解析二級快取標籤

還是採用上面sqlSession方式程式碼來debug

InputStream inputStream = Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml"); // 構建位元組流
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();   // 構建SqlSessionFactoryBuilder
SqlSessionFactory factory = builder.build(inputStream);  // 構建SqlSessionFactory

進入檢視builder.build()方法

// SqlSessionFactoryBuilder.java
  /**根據流構建SqlSessionFactory*/
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      /**構建XML檔案解析器*/
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      /**開始解析mybatis-config.xml檔案並構建全域性變數Configuration*/
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
 }

進入parser.parse()方法,,進一步分析

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first // 讀取properties配置
      typeAliasesElement(root.evalNode("typeAliases")); // 讀取別名設定
      pluginElement(root.evalNode("plugins")); // 讀取外掛設定
      objectFactoryElement(root.evalNode("objectFactory")); // 讀取物件工廠設定
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 讀取物件包裝工廠設定
      settingsElement(root.evalNode("settings")); // 讀取setting設定
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 // 讀取環境設定
      databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 讀取資料庫ID提供資訊
      typeHandlerElement(root.evalNode("typeHandlers"));  // 讀取型別轉換處理器
      mapperElement(root.evalNode("mappers"));  // 解析mapper檔案
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

此處僅分析<cache/> 和 <cache-ref/>標籤的解析,<cache/> 和 <cache-ref/>存在具體的mapper.xml檔案中,分析mapperElement()方法。因為在mybatis-config.xml檔案中關於<mapper>標籤的值可配置package,resource,url,class等配置。如

<mappers>
    <mapper class="com.xiaobing.mapper.SysUserMapper"/>
</mappers>

分析mapperElement()方法

/**
   * 對映檔案支援四種配置,package,resource,url,class四種
   * 如在mybatis-config.xml中配置
   * <mappers>
   <mapper class="com.xiaobing.mapper.SysUserMapper"/>
   </mappers>
   * */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) { // 若配置的是package,在講package下的所有mapper檔案進行解析
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {  // 若配置的是resource,在解析resource對應的mapper.xml
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource); // 獲取xml檔案位元組流
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 構建xml檔案構造器
            mapperParser.parse(); // 解析xml檔案
          } else if (resource == null && url != null && mapperClass == null) { // 若配置的是url,在解析url對應的mapper.xml
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) { // 若配置的是class,在解析class對應的mapper檔案
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface); // 分析addMapper()方法
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

因為我採用的是class配置,所以分析configuration.addMapper()方法

 // Configuration.java
  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

繼續進入mapperRegistry.addMapper進行分析

// MapperRegistry.java
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) { // mapper介面
      if (hasMapper(type)) { // 若mapper已被註冊
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));  // 註冊對映介面
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 生成註解構造器
        parser.parse(); // 解析mapper上的註解
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

knownMappers.put(type, new MapperProxyFactory<T>(type));這裡很重要,是註冊mapper檔案代理物件。此處只做快取的解釋,不做註冊詳解,後面在分析sql執行流程時單獨去分析。

parser.parse()是對mapper檔案進行解析的關鍵,繼續分析

// MapperAnnotationBuilder.java
 // 解析配置檔案
  public void parse() {
    String resource = type.toString(); // 介面的全限定名 class com.test.userMapper
    if (!configuration.isResourceLoaded(resource)) {  // 是否載入過
      loadXmlResource(); // 在預設路徑下(預設和mapper介面同個包下),載入xml檔案
      configuration.addLoadedResource(resource); // 設為該mapper配置檔案已解析
      assistant.setCurrentNamespace(type.getName()); // 設定構建助力器當前名稱空間 com.test.userMapper
      parseCache(); // 解析CacheNamespace註解,構建一個Cache物件,並儲存到Mybatis全域性配置資訊中
      parseCacheRef(); //解析CacheNamespace註解,引用CacheRef對應的Cache物件。
      // 由此可知,當引入了<cache/>和<cacheRef/>後,該名稱空間的快取物件變為了CacheRef引用的快取物件
      Method[] methods = type.getMethods(); // 獲取方法
      for (Method method : methods) {
        try {
          if (!method.isBridge()) { // issue #237 若該方法不是橋接方法
            parseStatement(method); //構建MapperStatement物件,並新增到Mybatis全域性配置資訊中
          }
        } catch (IncompleteElementException e) {
          //當出現未完成元素時,新增構建Method時丟擲異常的MethodResolver例項,到下個Mapper的解析時再次嘗試解析
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods(); // 解析未完成解析的Method
  }

通過上面的程式碼註釋,可知,當解析mapper.java檔案前,會先在同個資料夾下檢視是否存在mapper.xml檔案,若存在,則先解析mapper.xml檔案。在解析mapper.xml檔案時,若在mapper.xml中寫了快取<cache/>或<cache-ref>,也會生成二級快取。若同時還在mapper.java檔案裡寫了@CacheNamespace註解。則會進行報錯,因為出現了兩個快取。此時我們根據註解配置去分析。去分析parseCache()和parseCacheRef(),看配置了註解@CacheNamespace和CacheNamespaceRef之後快取具體怎樣生成。

// MapperAnnotationBuilder.java
private void parseCache() {
    // 獲取是否有@CacheNamespace 註解
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      /*
      * 構建一個快取物件,具體分析
      * */
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), cacheDomain.flushInterval(), cacheDomain.size(), cacheDomain.readWrite(), null);
    }
  }
// mapperBuilderAssistant.java

public Cache useNewCache(Class<? extends Cache> typeClass, // 基本快取類
      Class<? extends Cache> evictionClass,  // 快取裝飾類
      Long flushInterval, // 快取重新整理間隔
      Integer size, // 快取大小
      boolean readWrite, // 快取可讀寫
      Properties props) {
    typeClass = valueOrDefault(typeClass, PerpetualCache.class); // 沒有設定則採用預設的PerpetualCache
    evictionClass = valueOrDefault(evictionClass, LruCache.class); // 沒有設定則採用預設的LruCache
    Cache cache = new CacheBuilder(currentNamespace) // 名稱空間作為快取唯一ID
        .implementation(typeClass)
        .addDecorator(evictionClass)
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .properties(props)
        .build();
    configuration.addCache(cache); // 加入到全域性快取
    currentCache = cache; // 當前快取設為cache,由此可知,快取是mapper級別
    return cache;
  }

此處是生成了二級快取的地方,並設定當前mapper檔案的快取為這個生成的二級快取。若沒有配置@CacheNamespaceRef,那木此mapper檔案就使用了這個自己生成的二級快取。那@CacheNamespaceRef是用來幹嘛的?回到上面程式碼處進行分析。

// MapperAnnotationBuilder.java
 
  private void parseCacheRef() {
    // @CacheNamespaceRef 相當於<cacheRef/>標籤
    CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
    if (cacheDomainRef != null) {
      assistant.useCacheRef(cacheDomainRef.value().getName()); // 構建快取引用,進入分析
    }
  }
public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      Cache cache = configuration.getCache(namespace); // 獲取被引用的快取
      if (cache == null) { //被引用的快取是否存在
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      currentCache = cache; // 設定當前快取物件為被引用的快取物件
      unresolvedCacheRef = false; // 標誌設定為false,代表有快取引用。
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }

由上文可知,當配置了@CacheNamespaceRef和@CacheNamespace後,該mapper檔案對應的快取以@CacheNamespaceRef引用的快取為準。這樣可是使得不同的mapper檔案有相同的快取。

4.2 快取具體使用場景

上文說了,開啟二級快取後,sqlSession中的Executor是CachingExecutor,檢視生成CachingExecutor具體位置。繼續從那段測試程式碼分析

SqlSession sqlSession = factory.openSession(); // 生成SqlSession
List<SysUser> userList = sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser"); // 執行SysUserMapper類的getSysUser方法

debug進入DefaultSqlSessionfactory.openSession()方法

// DefaultSqlSessionfactory.java
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }
  ...
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment(); // 獲取當前配置設定的環境,有事務工廠,資料來源
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 建立事務工廠
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 事務類
      final Executor executor = configuration.newExecutor(tx, execType); // 生成執行器
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  ....

分析Executor executor = configuration.newExecutor(tx, execType);此段程式碼

// Configuration.java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType; // 預設為SimpleExecutor
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    .......
    if (cacheEnabled) {   // 若開啟二級快取,則生成CachingExecutor
      executor = new CachingExecutor(executor);
    }
    .......
  }

當執行查詢語句時,會執行Executor的query()方法。分析CachingExecutor中query()方法究竟是怎樣使用二級快取。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // mapper.xml設定了<cache>或者mapper.java使用了二級快取註解
    Cache cache = ms.getCache();
    if (cache != null) {
      // 若該mapper檔案中執行的上一條語句是更新語句(增刪改),則會清空該mapper檔案對應的二級快取
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")  
        List<E> list = (List<E>) tcm.getObject(cache, key); // 從二級快取中獲取
        if (list == null) { // 若二級快取中不存在
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 呼叫後續的Executor執行語句,後續的Executor會繼續使用一級快取。
          tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks  // 放入二級快取中
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 若沒開啟二級快取,則呼叫後續的Executor執行語句。後續的Executor會繼續使用一級快取。
  }
 
 // 此處的update包括增刪改
 public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 清空二級快取
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

通過上面分析可知,二級快取的實現是mapper級別的。只要對這個mapper檔案使用@CacheNamespace註解或對應的xml使用<cache/>等標籤,那木該mapper在生成時就會註冊一個mapper級別的快取。在後續
對這一mapper檔案任何查詢語句程式操作的時候,都會使用到這個二級快取。二級快取就相當於在一級快取上在加入一個快取。二級快取Cache的實現是在LruCache上在封裝了一層TransactionCache,為了防止髒資料的產生。感興趣的可以自行去檢視。以上便是關於mybatis快取的內容。

4. 總結驗證

我們知道,二級快取是mapper級別的,在mybatis初始化時便生成了。當此mapper檔案中有更新語句時,才會重新整理二級快取。舉個例子,有MapperA.java和MapperB.java兩個檔案,並都開啟了二級快取,cacheA和cacheB。MapperA.java中有一條查詢語句select1,此查詢語句關聯了B的表。在第一次執行MapperA.java中select1時,會從庫中取出資料,並放入在cacheA中。當mapperB.java中如果有一條更新語句update2,執行update2,會重新整理二級快取cacheB。但不會重新整理cacheA,因為update2並不在MapperA.java中。那此時cacheA中存在的資料便是髒資料了。
其實也有解決辦法,即在MapperA.java中使用@CacheNamespaceRef = "mapperB.java".讓兩個檔案公用同一個二級快取。這樣就OK啦

 

 

若對mybatis感興趣的小夥伴,請移步我github專案,從零手寫了一個ORM框架,希望你的star和交流:https://github.com/xbcrh/simple-ibatis 

相關文章