<MyBatis緩存機制>一級緩存源碼淺析

前言

昨天晚上在研究mysql不同級別日誌的時候,發現了很多髒讀情況,一些明明已經被測試用例修改過的數據在讀取後還是原值,不用說,肯定是緩存在作祟。百度了一些資料,發現Mybatis和Hibernate一樣,也是分級緩存,於是想着藉此機會研究一下Mybatis自帶的緩存機制,看了一會兒源碼,把自己的一些淺見在這裏記錄一下,希望能給看到的人一點啓示。如果這篇源碼導讀有什麼地方邏輯不是很清楚的,歡迎各位看官積極指正,通過大家的建議不斷完善自己的表達思維也是很重要的。


SqlSession

爲什麼會首先提到這個,因爲博主網上百度了一陣子後,發現Mybatis的所謂一級緩存,實際上就是一個Session級別的緩存,且瞭解到的測試一級緩存的代碼如下:

String resource = "spring-mybatis.xml";
// 通過Mybatis包中的Resources對象很輕鬆的獲取到配置文件
Reader reader = Resources.getResourceAsReader(resource);
// 通過SqlSessionFactoryBuilder創建
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 獲得session實例
SqlSession session = sqlSessionFactory.openSession();
//測試實例
AuthorMapper authorMapper = session.getMapper(AuthorMapper.class);

看到這裏,我們不難發現,SqlSession需要通過系統讀取spring-mybatis.xml中配置的sqlSessionFactory來創建。於是我找到了jar包內關於sqlSessionFactory的源碼。順藤摸瓜,找到了SqlSessionFactory接口的一個實現類DefaultSqlSessionFactory。
這裏寫圖片描述

紅框中的方法即是openSession的具體實現,通過連接和通過數數據源兩種方式獲取Session。

// 獲得session實例
SqlSession session = sqlSessionFactory.openSession();

我們來看一下具體實現的代碼。

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,
                    autoCommit);
            return new DefaultSqlSession(configuration, executor);
        } 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();
        }
    }

從方法的具體實現代碼中,我們可以發現SqlSession具體做了下面幾件事情:
1) 從配置中獲取Environment;
2) 從Environment中取得DataSource;
3) 從Environment中取得TransactionFactory;
4) 從DataSource裏獲取數據庫連接對象Connection;
5) 通過DataSource創建事務對象Transaction;
6) 創建Executor對象
7) 創建sqlsession對象。

一個個查看這些創建的對象,我發現SqlSession關於緩存的所有操作都是由一個Executor接口的實現類BaseExecutor完成的。
這裏寫圖片描述
找到了目標,接下來就是好好研究它實現的機制。


BaseExecutor

查看BaseExecutor的成員,我驚喜地發現了本地緩存localCache的身影。

protected PerpetualCache localCache;

點擊PerpetualCache 查看緩存的具體實現,代碼如下:

public class PerpetualCache implements Cache {

  private String id;

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

  private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

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

我們可以發現Mybatis的緩存實際上也是一個HashMap(爲什麼要說也。。。)
好了,不說cache對象的問題了,回到Executor,SqlSession把具體的查詢職責全權委託給了Executor。按照配置條件一步一步往下找,循着判斷的配置,我們可以發現如果開啓了一級緩存的話(默認開啓,這個後面會再提到),首先會進入BaseExecutor的query方法。代碼如下所示:

@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.");
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      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();
      }
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // issue #482
      }
    }
    return list;
  }

上面這段代碼有兩個值得關注的關鍵點。

第一點是try包裹的那部分代碼,我們可以看到請求會先去localCache裏找,如果在localCache中未命中的話,纔會進入queryFromDatabase方法,連接數據庫獲取對象,同時在queryFromDatabase中將新生成的cache鍵值key寫入localCache。

localCache.putObject(key, EXECUTION_PLACEHOLDER);

這一點來說,和Hibernate很像,通過一個記錄K(sql),V(statement)鍵值對的HashMap管理一級緩存。

第二點是下面這一段

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // issue #482
      }

通過這段代碼我們可以看到,如果一級緩存的級別是STATEMENT,那麼就會清理本地緩存。這就呼應了我前面說到的一點,爲什麼我說一級緩存是默認開啓的呢,請看Configuration類中的一個成員變量:

protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

我們發現configuration.getLocalCacheScope()的值默認是SESSION,所以結合上面的if條件,只有這個值爲STATEMENT時,我們在查詢的時候纔會清理緩存,不會讀出髒數據。我在文章開頭提到的問題也就迎刃而解了,那就是讓LocalCacheScope ==STATEMENT。

點開LocalCacheScope,我們發現這是一個枚舉類。

public enum LocalCacheScope {
  SESSION,STATEMENT
}

果然不出所料,STATEMENT就是選項之一,那麼問題來了,這個值得選項應該在哪裏配置呢?
還記得configuration是從哪裏讀取的嗎?打開你的spring-mybatis.xml也就是配置sqlSessionFactory的地方。

<!-- 配置sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 實例化sqlSessionFactory時需要使用上述配置好的數據源以及SQL映射文件 -->
        <property name="dataSource" ref="dataSource" />
        <!-- 文件映射器,指定類文件 -->
        <property name="configLocation" value="classpath:mybatis/configuration.xml"/>  
        <property name="mapperLocations" value="classpath:com/lhx/x/sqlmap/*.xml" />
    </bean>

找到配置configLocation的property,在這個引入的配置文件configuration.xml下的settings標籤內加入如下代碼:

<setting name="localCacheScope" value="STATEMENT"/>

好了,再去試試SqlSession下的查詢還會出現髒讀數據嗎?

總結

1.Mybatis的緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念,同時只是使用了默認的hashmap,也沒有做容量上的限定。

2.Mybatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,有操作數據庫寫的話,會引起髒數據,建議是把一級緩存的默認級別設定爲Statement,即不使用一級緩存。

參考文章

十分感謝!

https://my.oschina.net/kailuncen/blog/1334771
深入淺出MyBatis-Sqlsession

http://blog.csdn.net/hupanfeng/article/details/9238127
Mybatis緩存特性的使用及源碼分析,避坑指南~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章