深入瞭解Mybatis架構設計

女友在高考發表於2021-11-02

架構設計

我們可以把Mybatis的功能架構分為三層:

  1. API介面層:提供給外部使用的介面API,開發人員通過這些本地API來操縱資料庫。介面層一接收到呼叫請求就會呼叫資料處理層來完成具體的資料處理。

Mybatis和資料庫的互動有兩種方式:

  • 使用傳統的Mybatis提供API
  • 使用Mapper代理的方式
  1. 資料處理層:負責具體的SQL查詢、SQL解析、SQL執行和執行結果對映處理等。他主要的目的是根據呼叫的請求完成一次資料庫操作。
  2. 基礎支撐層:負責最基礎的功能支撐,包括連線管理、事務管理、配置載入和快取處理,這些都是共用的東西,將他們抽取出來最為基礎元件。為上層的資料處理層提供最基礎的支撐。

Mybatis主要構件

構件 描述
SqlSession 作為Mybatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪查改功能
Executor Mybatis執行器,是Mybatis排程的核心,負責SQL語句的生成和查詢快取的維護
StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement的操作,如設定引數、將Statement結果集轉換為List集合
ParameterHandler 負責對使用者傳遞的引數轉換為JDBC Statement所需要的引數
ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換為List型別的集合
TypeHandler 負責java資料型別和jdbc資料型別之間的對映和轉換
MappedStatement MappedStatement維護了一條<select、 update 、 delete 、insert >節點的封裝
SqlSource 負責根據使用者傳遞的parameterObject,動態的生成SQL語句,將資訊封裝到BoundSql物件中
BoundSql 表示動態生成的SQL語句以及相應的引數資訊

總體流程:

  1. 載入配置並初始化

配置來源於兩個地方,一個是配置檔案(conf.xml,mapper*.xml),一個是java程式碼中的註解,將配置檔案內容封裝到Configuration,將sql的配置資訊載入成為一個mappedstatement物件,儲存在記憶體中。
2. 接收呼叫請求

觸發條件:呼叫Mybatis提供的API

傳入引數:為SQL的ID和傳入的引數

將請求傳遞給下層的請求處理層進行處理

  1. 處理操作請求
  • 根據SQL的ID查詢對應的MappedStatement物件
  • 根據傳入引數物件解析,得到最終要執行的SQL和執行傳入引數
  • 獲取資料庫連線,將最終SQL語句和引數給到資料庫執行,並得到執行結果
  • 根據MappedStatement物件中的結果對映配置對得到的執行結果進行轉換處理,並得到最終的處理結果
  • 釋放連線資源
  1. 返回處理結果

Mybatis快取

Mybatis有一級快取和二級快取。Mybatis收到查詢請求後首先會查詢二級快取,若二級快取未命中,再去查詢一級快取,一級快取沒有,再查詢資料庫。

一級快取

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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //從localCache快取裡查資料,沒有就去查資料庫
      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);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

這個localCache是BaseExecutor裡面的一個屬性

public abstract class BaseExecutor implements Executor {


  protected PerpetualCache localCache;

PerpetualCache類

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

二級快取

啟用二級快取步驟:

  1. 開啟cacheEnabled(預設開啟)
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
  1. 需要在二級快取的Mapper配置檔案中加入
<cache></cache>
  1. 注意,二級快取要想生效,必須要呼叫sqlSession.commit或close方法
 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      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);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

注意Cache cache = ms.getCache();,這個cache是從MappedStatement中獲取到的,由於MappedStatement存在全域性配置中,可以多個CachingExecutor獲取到,這樣就會出現執行緒安全問題。除此之外,若不加以控制,多個事務共用一個快取例項,會導致髒讀的存在。

那麼mybatis是怎麼解決髒讀的呢?借用了上面的tcm這個變數,也就是TransactionalCacheManager類來解決的。

TransactionalCacheManager類維護了Cache,TransactionalCache的關係,真正的資料還是交由TransactionalCache處理的。

結構如圖:

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

接下來看一下TransactionalCache的程式碼

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  // 真正的快取物件
  private final Cache delegate;
  private boolean clearOnCommit;
  //在事務被提交前,所有從資料庫中查詢的結果將快取在此集合中
  private final Map<Object, Object> entriesToAddOnCommit;
  //在事務被提交前,當快取未命中時,CacheKey 將會被儲存在此集合中
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    this.entriesMissedInCache = new HashSet<Object>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    //獲取快取的時候從delegate裡獲取的
    Object object = delegate.getObject(key);
    if (object == null) {
      //快取未命中,將key存入entriesMissedInCache.
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public void putObject(Object key, Object object) {
    //put的時候只是將資料庫的資料放入到了entriesToAddOnCommit
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    //重新整理未快取的結果到delegate中去
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
      }
    }
  }

}

我們儲存二級快取的時候是放入到TransactionalCache.entriesToAddOnCommit這個map中,但是每次查詢的時候是從delegate查詢的,所以這個二級快取查詢資料庫後,快取是沒有立刻生效的。只有當執行了sqlSession的commit或close方法後,它會呼叫到tcm的commit,在呼叫到transactionlCache的commit,重新整理快取到delegate了。

總結:

  • 二級快取的設計上,大量運用了裝飾器模式,如SynchronizedCache、LoggingCache。
  • 二級快取實現了Sqlsession之間的快取資料共享,屬於namespace級別
  • 二級快取的實現由CachingExecutor和一個事務型預快取TransactionlCache完成。

相關文章