前言
昨天晚上在研究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-Sqlsessionhttp://blog.csdn.net/hupanfeng/article/details/9238127
Mybatis緩存特性的使用及源碼分析,避坑指南~