Java編程MyBatis一級緩存和二級緩存的區別,了解一下?

鎮樓小姐姐

可獲得兩大新人禮包

36份一線互聯網Java面試電子書

84個Java稀缺面試題視頻

MyBatis自帶的緩存有一級緩存和二級緩存

一級緩存

Mybatis的一級緩存是指Session緩存。一級緩存的作用域默認是一個SqlSession。Mybatis默認開啟一級緩存。

也就是在同一個SqlSession中,執行相同的查詢SQL,第一次會去數據庫進行查詢,並寫到緩存中;

第二次以後是直接去緩存中取。

當執行SQL查詢中間發生了增刪改的操作,MyBatis會把SqlSession的緩存清空。

一級緩存的範圍有SESSION和STATEMENT兩種,默認是SESSION,如果不想使用一級緩存,可以把一級緩存的範圍指定為STATEMENT,這樣每次執行完一個Mapper中的語句後都會將一級緩存清除。

如果需要更改一級緩存的範圍,可以在Mybatis的配置文件中,在下通過localCacheScope指定。

<setting>

建議不需要修改

需要注意的是

當Mybatis整合Spring後,直接通過Spring注入Mapper的形式,如果不是在同一個事務中每個Mapper的每次查詢操作都對應一個全新的SqlSession實例,這個時候就不會有一級緩存的命中,但是在同一個事務中時共用的是同一個SqlSession。

如有需要可以啟用二級緩存。

二級緩存

Mybatis的二級緩存是指mapper映射文件。二級緩存的作用域是同一個namespace下的mapper映射文件內容,多個SqlSession共享。Mybatis需要手動設置啟動二級緩存。

二級緩存是默認啟用的(要生效需要對每個Mapper進行配置),如想取消,則可以通過Mybatis配置文件中的元素下的子元素來指定cacheEnabled為false。

<settings>

<setting>

cacheEnabled默認是啟用的,只有在該值為true的時候,底層使用的Executor才是支持二級緩存的CachingExecutor。具體可參考Mybatis的核心配置類org.apache.ibatis.session.Configuration的newExecutor方法實現。

可以通過源碼看看

... public Executor newExecutor(Transaction transaction) { return this.newExecutor(transaction, this.defaultExecutorType);

} public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

executorType = executorType == null ? this.defaultExecutorType : executorType;

executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

Object 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 (this.cacheEnabled) {//設置為true才執行的

executor = new CachingExecutor((Executor)executor);

}

Executor executor = (Executor)this.interceptorChain.pluginAll(executor); return executor;

}

...

要使用二級緩存除了上面一個配置外,我們還需要在我們每個DAO對應的Mapper.xml文件中定義需要使用的cache

...

<mapper>

<cache>

...

具體可以看org.apache.ibatis.executor.CachingExecutor類的以下實現

其中使用的cache就是我們在對應的Mapper.xml中定義的cache。

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

BoundSql boundSql = ms.getBoundSql(parameterObject);

CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql); return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

} public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

Cache cache = ms.getCache(); if (cache != null) {//第一個條件 定義需要使用的cache

this.flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) {//第二個條件 需要當前的查詢語句是配置了使用cache的,即下面源碼的useCache()是返回true的 默認是true

this.ensureNoOutParams(ms, parameterObject, boundSql);

List list = (List)this.tcm.getObject(cache, key); if (list == null) {

list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); this.tcm.putObject(cache, key, list);

} return list;

}

} return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

還有一個條件就是需要當前的查詢語句是配置了使用cache的,即上面源碼的useCache()是返回true的,默認情況下所有select語句的useCache都是true,如果我們在啟用了二級緩存後,有某個查詢語句是我們不想緩存的,則可以通過指定其useCache為false來達到對應的效果。

如果我們不想該語句緩存,可使用useCache=”false

<select>

select

<include>

from tuser

where id = #{id,jdbcType=VARCHAR}

cache定義的兩種使用方式

上面說了要想使用二級緩存,需要在每個DAO對應的Mapper.xml文件中定義其中的查詢語句需要使用cache來緩存數據的。

這有兩種方式可以定義,一種是通過cache元素定義,一種是通過cache-ref元素來定義。

需要注意的是

對於同一個Mapper來講,只能使用一個Cache,當同時使用了和時,定義的優先級更高(後面的代碼會給出原因)。

Mapper使用的Cache是與我們的Mapper對應的namespace綁定的,一個namespace最多隻會有一個Cache與其綁定。

cache元素定義

使用cache元素來定義使用的Cache時,最簡單的做法是直接在對應的Mapper.xml文件中指定一個空的元素(看前面的代碼),這個時候Mybatis會按照默認配置創建一個Cache對象,準備的說是PerpetualCache對象,更準確的說是LruCache對象(底層用了裝飾器模式)。

