一、創(chuàng)建cache的完整過(guò)程
我們從sqlsessionfactorybuilder解析mybatis-config.xml配置文件開(kāi)始:
1
2
|
reader reader = resources.getresourceasreader( "mybatis-config.xml" ); sqlsessionfactory sqlsessionfactory = new sqlsessionfactorybuilder().build(reader); |
然后是:
1
2
|
xmlconfigbuilder parser = new xmlconfigbuilder(inputstream, environment, properties); return build(parser.parse()); |
看parser.parse()方法:
1
|
parseconfiguration(parser.evalnode( "/configuration" )); |
看處理mapper.xml文件的位置:
1
|
mapperelement(root.evalnode( "mappers" )); |
看處理mapper.xml的xmlmapperbuilder:
1
2
3
|
xmlmapperbuilder mapperparser = new xmlmapperbuilder(inputstream, configuration, resource, configuration.getsqlfragments()); mapperparser.parse(); |
繼續(xù)看parse方法:
1
|
configurationelement(parser.evalnode( "/mapper" )); |
到這里:
1
2
3
4
5
6
7
|
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,但是如果一開(kāi)始cache-ref沒(méi)有找到引用的cache,他就不會(huì)被覆蓋,會(huì)一直到最后處理完成為止,最后如果存在cache,反而會(huì)被cache-ref覆蓋。這里是不是看著有點(diǎn)暈、有點(diǎn)亂?所以千萬(wàn)別同時(shí)配置這兩個(gè),實(shí)際上也很少有人會(huì)這么做。
看看mybatis如何處理<cache/>:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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對(duì)象方法為builderassistant.usenewcache,我們看看這段代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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)用該方法的地方,我們可以看到并沒(méi)有使用返回值cache,在后面的過(guò)程中創(chuàng)建mappedstatement的時(shí)候使用了currentcache。
二、使用cache過(guò)程
在系統(tǒng)中,使用cache的地方在cachingexecutor中:
1
2
3
4
5
6
|
@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后,先判斷是否有二級(jí)緩存。
只有通過(guò)<cache/>,<cache-ref/>或@cachenamespace,@cachenamespaceref標(biāo)記使用緩存的mapper.xml或mapper接口(同一個(gè)namespace,不能同時(shí)使用)才會(huì)有二級(jí)緩存。
1
|
if (cache != null ) { |
如果cache存在,那么會(huì)根據(jù)sql配置(<insert>,<select>,<update>,<delete>的flushcache屬性來(lái)確定是否清空緩存。
1
|
flushcacheifrequired(ms); |
然后根據(jù)xml配置的屬性u(píng)secache來(lái)判斷是否使用緩存(resulthandler一般使用的默認(rèn)值,很少會(huì)null)。
1
|
if (ms.isusecache() && resulthandler == null ) { |
確保方法沒(méi)有out類(lèi)型的參數(shù),mybatis不支持存儲(chǔ)過(guò)程的緩存,所以如果是存儲(chǔ)過(guò)程,這里就會(huì)報(bào)錯(cuò)。
1
|
ensurenooutparams(ms, parameterobject, boundsql); |
沒(méi)有問(wèn)題后,就會(huì)從cache中根據(jù)key來(lái)取值:
1
2
|
@suppresswarnings ( "unchecked" ) list<e> list = (list<e>) tcm.getobject(cache, key); |
如果沒(méi)有緩存,就會(huì)執(zhí)行查詢(xún),并且將查詢(xún)結(jié)果放到緩存中。
1
2
3
4
5
|
if (list == null ) { list = delegate.<e>query(ms, parameterobject, rowbounds, resulthandler, key, boundsql); tcm.putobject(cache, key, list); // issue #578 and #116 } |
返回結(jié)果
1
2
3
|
return list; } } |
沒(méi)有緩存時(shí),直接執(zhí)行查詢(xún)
1
2
|
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. 在可以保證查詢(xún)遠(yuǎn)遠(yuǎn)大于insert,update,delete操作的情況下使用緩存
這一點(diǎn)不需要多說(shuō),所有人都應(yīng)該清楚。記住,這一點(diǎn)需要保證在1的前提下才可以!
四、避免使用二級(jí)緩存
可能會(huì)有很多人不理解這里,二級(jí)緩存帶來(lái)的好處遠(yuǎn)遠(yuǎn)比不上他所隱藏的危害。
- 緩存是以namespace為單位的,不同namespace下的操作互不影響。
- insert,update,delete操作會(huì)清空所在namespace下的全部緩存。
- 通常使用mybatis generator生成的代碼中,都是各個(gè)表獨(dú)立的,每個(gè)表都有自己的namespace。
為什么避免使用二級(jí)緩存
在符合【cache使用時(shí)的注意事項(xiàng)】的要求時(shí),并沒(méi)有什么危害。
其他情況就會(huì)有很多危害了。
針對(duì)一個(gè)表的某些操作不在他獨(dú)立的namespace下進(jìn)行。
例如在usermapper.xml中有大多數(shù)針對(duì)user表的操作。但是在一個(gè)xxxmapper.xml中,還有針對(duì)user單表的操作。
這會(huì)導(dǎo)致user在兩個(gè)命名空間下的數(shù)據(jù)不一致。如果在usermapper.xml中做了刷新緩存的操作,在xxxmapper.xml中緩存仍然有效,如果有針對(duì)user的單表查詢(xún),使用緩存的結(jié)果可能會(huì)不正確。
更危險(xiǎn)的情況是在xxxmapper.xml做了insert,update,delete操作時(shí),會(huì)導(dǎo)致usermapper.xml中的各種操作充滿(mǎn)未知和風(fēng)險(xiǎn)。
有關(guān)這樣單表的操作可能不常見(jiàn)。但是你也許想到了一種常見(jiàn)的情況。
多表操作一定不能使用緩存
為什么不能?
首先不管多表操作寫(xiě)到那個(gè)namespace下,都會(huì)存在某個(gè)表不在這個(gè)namespace下的情況。
例如兩個(gè)表:role和user_role,如果我想查詢(xún)出某個(gè)用戶(hù)的全部角色role,就一定會(huì)涉及到多表的操作。
1
2
3
|
<select id= "selectuserroles" resulttype= "userrolevo" > select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid} </select> |
像上面這個(gè)查詢(xún),你會(huì)寫(xiě)到那個(gè)xml中呢??
不管是寫(xiě)到rolemapper.xml還是userrolemapper.xml,或者是一個(gè)獨(dú)立的xxxmapper.xml中。如果使用了二級(jí)緩存,都會(huì)導(dǎo)致上面這個(gè)查詢(xún)結(jié)果可能不正確。
如果你正好修改了這個(gè)用戶(hù)的角色,上面這個(gè)查詢(xún)使用緩存的時(shí)候結(jié)果就是錯(cuò)的。
這點(diǎn)應(yīng)該很容易理解。
在我看來(lái),就以mybatis目前的緩存方式來(lái)看是無(wú)解的。多表操作根本不能緩存。
如果你讓他們都使用同一個(gè)namespace(通過(guò)<cache-ref>)來(lái)避免臟數(shù)據(jù),那就失去了緩存的意義。
看到這里,實(shí)際上就是說(shuō),二級(jí)緩存不能用。整篇文章介紹這么多也沒(méi)什么用了。
五、挽救二級(jí)緩存?
想更高效率的使用二級(jí)緩存是解決不了了。
但是解決多表操作避免臟數(shù)據(jù)還是有法解決的。解決思路就是通過(guò)攔截器判斷執(zhí)行的sql涉及到那些表(可以用jsqlparser解析),然后把相關(guān)表的緩存自動(dòng)清空。但是這種方式對(duì)緩存的使用效率是很低的。
設(shè)計(jì)這樣一個(gè)插件是相當(dāng)復(fù)雜的,既然我沒(méi)想著去實(shí)現(xiàn),就不廢話(huà)了。
最后還是建議,放棄二級(jí)緩存,在業(yè)務(wù)層使用可控制的緩存代替更好。
推薦:集成 spring redis 緩存
http://www.jfrwli.cn/article/173078.html
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)服務(wù)器之家的支持。如果你想了解更多相關(guān)內(nèi)容請(qǐng)查看下面相關(guān)鏈接
原文鏈接:https://blog.csdn.net/isea533/article/details/44566257