緩存這個東西在很多應用中都能看到它們的身影,這次就講講在Mybatis中的緩存是怎么應用的,雖然說吧Mybatis中的緩存基本不怎么用,用的更多是第三方組件redis、MongoDB、MemCache等等。
Mybatis的緩存是基于Map實現的,從緩存中讀寫數據是緩存模塊的核心功能,但是除了這個功能Mybatis也提供了很多附加功能,比如防止緩存擊穿、添加緩存清空策略等等,并且這些附加的功能屬性可以隨意組合到核心功能上。
緩存在Mybatis中的使用介紹
Myabtis中有兩個緩存,一級緩存以及二級緩存,
一級緩存是默認開啟的,而二級緩存是在配置文件中開啟的,默認是開啟,但是可以配置不開啟,Mybatis配置參數定義
一級緩存的僅僅存在于同一SqlSession當中,如果關閉了SqlSession的話,那么這個緩存就沒有了,
二級緩存是跨SqlSession的存在,同一個SqlSessionFactory是有效的,在多個SqlSession當中,也會存在多個二級緩存的存在,所以就容易出現數據臟讀,個人建議二級緩存還是禁止比較好,使用第三方的緩存組件。
在對應的Mapper.xml當中使用:
eviction:使用什么類型的緩存,默認LRU緩存,緩存滿后,將使用的最少的緩存去除;
flushInterval:刷新時間;
readOnly:是否只讀;
size:緩存大小;
blocking:是否采用阻塞策略;
<cache eviction="FIFO" flushInterval="6000" readOnly="false" size="1024"></cache>
?緩存的引用,直接引用對應的工作空間,引用后,就會直接使用這個引用的緩存:
<cache-ref namespace="xxx.xxx.xxxDao"></cache-ref>
設計模式
先來看看這個模塊用到了哪些設計模式
裝飾器模式(Decorator Pattern)
定義:允許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬于結構型模式,它是作為現有的類的一個包裝。
這種模式創建了一個裝飾類,用來包裝原有的類,并在保持類方法簽名完整性的前提下,提供了額外的功能。
像這種組合附加功能的業務上也可以通過繼承去組合相對應的屬性,但是繼承的方式是靜態的,并不能控制增加新的屬性,如果功能屬性較多的話,那么使用繼承就會有很多子類,可讀性不強。
圖示:
組件(Component):組件接口定義了全部組件類 和裝飾器實現的行為;
組件實現類(ConcreteComponent):實現 Component接口,組件實現類就是被裝飾器裝飾的 原始對象,新功能或者附加功能都是通過裝飾器添加 到該類的對象上的;
裝飾器抽象類(Decorator):實現Component接 口的抽象類,在其中封裝了一個Component 對象, 也就是被裝飾的對象;
具體裝飾器類(ConcreteDecorator):該實現類 要向被裝飾的對象添加某些功能;
相對于繼承來說,使用裝飾器模式的靈活性更高,擴展性更強。
裝飾器使用:
JDK中的IO輸入流與輸出流設計:
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt")));
在Servlet?API當中的對request的封裝類HttpServletRequestWrapper;
源碼分析
Mybatis的緩存模塊就是使用的裝飾器模式
BlockingCache示例:
同一個緩存的接口Cache(這個接口是緩存模塊的核心接口,定義了緩存的基本操作):
public interface Cache {/*** @return The identifier of this cache*/String getId();/*** @param key* Can be any object but usually it is a {@link CacheKey}* @param value* The result of a select.*/void putObject(Object key, Object value);/*** @param key* The key* @return The object stored in the cache.*/Object getObject(Object key);/*** @param key* The key* @return Not used*/Object removeObject(Object key);/*** Clears this cache instance.*/void clear();/*** Optional. This method is not called by the core.** @return The number of elements stored in the cache (not its capacity).*/int getSize();/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.** @return A ReadWriteLock*/default ReadWriteLock getReadWriteLock() {return null;}}
在不同屬性的實現類分別封裝一個Cache對象,實現了每個附加功能的組合。
FifoCache:一個先進先出的緩存,如果緩存的數量達到了臨界點,則將緩存時間最長的那個緩存去除。
LoggingCache:附加了日志以及統計功能。
?LruCache:如果緩存數據達到了臨界點,會將訪問次數最少的緩存數據清空。
ScheduledCache:會定時清除緩存的一個緩存。
SerializedCache:一個可以序列化以及反序列化的緩存。
SoftCache:軟引用緩存,如果緩存被GC回收后,對應的緩存數據也會被清空。
WeakCache:弱引用緩存,如果緩存被GC回收后,對應的緩存數據也會被清空。
SynchronizedCache:同步運行的緩存。
TransactionalCache:一個存在事務提交和回滾的緩存。
BlockingCache:包含阻塞機制的緩存
在緩存模塊中用到的緩存類實際上是PerpetualCache,以上的那些緩存類僅僅是附加功能,具體實現緩存功能的是PerpetualCache。
public class PerpetualCache implements Cache {private final String id;//數據被緩存的地方private final Map<Object, Object> cache = new HashMap<>();public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic boolean equals(Object o) {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}if (this == o) {return true;}if (!(o instanceof Cache)) {return false;}Cache otherCache = (Cache) o;return getId().equals(otherCache.getId());}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}return getId().hashCode();}}
一個比較簡單的基于Map的緩存實現類。
緩存擊穿
對于數據庫來說,緩存是一個很好的東西,它能夠擋住大部分的數據請求,但是如果在緩存中找不到對應的key和value時,那么這個這些請求就會去請求數據庫,如果這個請求是在高并發的情況或者數據是“熱數據”的情況,那么數據庫訪問那么也會變成高并發訪問查詢,這就是緩存擊穿了。
所以在設計緩存模塊的時候就應該考慮到這個問題,在Mybatis里面這個問題已經得到解決,BlockingCache就是解決方法。
我們先來看看緩存擊穿的一些解決辦法:
第一種:如果當前緩存被擊穿后,不應該讓所有的請求去請求數據庫,而是只讓一個請求去請求數據庫,其他線程等待,當數據查詢完畢并緩存進去后,再讓其他線程去查詢緩存。
但是這個方法存在一個問題,面對大量的請求來說,就會有效率問題。
第二種:既然以上方法存在效率問題,那么就可以給同等類型的key上鎖,讓相同的請求只派出一個請求去請求數據庫,其他同等類型key的請求就等待,當請求到后,那么就釋放鎖其他線程回去緩存中獲取數據了。
但是如果key太多的話,會比較浪費資源。
綜上所述:對應這種緩存的話,個人覺得應該要與業務掛鉤,取得一個平衡點取設計緩存模塊。
現在我們來看看BlockingCache是怎么玩的,其實就是給每個key上鎖然后,請求完在釋放鎖。
我目前的Mybatis版本是最新的3.5.7,可能其他版本并非使用的事CountDownLatch,而是使用的ReentrantLock,基本都是一樣,基于AQS同步,想要了解線程相關的東西,可以去看看我之前發布博客喲。
CacheKey
現在來看一下緩存的key是怎么組成的,緩存模塊中的key值并非直接的使用SQL語句中的唯一ID啥的。
在Mybatis設計緩存時,就將key包裝成一個對象,只有這個對象的hashCode相等,這兩個key才能一致。
構成CacheKey的因素有四個:
1、mappedStatment的id ;
2、指定查詢結果集的范圍(分頁信息);
3、查詢所使用的SQL語句;
4、用戶傳遞給SQL語句的實際參數值;
update方法:
equals方法:
Mybatis的緩存基本講的差不多了。