用了這么久的Mybatis,結(jié)果面試官問(wèn)的問(wèn)題,我竟然還猶豫了
本文轉(zhuǎn)載自微信公眾號(hào)「Java極客技術(shù)」,作者鴨血粉絲。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java極客技術(shù)公眾號(hào)。
前段時(shí)間阿粉的一個(gè)朋友和阿粉吃飯,在吃飯的時(shí)候和阿粉瘋狂的吐槽面試官,說(shuō)面試官問(wèn)的問(wèn)題都是些什么問(wèn)題呀,我一個(gè)干了三四年的開(kāi)發(fā),也不說(shuō)問(wèn)點(diǎn)靠譜的,阿粉很好奇,問(wèn)題問(wèn)完基礎(chǔ)的,一般不都是根據(jù)你自己的簡(jiǎn)歷進(jìn)行提問(wèn)么?而接下來(lái)他說(shuō)的出來(lái)的問(wèn)題,阿粉表示,阿粉需要繼續(xù)學(xué)習(xí)了。
Mybatis是什么?
說(shuō)到這個(gè),讀者大人們肯定心想,阿粉是在開(kāi)玩笑么?你一個(gè) Java 程序員,你不知道Mybatis是啥么?不就是個(gè)持久層的框架么,這東西有啥好說(shuō)的呢?但是阿粉還是要給大家說(shuō)。
Mybatis是一個(gè)半自動(dòng) ORM(對(duì)象關(guān)系映射)框架,它內(nèi)部封裝了JDBC,加載驅(qū)動(dòng)、創(chuàng)建連接、創(chuàng)建 statement 等繁雜的過(guò)程,我們開(kāi)發(fā)的時(shí)候只需要關(guān)注如何編寫(xiě) SQL 語(yǔ)句,而不用關(guān)心其他的。
為什么說(shuō) Mybatis 是一個(gè)半自動(dòng) ORM 的框架呢?
ORM,是Object和Relation之間的映射,而Mybatis 在查詢關(guān)聯(lián)對(duì)象或關(guān)聯(lián)集合對(duì)象時(shí),需要手動(dòng)編寫(xiě) sql 來(lái)完成,所以,稱(chēng)之為半自動(dòng) ORM 框架,而Hibernate 屬于全自動(dòng) ORM 映射工具,使用 Hibernate 查詢關(guān)聯(lián)對(duì)象或者關(guān)聯(lián)集合對(duì)象時(shí),可以根據(jù)對(duì)象關(guān)系模型直接獲取,所以它是全自動(dòng)的。
這也是為什么有些面試官在面試初級(jí)程序員的時(shí)候,很喜歡說(shuō),你覺(jué)得 Mybatis , 和 Hibernate 都有什么優(yōu)缺點(diǎn),為啥你們選擇使用的 Mybatis 而不選擇使用 Hibernate 呢?
我們都說(shuō)了 Mybatis是什么了,接下來(lái)肯定需要說(shuō)說(shuō)面試官都問(wèn)了什么問(wèn)題,能讓阿粉的朋友變得非常猶豫。
Mybatis的一級(jí)、二級(jí)緩存是什么你了解么?
Mybatis 的一級(jí)緩存
我們先說(shuō) Mybatis 的一級(jí)緩存,因?yàn)檫@是如果不手動(dòng)配置,他是自己默認(rèn)開(kāi)啟的一級(jí)緩存,一級(jí)緩存只是相對(duì)于同一個(gè) SqlSession 而言,參數(shù)和SQL完全一樣的情況下,我們使用同一個(gè)SqlSession對(duì)象調(diào)用一個(gè)Mapper方法,往往只執(zhí)行一次SQL,因?yàn)槭褂肧elSession第一次查詢后,MyBatis會(huì)將其放在緩存中,以后再查詢的時(shí)候,如果沒(méi)有聲明需要刷新,并且緩存沒(méi)有超時(shí)的情況下,SqlSession都會(huì)取出當(dāng)前緩存的數(shù)據(jù),而不會(huì)再次發(fā)送SQL到數(shù)據(jù)庫(kù)。
當(dāng)我們面試的時(shí)候,說(shuō)完這個(gè),一般情況下,面試官一定會(huì)追問(wèn)下去,畢竟技術(shù)就是要問(wèn)到你的知識(shí)盲區(qū)才會(huì)停止。
那我們就來(lái)畫(huà)個(gè)圖表示一下一級(jí)緩存:
那面試官肯定會(huì)說(shuō),直接從數(shù)據(jù)庫(kù)查不就行了,為啥要一級(jí)緩存呢?
當(dāng)我們使用MyBatis開(kāi)啟一次和數(shù)據(jù)庫(kù)的會(huì)話時(shí), MyBatis 會(huì)創(chuàng)建出一個(gè) SqlSession 對(duì)象表示一次與數(shù)據(jù)庫(kù)之間的信息傳遞,在我們執(zhí)行 SQL 語(yǔ)句的過(guò)程中,們可能會(huì)反復(fù)執(zhí)行完全相同的查詢語(yǔ)句,如果不采取一些措施,我們每一次查詢都會(huì)查詢一次數(shù)據(jù)庫(kù),而如果在極短的時(shí)間內(nèi)做了很多次相同的查詢操作,那么這些查詢返回的結(jié)果很可能相同。
也就是說(shuō),如果我們?cè)诙虝r(shí)間內(nèi),頻繁的去執(zhí)行一條 SQL ,查詢返回的結(jié)果本來(lái)應(yīng)該是改變了,但是我們查詢出來(lái)的時(shí)候,會(huì)出現(xiàn)結(jié)果一致的情況,正是為了解決這種問(wèn)題,也為了減輕數(shù)據(jù)庫(kù)的開(kāi)銷(xiāo),所以 Mybatis 默認(rèn)開(kāi)啟了一級(jí)緩存。
Mybatis 的二級(jí)緩存
Mybatis 的二級(jí)緩存一般如果你不對(duì)他進(jìn)行設(shè)置,他是不會(huì)開(kāi)啟的,而二級(jí)緩存是什么呢?Mybatis 中的二級(jí)緩存實(shí)際上就是 mapper 級(jí)別的緩存,而這時(shí)候肯定會(huì)有人說(shuō),那么不同之間的 Mapper 是同一個(gè)緩存么?
答案是否定的,他不是一個(gè),Mapper 級(jí)別的緩存實(shí)際上就是相同的 Mapper 使用的是一個(gè)二級(jí)緩存,但是在二級(jí)緩存中,又有多個(gè)不同的 SqlSession ,而不同的 Mapper 之間的二級(jí)緩存也就是互相不會(huì)影響的。
就類(lèi)似下面的圖:
這二級(jí)緩存是不是就看起來(lái)有點(diǎn)意思了?
那怎么能夠開(kāi)啟二級(jí)緩存呢?
1.MyBatis 配置文件
- <settings>
- <setting name = "cacheEnabled" value = "true" />
- </settings>
2.MyBatis 要求返回的 POJO 必須是可序列化的
3.Mapper 的 xml 配置文件中加入 標(biāo)簽
既然我們想要了解這個(gè)二級(jí)緩存,那么必然,我們還得知道它里面的配置都有哪些含義。
我們先從標(biāo)簽看起,然后從源碼里面看都有哪些配置信息提供給我們使用:
blocking : 直譯就是調(diào)度,而在 Mybatis 中,如果緩存中找不到對(duì)應(yīng)的 key ,是否會(huì)一直 blocking ,直到有對(duì)應(yīng)的數(shù)據(jù)進(jìn)入緩存。
eviction : 緩存回收策略
而緩存回收策略,在源碼中是有直接體現(xiàn)的,那么他們分別都對(duì)應(yīng)了什么形式呢?
- typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
- typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
- typeAliasRegistry.registerAlias("LRU", LruCache.class);
- typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
- typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
- PERPETUAL : 選擇 PERPETUAL 來(lái)命名緩存,暗示這是一個(gè)最底層的緩存,數(shù)據(jù)一旦存儲(chǔ)進(jìn)來(lái),永不清除.好像這種緩存不怎么受待見(jiàn)。
- FIFO : 先進(jìn)先出:按對(duì)象進(jìn)入緩存的順序來(lái)移除它們
- LRU : 最近最少使用的:移除最長(zhǎng)時(shí)間不被使用的對(duì)象。
- SOFT : 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象。
- WEAK : 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象。
大家雖然看著 PERPETUAL 排在了第一位,但是它可不是默認(rèn)的,在 Mybatis 的緩存策略里面,默認(rèn)的是 LRU 。
PERPETUAL :
源代碼如下:
- public class PerpetualCache implements Cache {
- private final String id;
- private Map<Object, Object> cache = new HashMap<>();
- public PerpetualCache(String id) {
- this.id = id;
- }
恩?看著是不是有點(diǎn)眼熟,它怎么就只是包裝了 HashMap ? 你還別奇怪,他還真的就是使用的 HashMap ,不得不說(shuō),雖然人家是使用的 HashMap ,但是那可是比咱們寫(xiě)的高端多了。
既然使用 HashMap ,那么必然就會(huì)有Key,那么他們的Key是怎么設(shè)計(jì)的?
CacheKey:
- public class CacheKey implements Cloneable, Serializable {
- private static final long serialVersionUID = 1146682552656046210L;
- public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
- private static final int DEFAULT_MULTIPLYER = 37;
- private static final int DEFAULT_HASHCODE = 17;
- private final int multiplier;
- private int hashcode; //用于表示CacheKey的哈希碼
- private long checksum; //總和校驗(yàn),當(dāng)出現(xiàn)復(fù)合key的時(shí)候,分布計(jì)算每個(gè)key的哈希碼,然后求總和
- private int count;//當(dāng)出現(xiàn)復(fù)合key的時(shí)候,計(jì)算key的總個(gè)數(shù)
- // 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.
- private List<Object> updateList;//當(dāng)出現(xiàn)復(fù)合key的時(shí)候,保存每個(gè)key
確實(shí)牛逼,至于內(nèi)部如何初始化,如何進(jìn)行操作,大家有興趣的可以去閱讀一下源碼,導(dǎo)入個(gè)源碼包,打開(kāi)自己看一下。
FIFO: 先進(jìn)先出緩沖淘汰策略
- public class FifoCache implements Cache {
- private final Cache delegate; //被裝飾的Cache對(duì)象
- private final Deque<Object> keyList;//用于記錄key 進(jìn)入緩存的先后順序
- private int size;//記錄了緩存頁(yè)的上限,超過(guò)該值需要清理緩存(FIFO)
- public FifoCache(Cache delegate) {
- this.delegate = delegate;
- this.keyList = new LinkedList<>();
- this.size = 1024;
- }
在 FIFO 淘汰策略中使用了 Java 中的 Deque,而 Deque 一種常用的數(shù)據(jù)結(jié)構(gòu),可以將隊(duì)列看做是一種特殊的線性表,該結(jié)構(gòu)遵循的先進(jìn)先出原則。Java中,LinkedList實(shí)現(xiàn)了Queue接口,因?yàn)長(zhǎng)inkedList進(jìn)行插入、刪除操作效率較高。
當(dāng)你看完這個(gè)源碼的時(shí)候,是不是就感覺(jué)源碼其實(shí)也沒(méi)有那么難看懂,里面都是我們已經(jīng)掌握好的知識(shí),只不過(guò)中間做了一些操作,進(jìn)行了一些封裝。
LRU : 最近最少使用的緩存策略
而 LUR 算法,阿粉之前都說(shuō)過(guò),如果對(duì)這個(gè)算法感興趣的話,文章地址給大家送上,經(jīng)典的 LRU 算法,你真的了解嗎?
而我們需要看的源碼則是在 Mybatis 中的源碼,
- public class LruCache implements Cache {
- private final Cache delegate;
- private Map<Object, Object> keyMap;
- private Object eldestKey;//記錄最少被使用的緩存項(xiàng)key
- public LruCache(Cache delegate) {
- this.delegate = delegate;
- setSize(1024);//重新設(shè)置緩存的大小,會(huì)重置KeyMap 字段 如果到達(dá)上限 則更新eldestKey
- }
- public void putObject(Object key, Object value) {
- delegate.putObject(key, value);
- // 刪除最近未使用的key
- cycleKeyList(key);
- }
SOFT: 基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象
在看到基于垃圾回收器的時(shí)候,阿粉就已經(jīng)開(kāi)始興奮了,竟然有GC的事情,那還不趕緊看看,這如此高大上(裝杯)的事情,來(lái)瞅瞅吧!
- public class SoftCache implements Cache {
- //在SoftCache 中,最近使用的一部分緩存項(xiàng)不會(huì)被GC回收,這就是通過(guò)將其value添加到
- private final Deque<Object> hardLinksToAvoidGarbageCollection;
- //引用隊(duì)列,用于記錄GC回收的緩存項(xiàng)所對(duì)應(yīng)的SoftEntry對(duì)象
- private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
- //底層被修飾的Cache 對(duì)象
- private final Cache delegate;
- //連接的個(gè)數(shù),默認(rèn)是256
- private int numberOfHardLinks;
- public SoftCache(Cache delegate) {
- this.delegate = delegate;
- this.numberOfHardLinks = 256;
- this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
- this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
- }
- public void putObject(Object key, Object value) {
- // 清除被GC回收的緩存項(xiàng)
- removeGarbageCollectedItems();
- // 向緩存中添加緩存項(xiàng)
- delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
- }
- public Object getObject(Object key) {
- Object result = null;
- // 查找對(duì)應(yīng)的緩存項(xiàng)
- @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
- SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
- if (softReference != null) {
- result = softReference.get();
- // 已經(jīng)被GC 回收
- if (result == null) {
- // 從緩存中清除對(duì)應(yīng)的緩存項(xiàng)
- delegate.removeObject(key);
- } else {
- // See #586 (and #335) modifications need more than a read lock
- synchronized (hardLinksToAvoidGarbageCollection) {
- hardLinksToAvoidGarbageCollection.addFirst(result);
- if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
- hardLinksToAvoidGarbageCollection.removeLast();
- }
- }
- }
- }
- return result;
- }
- public void clear() {
- synchronized (hardLinksToAvoidGarbageCollection) {
- // 清理強(qiáng)引用集合
- hardLinksToAvoidGarbageCollection.clear();
- }
- // 清理被GC回收的緩存項(xiàng)
- removeGarbageCollectedItems();
- delegate.clear();
- }
- //其中指向key的引用是強(qiáng)引用,而指向value的引用是弱引用
- private static class SoftEntry extends SoftReference<Object> {
- private final Object key;
- SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
- super(value, garbageCollectionQueue);
- this.key = key;
- }
- }
WEAK : 基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象
- public class WeakCache implements Cache {
- private final Deque<Object> hardLinksToAvoidGarbageCollection;
- private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
- private final Cache delegate;
- private int numberOfHardLinks;
- public WeakCache(Cache delegate) {
- this.delegate = delegate;
- this.numberOfHardLinks = 256;
- this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
- this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
- }
WeakCache在實(shí)現(xiàn)上與SoftCache幾乎相同,只是把引用對(duì)象由SoftReference軟引用換成了WeakReference弱引用。
在這里阿粉也就不再多說(shuō)了,關(guān)于 Mybatis 的二級(jí)緩存,你了解了么?下次遇到面試官問(wèn)這個(gè)的時(shí)候,你應(yīng)該知道怎么成功(裝杯)不被打了吧。