Mybatis二級緩存原理
記錄是一種精神,是加深理解最好的方式之一。
最近看了下Mybatis的源碼,分析了二級緩存的實現方式,在這里把他記下來。雖然這不復雜,對這方面的博客也有很多,寫的也很好。但我堅信看懂了是其一,能夠教別人或者描述清楚記下來才能真正的掌握。
這篇文章能夠幫你
- 學會對Mybatis配置二級緩存
- 學會Mybatis二級緩存的實現方式
- 學會整合外部緩存框架(如: Ehcache )
- 學會自定義二級緩存
1. Mybatis內部二級緩存的配置
要使用Mybatis的二級緩存,需要對Mybatis進行配置,配置分三步
- Mybatis全局配置中啟用二級緩存配置
<setting name="cacheEnabled" value="true"/>
- 在對應的Mapper.xml中配置cache節點
<mapper namespace="userMapper"> <cache /> <result ... /> <select ... /> </mapper>
-
在對應的select查詢節點中添加useCache=true
<select id="findUserById" parameterType="int" resultMap="user" useCache="true"> select * from users where id=#{id}; </select>
-
高級配置
a. 為每一個Mapper分配一個Cache緩存對象(使用<cache>節點配置)
b. 多個Mapper共用一個Cache緩存對象(使用<cache-ref>節點配置)
只要簡單的三步配置即可開啟Mybatis的二級緩存了。在使用mybatis查詢時候("userMapper.findUserById"),不同會話(Sqlsession)在查詢時候,只會查詢數據庫一次,第二次會從二級緩存中讀取。
@Before
public void before() {
String mybatisConfigFile = "MybatisConfig/Mybatis-conf.xml";
InputStream stream = TestMybatis.class.getClassLoader().getResourceAsStream(mybatisConfigFile);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream); //構建sqlSession的工廠
}
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
User i = sqlSession.selectOne("userMapper.findUserById", 1);
System.out.println(i);
sqlSession.close();
sqlSession = sqlSessionFactory.openSession();
User x = sqlSession.selectOne("userMapper.findUserById", 1); // 讀取二級緩存數據
System.out.println(x);
sqlSession.close();
}
2. Mybatis內部二級緩存的設計及工作模式
首先我們要知道,mybatis的二級緩存是通過CacheExecutor實現的。CacheExecutor其實是Executor的代理對象。所有的查詢操作,在CacheExecutor中都會先匹配緩存中是否存在,不存在則查詢數據庫。
3. 內部二級緩存的實現詳解
竟然知道Mybatis二級緩存是通過CacheExecotur實現的,那看下Mybatis中創建Executor的過程
// 創建執行器(Configuration.newExecutor)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//確保ExecutorType不為空(defaultExecutorType有可能為空)
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) { //重點在這里,如果啟用全局代理對象,返回Executor的Cache包裝類對象
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
重點在cacheEnabled這個參數。如果你看了我的文章[ Mybatis配置文件解析過程詳解 ],就應該知道了怎么設置cacheEnabled。對,就是此文章第一點說的開啟Mybatis的全局配置項。我們繼續看下CachingExecutor具體怎么實現的。
public class CachingExecutor implements Executor {
private Executor delegate;
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms); //是否需要更緩存
return delegate.update(ms, parameterObject); //更新數據
}
......
}
很清晰,靜態代理模式。在CachingExecutor的所有操作都是通過調用內部的delegate對象執行的。緩存只應用于查詢,我們看下CachingExecutor的query方法。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//創建緩存值
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//獲取記錄
return 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();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// ensureNoOutParams
if (ms.getStatementType() == StatementType.CALLABLE) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
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); // 結果保存到緩存中
}
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果MappedStatement中對應的Cache存在,并且對于的查詢開啟了二級緩存(useCache="true"),那么在CachingExecutor中會先從緩存中根據CacheKey獲取數據,如果緩存中不存在則從數據庫獲取。這里的代碼很簡單,很容易理解。
說到緩存,有效期和緩存策略不得不提。在Mybatis中二級緩存也實現了有效期的控制和緩存策略。Mybatis中是使用責任鏈模式實現的,具體可以看下mybatis的cache包
具體于配置如下:
<cache eviction="FIFO|LRU|SOFT|WEAK" flushInterval="300" size="100" />
對應具體實現源碼可以參考CacheBuilder類的源碼。
public Cache build() {
if (implementation == null) { //緩存實現類
implementation = PerpetualCache.class;
if (decorators.size() == 0) {
decorators.add(LruCache.class);
}
}
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 采用默認緩存包裝類
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
4. 一級緩存和二級緩存的使用順序
如果你的MyBatis使用了二級緩存,并且你的Mapper和select語句也配置使用了二級緩存,那么在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次才是一級緩存,即MyBatis查詢數據的順序是:
二級緩存 ———> 一級緩存——> 數據庫
5. mybatis二級緩存和分頁插件同時使用產生的問題
問題:分頁插件開啟二級緩存后,分頁查詢時無論查詢哪一頁都返回第一頁的數據
在之前講解Mybatis的執行流程的時候提到,在開啟cache的前提下,Mybatis的executor會先從緩存里讀取數據,讀取不到才去數據庫查詢。問題就出在這里,sql自動生成插件和分頁插件執行的時機是在statementhandler里,而statementhandler是在executor之后執行的,無論sql自動生成插件和分頁插件都是通過改寫sql來實現的,executor在生成讀取cache的key(key由sql以及對應的參數值構成)時使用都是原始的sql,這樣當然就出問題了。
找到問題的原因后,解決起來就方便了。只要通過攔截器改寫executor里生成key的方法,在生成可以時使用自動生成的sql(對應sql自動生成插件)或加入分頁信息(對應分頁插件)就可以了。
參考: http://blog.csdn.net/hupanfeng/article/details/16950161
6. mybatis整合第三方緩存框架
我們以ehcache為例。對于ehcache我只會簡單的使用。這里我只是介紹Mybatis怎么使用ehcache,不對ehcache配置作說明。我們知道,在配置二級緩存時候,我們可以指定對應的實現類。這里需要mybatis-ehcache-1.0.3.jar這個jar包。在Mapper中我們只要配置如下即可。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
當然,項目中ehcache的配置還是需要的。
小結
對于Mybatis整合第三方的緩存,實現騎士很簡單,只要在配置的地方制定實現類即可。
Mybatis默認二級緩存的實現在集群或者分布式部署下是有問題的,Mybatis默認緩存只在當節點內有效,并且對緩存的失效操作無法同步的其他節點。需要整合第三方分布式緩存實現,如ehcache或者自定義實現。
來自:http://www.jianshu.com/p/5ff874fa696f