Mybatis介紹之緩存
Mybatis中有一級緩存和二級緩存,默認情況下一級緩存是開啓的,而且是不能關閉的。一級緩存是指SqlSession級別的緩存,當在同一個SqlSession中進行相同的SQL語句查詢時,第二次以後的查詢不會從數據庫查詢,而是直接從緩存中獲取,一級緩存最多緩存1024條SQL。二級緩存是指可以跨SqlSession的緩存。
Mybatis中進行SQL查詢是通過org.apache.ibatis.executor.Executor接口進行的,總體來講,它一共有兩類實現,一類是BaseExecutor,一類是CachingExecutor。前者是非啓用二級緩存時使用的,而後者是採用的裝飾器模式,在啓用了二級緩存時使用,當二級緩存沒有命中時,底層還是通過BaseExecutor來實現的。
1 一級緩存
一級緩存是默認啓用的,在BaseExecutor的query()方法中實現,底層默認使用的是PerpetualCache實現,PerpetualCache採用HashMap存儲數據。一級緩存會在進行增、刪、改操作時進行清除。
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一級緩存的範圍有SESSION和STATEMENT兩種,默認是SESSION,如果我們不需要使用一級緩存,那麼我們可以把一級緩存的範圍指定爲STATEMENT,這樣每次執行完一個Mapper語句後都會將一級緩存清除。如果需要更改一級緩存的範圍,請在Mybatis的配置文件中,在<settings>下通過localCacheScope指定。
<setting name="localCacheScope" value="SESSION"/>
爲了驗證一級緩存,我們進行如下測試,在testCache1中,我們通過同一個SqlSession查詢了兩次一樣的SQL,第二次不會發送SQL。在testCache2中,我們也是查詢了兩次一樣的SQL,但是它們是不同的SqlSession,結果會發送兩次SQL請求。需要注意的是當Mybatis整合Spring後,直接通過Spring注入Mapper的形式,如果不是在同一個事務中每個Mapper的每次查詢操作都對應一個全新的SqlSession實例,這個時候就不會有一級緩存的命中,如有需要可以啓用二級緩存。而在同一個事務中時共用的就是同一個SqlSession。這塊有興趣的朋友可以去查看MapperFactoryBean的源碼,其父類SqlSessionDaoSupport在設置SqlSessionFactory或設置SqlSessionTemplate時的邏輯。
/**
* 默認是有一級緩存的,一級緩存只針對於使用同一個SqlSession的情況。<br/>
* 注意:當使用Spring整合後的Mybatis,不在同一個事務中的Mapper接口對應的操作也是沒有一級緩存的,因爲它們是對應不同的SqlSession。在本示例中如需要下面的第二個語句可使用一級緩存,需要testCache()方法在一個事務中,使用@Transactional標註。
*/
@Test
public void testCache() {
PersonMapper mapper = session.getMapper(PersonMapper.class);
mapper.findById(5L);
mapper.findById(5L);
}
@Test
public void testCache2() {
SqlSession session1 = this.sessionFactory.openSession();
SqlSession session2 = this.sessionFactory.openSession();
session1.getMapper(PersonMapper.class).findById(5L);
session2.getMapper(PersonMapper.class).findById(5L);
}
2 二級緩存
2.1簡介
二級緩存是默認啓用的,如想取消,則可以通過Mybatis配置文件中的元素下的子元素來指定cacheEnabled爲false。
<settings>
<setting name="cacheEnabled" value="false" />
</settings>
cacheEnabled默認是啓用的,只有在該值爲true的時候,底層使用的Executor纔是支持二級緩存的CachingExecutor。具體可參考Mybatis的核心配置類org.apache.ibatis.session.Configuration的newExecutor方法實現。
public Executor newExecutor(Transaction transaction, ExecutorTypeexecutorType) {
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 = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
要使用二級緩存除了上面一個配置外,我們還需要在我們對應的Mapper.xml文件中定義需要使用的cache,具體可以參考CachingExecutor的以下實現,其中使用的cache就是我們在對應的Mapper.xml中定義的cache。還有一個條件就是需要當前的查詢語句是配置了使用cache的,即下面源碼的useCache()是返回true的,默認情況下所有select語句的useCache都是true,如果我們在啓用了二級緩存後,有某個查詢語句是我們不想緩存的,則可以通過指定其useCache爲false來達到對應的效果。
@Override
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);
}
2.2cache定義
剛剛說了我們要想使用二級緩存,是需要在對應的Mapper.xml文件中定義其中的查詢語句需要使用哪個cache來緩存數據的。這有兩種方式可以定義,一種是通過cache元素定義,一種是通過cache-ref元素來定義。但是需要注意的是對於同一個Mapper來講,它只能使用一個Cache,當同時使用了和時使用定義的優先級更高。Mapper使用的Cache是與我們的Mapper對應的namespace綁定的,一個namespace最多隻會有一個Cache與其綁定。
2.2.1cache元素定義
使用cache元素來定義使用的Cache時,最簡單的做法是直接在對應的Mapper.xml文件中指定一個空的元素,這個時候Mybatis會按照默認配置創建一個Cache對象,準備的說是PerpetualCache對象,更準確的說是LruCache對象(底層用了裝飾器模式)。具體可以參考XMLMapperBuilder中的cacheElement()方法中解析cache元素的邏輯。空cache元素定義會生成一個採用最近最少使用算法最多隻能存儲1024個元素的緩存,而且是可讀寫的緩存,即該緩存是全局共享的,任何一個線程在拿到緩存結果後對數據的修改都將影響其它線程獲取的緩存結果,因爲它們是共享的,同一個對象。
cache元素可指定如下屬性,每種屬性的指定都是針對都是針對底層Cache的一種裝飾,採用的是裝飾器的模式。
Ø blocking:默認爲false,當指定爲true時將採用BlockingCache進行封裝,blocking,阻塞的意思,使用BlockingCache會在查詢緩存時鎖住對應的Key,如果緩存命中了則會釋放對應的鎖,否則會在查詢數據庫以後再釋放鎖,這樣可以阻止併發情況下多個線程同時查詢數據,詳情可參考BlockingCache的源碼。
Ø eviction:eviction,驅逐的意思。也就是元素驅逐算法,默認是LRU,對應的就是LruCache,其默認只保存1024個Key,超出時按照最近最少使用算法進行驅逐,詳情請參考LruCache的源碼。如果想使用自己的算法,則可以將該值指定爲自己的驅逐算法實現類,只需要自己的類實現Mybatis的Cache接口即可。除了LRU以外,系統還提供了FIFO(先進先出,對應FifoCache)、SOFT(採用軟引用存儲Value,便於垃圾回收,對應SoftCache)和WEAK(採用弱引用存儲Value,便於垃圾回收,對應WeakCache)這三種策略。
Ø flushInterval:清空緩存的時間間隔,單位是毫秒,默認是不會清空的。當指定了該值時會再用ScheduleCache包裝一次,其會在每次對緩存進行操作時判斷距離最近一次清空緩存的時間是否超過了flushInterval指定的時間,如果超出了,則清空當前的緩存,詳情可參考ScheduleCache的實現。
Ø readOnly:是否只讀,默認爲false。當指定爲false時,底層會用SerializedCache包裝一次,其會在寫緩存的時候將緩存對象進行序列化,然後在讀緩存的時候進行反序列化,這樣每次讀到的都將是一個新的對象,即使你更改了讀取到的結果,也不會影響原來緩存的對象,即非只讀,你每次拿到這個緩存結果都可以進行修改,而不會影響原來的緩存結果;當指定爲true時那就是每次獲取的都是同一個引用,對其修改會影響後續的緩存數據獲取,這種情況下是不建議對獲取到的緩存結果進行更改,意爲只讀。這是Mybatis二級緩存讀寫和只讀的定義,可能與我們通常情況下的只讀和讀寫意義有點不同。每次都進行序列化和反序列化無疑會影響性能,但是這樣的緩存結果更安全,不會被隨意更改,具體可根據實際情況進行選擇。詳情可參考SerializedCache的源碼。
Ø size:用來指定緩存中最多保存的Key的數量。其是針對LruCache而言的,LruCache默認只存儲最多1024個Key,可通過該屬性來改變默認值,當然,如果你通過eviction指定了自己的驅逐算法,同時自己的實現裏面也有setSize方法,那麼也可以通過cache的size屬性給自定義的驅逐算法裏面的size賦值。
Ø type:type屬性用來指定當前底層緩存實現類,默認是PerpetualCache,如果我們想使用自定義的Cache,則可以通過該屬性來指定,對應的值是我們自定義的Cache的全路徑名稱。
2.2.2cache-ref元素定義
cache-ref元素可以用來指定其它Mapper.xml中定義的Cache,有的時候可能我們多個不同的Mapper需要共享同一個緩存的,是希望在MapperA中緩存的內容在MapperB中可以直接命中的,這個時候我們就可以考慮使用cache-ref,這種場景只需要保證它們的緩存的Key是一致的即可命中,二級緩存的Key是通過Executor接口的createCacheKey()方法生成的,其實現基本都是BaseExecutor,源碼如下。
public CacheKey createCacheKey(MappedStatement ms, ObjectparameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings =boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry =ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject =configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
打個比方我想在PersonMapper.xml中的查詢都使用在UserMapper.xml中定義的Cache,則可以通過cache-ref元素的namespace屬性指定需要引用的Cache所在的namespace,即UserMapper.xml中的定義的namespace,假設在UserMapper.xml中定義的namespace是com.elim.learn.mybatis.dao.UserMapper,則在PersonMapper.xml的cache-ref應該定義如下。
2.3自定義cache
前面提到Mybatis的Cache默認會使用PerpetualCache存儲數據,如果我們不想按照它的邏輯實現,或者我們想使用其它緩存框架來實現,比如使用Ehcache、Redis等,這個時候我們就可以使用自己的Cache實現,Mybatis是給我們留有對應的接口,允許我們進行自定義的。要想實現自定義的Cache我們必須定義一個自己的類來實現Mybatis提供的Cache接口,實現對應的接口方法。注意,自定義的Cache必須包含一個接收一個String參數的構造方法,這個參數就是Cache的ID,詳情請參考Mybatis初始化Cache的過程,對應XMLMapperBuilder的cacheElement()方法。以下是一個簡單的MyCache的實現。
/**
* @author Elim
* 2016年12月20日
*/
publicclass MyCache implements Cache {
private String id;
private String name;//Name,故意加這麼一個屬性,以方便演示給自定義Cache的屬性設值
private Map<Object, Object> cache = new HashMap<Object, Object>();
/**
* 構造方法。自定義的Cache實現一定要有一個id參數
* @param id
*/
public MyCache(String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return this.cache.get(key);
}
@Override
public Object removeObject(Object key) {
return this.cache.remove(key);
}
@Override
public void clear() {
this.cache.clear();
}
@Override
public int getSize() {
return this.cache.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
}
定義了自己的Cache實現類後我們就可以在需要使用它的Mapper.xml文件中通過<cache>標籤的type屬性來指定我們需要使用的Cache。如果我們的自定義Cache是需要指定參數的,則可以通過<cache>標籤的子標籤<property>來指定對應的參數,Mybatis在解析的時候會調用指定屬性對應的set方法。針對於上面的自定義Cache,我們的配置如下。
<cache type="com.elim.learn.mybatis.cache.MyCache">
<property name="name" value="調用setName()方法需要傳遞的參數值"/>
</cache>
圓角矩形: 注意:如果我們使用了自定義的Cache,那麼cache標籤的其它屬性,如size、eviction等都不會對自定義的Cache起作用,也就是說不會自動對自定義的Cache進行包裝,如果需要使用自定義的Cache,同時又希望使用Mybatis自帶的那些Cache包裝類,則可以在自定義的Cache中自己進行包裝。
2.4緩存的清除
二級緩存默認是會在執行update、insert和delete語句時進行清空的,具體可以參考CachingExecutor的update()實現。如果我們不希望在執行某一條更新語句時清空對應的二級緩存,那麼我們可以在對應的語句上指定flushCache屬性等於false。
<insert id="delete" parameterType="java.lang.Long"flushCache="false">
delete t_person where id=#{id}
</insert>
2.5自己操作Cache
Mybatis中創建的二級緩存都會交給Configuration進行管理,Configuration類是Mybatis的核心類,裏面包含了各種Mybatis資源的管理,其可以很方便的通過SqlSession、SqlSessionFactory獲取,如有需要我們可以直接通過它來操作我們的Cache。
@Test
public void testGetCache() {
Configuration configuration = this.session.getConfiguration();
// this.sessionFactory.getConfiguration();
Collection<Cache> caches = configuration.getCaches();
System.out.println(caches);
}
2.6測試
針對二級緩存進行了以下測試,獲取兩個不同的SqlSession執行兩條相同的SQL,在未指定Cache時Mybatis將查詢兩次數據庫,在指定了Cache時Mybatis只查詢了一次數據庫,第二次是從緩存中拿的。
@Test
public void testCache2() {
SqlSession session1 = this.sessionFactory.openSession();
SqlSession session2 = this.sessionFactory.openSession();
session1.getMapper(PersonMapper.class).findById(5L);
session1.commit();
session2.getMapper(PersonMapper.class).findById(5L);
}
注意在上面的代碼中,我在session1執行完對應的SQL後調用了session1的commit()方法,即提交了它的事務,這樣我們在第二次查詢的時候纔會緩存命中,纔不會查詢數據庫,否則就會連着查詢兩次數據庫。這是因爲在CachingExecutor中Mybatis在查詢的過程中又在原來Cache的基礎上包裝了TransactionalCache,這個Cache只會在事務提交後才真正的寫入緩存,所以在上面的示例中,如果session1執行完SQL後沒有馬上commit就緊接着用session2執行SQL,雖然session1查詢時沒有緩存命中,但是此時寫入緩存操作還沒有進行,session2再查詢的時候也就不會緩存命中了。