本文主要是關(guān)于二級緩存的介紹,并著重描述了二級緩存的簡單配置全過程,希望通過本文能讓你對二級緩存有更深的了解。
二級緩存
CPU緩存(Cache Memory)位于CPU與內(nèi)存之間的臨時(shí)存儲(chǔ)器,它的容量比內(nèi)存小但交換速度快。在緩存中的數(shù)據(jù)是內(nèi)存中的一小部分,但這一小部分是短時(shí)間內(nèi)CPU即將訪問的,當(dāng)CPU調(diào)用大量數(shù)據(jù)時(shí),就可避開內(nèi)存直接從緩存中調(diào)用,從而加快讀取速度。最初緩存只有一級,二級緩存(L2 CACHE)出現(xiàn)是為了協(xié)調(diào)一級緩存與內(nèi)存之間的速度。二級緩存比一級緩存速度更慢,容量更大,主要就是做一級緩存和內(nèi)存之間數(shù)據(jù)臨時(shí)交換的地方用。實(shí)際上,現(xiàn)在Intel和AMD處理器在一級緩存的邏輯結(jié)構(gòu)設(shè)計(jì)上有所不同,所以二級緩存對CPU性能的影響也不盡相同。
緩存是指可以進(jìn)行高速數(shù)據(jù)交換的存儲(chǔ)器,它先于內(nèi)存與CPU交換數(shù)據(jù),因此速度很快。L1Cache(一級緩存)是CPU第一層高速緩存。內(nèi)置的L1高速緩存的容量和結(jié)構(gòu)對CPU的性能影響較大,不過高速緩沖存儲(chǔ)器均由靜態(tài)RAM組成,結(jié)構(gòu)較復(fù)雜,在CPU管芯面積不能太大的情況下,L1級高速緩存的容量不可能做得太大。一般L1緩存的容量通常在32—256KB。L2Cache(二級緩存)是CPU的第二層高速緩存,分內(nèi)部和外部兩種芯片。內(nèi)部的芯片二級緩存運(yùn)行速度與主頻相同,而外部的二級緩存則只有主頻的一半。L2高速緩存容量也會(huì)影響CPU的性能,原則是越大越好,現(xiàn)在普通臺(tái)式機(jī)CPU的L2緩存一般為128KB到2MB或者更高,筆記本、服務(wù)器和工作站上用CPU的L2高速緩存最高可達(dá)1MB-3MB。
二級緩存的簡單配置教程詳解
本教程以MyBatis二級緩存設(shè)置為例。
一、創(chuàng)建Cache的完整過程
我們從SqlSessionFactoryBuilder解析mybatis-config.xml配置文件開始:
Reader reader = Resources.getResourceAsReader(“mybatis-config.xml”);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
然后是:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
看parser.parse()方法:
parseConfiguration(parser.evalNode(“/configuration”));
看處理Mapper.xml文件的位置:
mapperElement(root.evalNode(“mappers”));
看處理Mapper.xml的XMLMapperBuilder:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration,
resource, configuration.getSqlFragments());
mapperParser.parse();
繼續(xù)看parse方法:
configurationElement(parser.evalNode(“/mapper”));
到這里:
String namespace = context.getStringAttribute(“namespace”);
if (namespace.equals(“”)) {
throw new BuilderException(“Mapper‘s namespace cannot be empty”);
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode(“cache-ref”));
cacheElement(context.evalNode(“cache”));
從這里看到namespace就是xml中《mapper》元素的屬性。然后下面是先后處理的cache-ref和cache,后面的cache會(huì)覆蓋前面的cache-ref,但是如果一開始cache-ref沒有找到引用的cache,他就不會(huì)被覆蓋,會(huì)一直到最后處理完成為止,最后如果存在cache,反而會(huì)被cache-ref覆蓋。這里是不是看著有點(diǎn)暈、有點(diǎn)亂?所以千萬別同時(shí)配置這兩個(gè),實(shí)際上也很少有人會(huì)這么做。
看看MyBatis如何處理《cache/》:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute(“type”, “PERPETUAL”);
Class《? extends Cache》 typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute(“eviction”, “LRU”);
Class《? extends Cache》 evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute(“flushInterval”);
Integer size = context.getIntAttribute(“size”);
boolean readWrite = !context.getBooleanAttribute(“readOnly”, false);
boolean blocking = context.getBooleanAttribute(“blocking”, false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass,
flushInterval, size, readWrite, blocking, props);
}
}
從源碼可以看到MyBatis讀取了那些屬性,而且很容易可以到這些屬性的默認(rèn)值。
創(chuàng)建Java的cache對象方法為builderAssistant.useNewCache,我們看看這段代碼:
public Cache useNewCache(Class《? extends Cache》 typeClass,
Class《? extends Cache》 evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
從調(diào)用該方法的地方,我們可以看到并沒有使用返回值cache,在后面的過程中創(chuàng)建MappedStatement的時(shí)候使用了currentCache。
二、使用Cache過程
在系統(tǒng)中,使用Cache的地方在CachingExecutor中:
@Override
public 《E》 List《E》 query(
MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
獲取cache后,先判斷是否有二級緩存。
只有通過《cache/》,《cache-ref/》或@CacheNamespace,@CacheNamespaceRef標(biāo)記使用緩存的Mapper.xml或Mapper接口(同一個(gè)namespace,不能同時(shí)使用)才會(huì)有二級緩存。
if (cache != null) {
如果cache存在,那么會(huì)根據(jù)sql配置(《insert》,《select》,《update》,《delete》的flushCache屬性來確定是否清空緩存。
flushCacheIfRequired(ms);
然后根據(jù)xml配置的屬性useCache來判斷是否使用緩存(resultHandler一般使用的默認(rèn)值,很少會(huì)null)。
if (ms.isUseCache() && resultHandler == null) {
確保方法沒有Out類型的參數(shù),mybatis不支持存儲(chǔ)過程的緩存,所以如果是存儲(chǔ)過程,這里就會(huì)報(bào)錯(cuò)。
ensureNoOutParams(ms, parameterObject, boundSql);
沒有問題后,就會(huì)從cache中根據(jù)key來取值:
@SuppressWarnings(“unchecked”)
List《E》 list = (List《E》) tcm.getObject(cache, key);
如果沒有緩存,就會(huì)執(zhí)行查詢,并且將查詢結(jié)果放到緩存中。
if (list == null) {
list = delegate.《E》query(ms, parameterObject,
rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
返回結(jié)果
return list;
}
}
沒有緩存時(shí),直接執(zhí)行查詢
return delegate.《E》query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在上面的代碼中tcm.putObject(cache, key, list);這句代碼是緩存了結(jié)果。但是實(shí)際上直到sqlsession關(guān)閉,MyBatis才以序列化的形式保存到了一個(gè)Map(默認(rèn)的緩存配置)中。
三、Cache使用時(shí)的注意事項(xiàng)
1. 只能在【只有單表操作】的表上使用緩存
不只是要保證這個(gè)表在整個(gè)系統(tǒng)中只有單表操作,而且和該表有關(guān)的全部操作必須全部在一個(gè)namespace下。
2. 在可以保證查詢遠(yuǎn)遠(yuǎn)大于insert,update,delete操作的情況下使用緩存
這一點(diǎn)不需要多說,所有人都應(yīng)該清楚。記住,這一點(diǎn)需要保證在1的前提下才可以!
四、避免使用二級緩存
可能會(huì)有很多人不理解這里,二級緩存帶來的好處遠(yuǎn)遠(yuǎn)比不上他所隱藏的危害。
緩存是以namespace為單位的,不同namespace下的操作互不影響。
insert,update,delete操作會(huì)清空所在namespace下的全部緩存。
通常使用MyBatis Generator生成的代碼中,都是各個(gè)表獨(dú)立的,每個(gè)表都有自己的namespace。
為什么避免使用二級緩存
在符合【Cache使用時(shí)的注意事項(xiàng)】的要求時(shí),并沒有什么危害。
其他情況就會(huì)有很多危害了。
針對一個(gè)表的某些操作不在他獨(dú)立的namespace下進(jìn)行。
例如在UserMapper.xml中有大多數(shù)針對user表的操作。但是在一個(gè)XXXMapper.xml中,還有針對user單表的操作。
這會(huì)導(dǎo)致user在兩個(gè)命名空間下的數(shù)據(jù)不一致。如果在UserMapper.xml中做了刷新緩存的操作,在XXXMapper.xml中緩存仍然有效,如果有針對user的單表查詢,使用緩存的結(jié)果可能會(huì)不正確。
更危險(xiǎn)的情況是在XXXMapper.xml做了insert,update,delete操作時(shí),會(huì)導(dǎo)致UserMapper.xml中的各種操作充滿未知和風(fēng)險(xiǎn)。
有關(guān)這樣單表的操作可能不常見。但是你也許想到了一種常見的情況。
多表操作一定不能使用緩存
為什么不能?
首先不管多表操作寫到那個(gè)namespace下,都會(huì)存在某個(gè)表不在這個(gè)namespace下的情況。
例如兩個(gè)表:role和user_role,如果我想查詢出某個(gè)用戶的全部角色role,就一定會(huì)涉及到多表的操作。
《select id=“selectUserRoles” resultType=“UserRoleVO”》
select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
《/select》
像上面這個(gè)查詢,你會(huì)寫到那個(gè)xml中呢??
不管是寫到RoleMapper.xml還是UserRoleMapper.xml,或者是一個(gè)獨(dú)立的XxxMapper.xml中。如果使用了二級緩存,都會(huì)導(dǎo)致上面這個(gè)查詢結(jié)果可能不正確。
如果你正好修改了這個(gè)用戶的角色,上面這個(gè)查詢使用緩存的時(shí)候結(jié)果就是錯(cuò)的。
這點(diǎn)應(yīng)該很容易理解。
在我看來,就以MyBatis目前的緩存方式來看是無解的。多表操作根本不能緩存。
如果你讓他們都使用同一個(gè)namespace(通過《cache-ref》)來避免臟數(shù)據(jù),那就失去了緩存的意義。
看到這里,實(shí)際上就是說,二級緩存不能用。整篇文章介紹這么多也沒什么用了。
五、挽救二級緩存?
想更高效率的使用二級緩存是解決不了了。
但是解決多表操作避免臟數(shù)據(jù)還是有法解決的。解決思路就是通過攔截器判斷執(zhí)行的sql涉及到那些表(可以用jsqlparser解析),然后把相關(guān)表的緩存自動(dòng)清空。但是這種方式對緩存的使用效率是很低的。
設(shè)計(jì)這樣一個(gè)插件是相當(dāng)復(fù)雜的,既然我沒想著去實(shí)現(xiàn),就不廢話了。
結(jié)語
關(guān)于二級緩存的介紹就到這了,希望本文能讓你對二級緩存有更深的理解,如有不足之處歡迎指正。
評論
查看更多