具體的可看org.apache.ibatis.builder.xml.XMLMapperBuilder中的cacheElement()方法解析cache元素的邏輯。

... private void configurationElement(XNode context) { try {

String namespace = context.getStringAttribute("namespace"); if (namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty");

} else { this.builderAssistant.setCurrentNamespace(namespace); this.cacheRefElement(context.evalNode("cache-ref")); this.cacheElement(context.evalNode("cache"));//執行在後面

this.parameterMapElement(context.evalNodes("/mapper/parameterMap")); this.resultMapElements(context.evalNodes("/mapper/resultMap")); this.sqlElement(context.evalNodes("/mapper/sql")); this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

}

} catch (Exception var3) { throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);

}

}

... private void cacheRefElement(XNode context) { if (context != null) { this.configuration.addCacheRef(this.builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));

CacheRefResolver cacheRefResolver = new CacheRefResolver(this.builderAssistant, context.getStringAttribute("namespace")); try {

cacheRefResolver.resolveCacheRef();

} catch (IncompleteElementException var4) { this.configuration.addIncompleteCacheRef(cacheRefResolver);

}

}

} private void cacheElement(XNode context) throws Exception { if (context != null) {

String type = context.getStringAttribute("type", "PERPETUAL");

Class extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);

String eviction = context.getStringAttribute("eviction", "LRU");

Class extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction);

Long flushInterval = context.getLongAttribute("flushInterval");

Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false).booleanValue();

Properties props = context.getChildrenAsProperties(); this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);//如果同時存在<cache>和<cache-ref>,這裡的設置會覆蓋前面的cache-ref的緩存/<cache-ref>/<cache>

}

}

空cache元素定義會生成一個採用最近最少使用算法最多隻能存儲1024個元素的緩存,而且是可讀寫的緩存,即該緩存是全局共享的,任何一個線程在拿到緩存結果後對數據的修改都將影響其它線程獲取的緩存結果,因為它們是共享的,同一個對象。

cache元素可指定如下屬性,每種屬性的指定都是針對都是針對底層Cache的一種裝飾,採用的是裝飾器的模式。

blocking:默認為false,當指定為true時將採用BlockingCache進行封裝,blocking,阻塞的意思,使用BlockingCache會在查詢緩存時鎖住對應的Key,如果緩存命中了則會釋放對應的鎖,否則會在查詢數據庫以後再釋放鎖,這樣可以阻止併發情況下多個線程同時查詢數據,詳情可參考BlockingCache的源碼。簡單理解,也就是設置true時,在進行增刪改之後的併發查詢,只會有一條去數據庫查詢,而不會併發eviction:eviction,驅逐的意思。也就是元素驅逐算法,默認是LRU,對應的就是LruCache,其默認只保存1024個Key,超出時按照最近最少使用算法進行驅逐,詳情請參考LruCache的源碼。如果想使用自己的算法,則可以將該值指定為自己的驅逐算法實現類,只需要自己的類實現Mybatis的Cache接口即可。除了LRU以外,系統還提供了FIFO(先進先出,對應FifoCache)、SOFT(採用軟引用存儲Value,便於垃圾回收,對應SoftCache)和WEAK(採用弱引用存儲Value,便於垃圾回收,對應WeakCache)這三種策略。這裡,根據個人需求選擇了,沒什麼要求的話,默認的LRU即可flushInterval:清空緩存的時間間隔,單位是毫秒,默認是不會清空的。當指定了該值時會再用ScheduleCache包裝一次,其會在每次對緩存進行操作時判斷距離最近一次清空緩存的時間是否超過了flushInterval指定的時間,如果超出了,則清空當前的緩存,詳情可參考ScheduleCache的實現。readOnly:是否只讀默認為false。當指定為false時,底層會用SerializedCache包裝一次,其會在寫緩存的時候將緩存對象進行序列化,然後在讀緩存的時候進行反序列化,這樣每次讀到的都將是一個新的對象,即使你更改了讀取到的結果,也不會影響原來緩存的對象,即非只讀,你每次拿到這個緩存結果都可以進行修改,而不會影響原來的緩存結果;當指定為true時那就是每次獲取的都是同一個引用,對其修改會影響後續的緩存數據獲取,這種情況下是不建議對獲取到的緩存結果進行更改,意為只讀(不建議設置為true)。這是Mybatis二級緩存讀寫和只讀的定義,可能與我們通常情況下的只讀和讀寫意義有點不同。每次都進行序列化和反序列化無疑會影響性能,但是這樣的緩存結果更安全,不會被隨意更改,具體可根據實際情況進行選擇。詳情可參考SerializedCache的源碼。size:用來指定緩存中最多保存的Key的數量。其是針對LruCache而言的,LruCache默認只存儲最多1024個Key,可通過該屬性來改變默認值,當然,如果你通過eviction指定了自己的驅逐算法,同時自己的實現裡面也有setSize方法,那麼也可以通過cache的size屬性給自定義的驅逐算法裡面的size賦值。type:type屬性用來指定當前底層緩存實現類,默認是PerpetualCache,如果我們想使用自定義的Cache,則可以通過該屬性來指定,對應的值是我們自定義的Cache的全路徑名稱。

cache-ref元素定義

cache-ref元素可以用來指定其它Mapper.xml中定義的Cache,有的時候可能我們多個不同的Mapper需要共享同一個緩存的

是希望在MapperA中緩存的內容在MapperB中可以直接命中的,這個時候我們就可以考慮使用cache-ref,這種場景只需要保證它們的緩存的Key是一致的即可命中,二級緩存的Key是通過Executor接口的createCacheKey()方法生成的,其實現基本都是BaseExecutor,源碼如下。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (this.closed) { throw new ExecutorException("Executor was closed.");

} else {

CacheKey cacheKey = new CacheKey();

cacheKey.update(ms.getId());

cacheKey.update(rowBounds.getOffset());

cacheKey.update(rowBounds.getLimit());

cacheKey.update(boundSql.getSql());

List<parametermapping> parameterMappings = boundSql.getParameterMappings();/<parametermapping>

TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for(int i = 0; i < parameterMappings.size(); ++i) {

ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) {

String propertyName = parameterMapping.getProperty();

Object value; 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 = this.configuration.newMetaObject(parameterObject);

value = metaObject.getValue(propertyName);

}

cacheKey.update(value);

}

} return cacheKey;

}

}

打個比方我想在MenuMapper.xml中的查詢都使用在UserMapper.xml中定義的Cache,則可以通過cache-ref元素的namespace屬性指定需要引用的Cache所在的namespace,即UserMapper.xml中的定義的namespace,假設在UserMapper.xml中定義的namespace是cn.chenhaoxiang.dao.UserMapper,則在MenuMapper.xml的cache-ref應該定義如下。

<cache-ref>

1

這樣這兩個Mapper就共享同一個緩存了

自定義cache就不介紹了。

測試二級緩存

查詢測試

/**

* Created with IntelliJ IDEA.

* User: 陳浩翔.

* Date: 2018/1/10.

* Time: 下午 10:15.

* Explain:

*/@RunWith(SpringJUnit4ClassRunner.class)//配置了@ContextConfiguration註解並使用該註解的locations屬性指明spring和配置文件之後@ContextConfiguration(locations = {"classpath:spring.xml","classpath:spring-mybatis.xml"})public class MyBatisTestBySpringTestFramework {

//注入userService

@Autowired

private UserService userService; @Test

public void testGetUserId(){

String userId = "4e07f3963337488e81716cfdd8a0fe04";

User user = userService.getUserById(userId);

System.out.println(user); //前面說到spring和MyBatis整合

User user2 = userService.getUserById(userId);

System.out.println("user2:"+user2);

}

}

接下來我們把Mapper中的cache元素刪除,不使用二級緩存

再運行測試

對二級緩存進行了以下測試,獲取兩個不同的SqlSession(前面有說,Spring和MyBatis集成,每次都是不同的SqlSession)執行兩條相同的SQL,在未指定Cache時Mybatis將查詢兩次數據庫,在指定了Cache時Mybatis只查詢了一次數據庫,第二次是從緩存中拿的。

Cache Hit Ratio 表示緩存命中率。

開啟二級緩存後,每執行一次查詢,系統都會計算一次二級緩存的命中率。

第一次查詢也是先從緩存中查詢,只不過緩存中一定是沒有的。

所以會再從DB中查詢。由於二級緩存中不存在該數據,所以命中率為0.但第二次查詢是從二級緩存中讀取的,所以這一次的命中率為1/2=0.5。

當然,若有第三次查詢,則命中率為1/3=0.66

0.5這個值可以從上面開啟cache的圖看出來,0.0的值未截取到~漏掉了~

注意:

增刪改操作,無論是否進行提交sqlSession.commit(),均會清空一級、二級緩存,使查詢再次從DB中select。

說明:

二級緩存的清空,實質上是對所查找key對應的value置為null,而非將

二級緩存的使用原則

只能在一個命名空間下使用二級緩存由於二級緩存中的數據是基於namespace的,即不同namespace中的數據互不干擾。在多個namespace中若均存在對同一個表的操作,那麼這多個namespace中的數據可能就會出現不一致現象。在單表上使用二級緩存如果一個表與其它表有關聯關係,那麼久非常有可能存在多個namespace對同一數據的操作。而不同namespace中的數據互補干擾,所以就有可能出現多個namespace中的數據不一致現象。查詢多於修改時使用二級緩存在查詢操作遠遠多於增刪改操作的情況下可以使用二級緩存。因為任何增刪改操作都將刷新二級緩存,對二級緩存的頻繁刷新將降低系統性能。