小林求職記(三)一上來就圍繞電商系統層層提問,我太難了….
前傳
面試官:什麼是大事務?小林哥:就是 很大...的...事務??
小林求職記(二):說好的問基礎,為啥我感覺一點也不基礎呢?
二面的面試官來到來我的跟前,開始對我的簡歷進行了一番打量然後就開始了技術提問。
面試官: 看了下你在簡歷上邊有寫到過關於電商系統的設計,那我想深入問下你在電商系統設計的幾個問題哈。
小林: 好的。
面試官: 你們電商系統的每天的日活量大概在多少呢?
小林: 嗯,日活使用者數目在5萬左右,搞促銷活動的時候還會涉及到一些大流量的訪問。
面試官: 嗯嗯,那麼接下來我問你幾個系統內部設計的場景吧。
小林: 嗯嗯。(表面風平浪靜,內心還是會有些慌張)
面試官:你剛才提到了促銷活動,那麼在搞促銷活動之前,你們應該會有一些特殊的準備吧,能和我講幾個場景的實際案例嗎?
小林: 嗯嗯,我們的商品資訊其實是儲存在mysql裡面的,當進行促銷活動的時候需要進行一次預熱工作,將一些熱點資料載入到快取層當中,減少對於實際mysql訪問的壓力。在快取方面我之前一貫都是使用了redis來儲存資料,但是高峰時期對於redis的查詢依然是需要網路消耗,有些特殊的業務場景需要在迴圈裡面對redis做查詢(老舊程式碼的原因,不推薦在工作中這麼使用),因此這部分的模組我加入了本地快取作為優化手段。
面試官: 嗯嗯(就這??)
小林停頓了一會,看面試官似乎還覺得說得不夠,然後繼續回答接下來的內容點。
小林: 對於一些熱點資料而言,我們的本地快取使用的是Guava Cache 技術,它提供了一個LoadingCache介面供開發者進行快取資料的本地管理。當查詢資料不存在的時候會發生快取命中失效,這時候可以通過定義內部的一些callable介面來實現對應的策略。
ps: 此時小林想起來自己以前剛學習guava cache技術時接觸的程式碼:
//這種型別到好處在於 查詢資料的時候,如果資料不存在,那麼就需要寫如何從記憶體里加載,每次查詢都需要做一個callable的處理 Cache<Object, Object> cache = CacheBuilder.newBuilder().build(); cache.put("k1","v1"); //如果物件資料不存在,則返回一個null值 Object v1=cache.getIfPresent("k1"); Object v2 = cache.get("k2",new Callable<Object>(){ @Override public Object call() throws Exception { System.out.println("該數值不存在,需要到redis去查詢"); return "k2"; } }); System.out.println(v1); System.out.println(v2);
面試官:如果每次查詢不了資料都需要在get的時候去重寫策略,豈不是很麻煩嗎?(其實面試官也用過這款技術,就是故意深入問問求職者是否有更多的瞭解內部構造)
小林:嗯嗯,其實可以在定義LoadingCache做一個全域性性的callable回撥操作處理,我腦海中還對這段程式碼有印象,主要是通過cacheloader來做實現。
ps:此時一段熟悉的程式碼模型從小林腦海中閃過。
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 當快取沒有命中的時候,可以通過這裡面的策略去載入資料資訊 System.out.println("快取命中失敗,需要查詢redis"); return "value"+key; } });
面試官: 嗯嗯,那你對於這些快取演算法有過相關研究嗎?可以講講自己的理解嗎?
小林: 嗯呢,常見的快取佇列可以採用lru演算法,所謂的lru其實本質的核心就在於:最近最久未使用的資料,可能在將來也不會再次使用,因此當快取空間滿了之後就可以將其淘汰掉。簡單的實現思路可以用一條佇列來實現,當陣列中的某個元素存在且再次被訪問的時候就會將其挪到連結串列的首位,如果查詢某些新元素髮現在lru佇列裡面沒有命中,則需要從db中查詢然後插入到佇列的首部。這樣能夠保持佇列裡面的元素大多數場景下都是熱點元素,當佇列的體積佔滿了之後,訪問最低頻率的元素就會從隊尾被擠出。
面試官: 嗯嗯,可以再深入講解下lru演算法所存在的弊端嗎?(內心彷彿在說,就這?)
小林: 嗯嗯,lru演算法其實存在這快取汙染的問題,例如說某次批量查詢操作,將一些平時用不到的資料也塞入到了佇列中,將一些真正的熱點資料給擠出了佇列,造成快取汙染現象。因此後邊就衍生出來了lru-k演算法,其核心思想是給每個訪問的元素都標識一個訪問次數k值,這類演算法需要多維護一條佇列(暫且稱之為訪問佇列),當資料的訪問次數超過了k次之後,才會從原先的訪問佇列中轉移到真正的lru佇列裡面。這樣就能避免之前所說的快取汙染問題了,但是採用lru-k演算法其實需要涉及到的演算法複雜度,空間大小遠高於前邊提到的lru演算法,這也是它的一個小”缺陷“吧。
面試官: 嗯嗯,好的,那關於快取的回收策略你有了解過嗎?
小林: 嗯嗯,我在之前的工程中使用的guava-cache技術就是採用惰性回收策略的,當快取的資料到達過期時間的時候不會去主動回收記憶體空間,而是在當程式有需要主動查詢資料的時候才會去做記憶體回收的檢測等相關操作。之所以不做主動回收的工作,我推測是因為自動回收程式的步驟對於cache自身需要維護的難度較高,所以改成了惰性回收策略,這一點和redis裡的惰性回收策略有點類似,這種策略容易造成某些長期不使用的資料一直沒法回收,佔用了大量的記憶體空間。
面試官: 嗯嗯,好的,那麼這個面試點先到此告一段落吧,我再問下你其他的業務場景。
小林內心漸漸恢復平靜,一開始的那種焦慮和緊張感漸漸地消失了,又恢復了從前的那種淡定和從容。
小林: 好的。
面試官: 你們的訂單業務系統一般是怎麼做分表操作的啊?可以介紹一下嗎?
小林:
嗯嗯,可以的,我們的訂單表每日的增加數目為5萬條資料左右,一個月左右訂單的資料量就會增加到100萬條左右的資料,因此我們通常每個月都會按照月為單位來做分表操作。在使用者的電商app介面上邊有個訂單查詢模組,關於那塊的業務我也做過相關的開發。
通常我的訂單查詢的資料都是按照時間順序,先查詢最近的資料,再查詢之前的資料資訊,結合前端做了分頁涉及的功能,會根據前端傳入的月份時間,來識別應該定位在哪張表進行查詢。通常來說近三個月時間內的訂單資料都是一些熱點資料,所以我們將前三個月的資料存在同一張表裡面專門做優化。
關於後續幾個月的資料大多數情況下使用者自身並不會涉及到查詢功能,因此我們會定時將資料同步到es資料庫裡面,如果後續需要涉及這塊的資料查詢,則走es資料庫。
關於訂單資料如何定時同步到到es這塊,相關的查詢邏輯圖如下所示:
這裡面會有一個job去維護MySQL和ES之間的資料一致性。
面試官: 嗯嗯,那麼你們的es和mysql之間是怎麼做資料一致性的維護呢?
小林: 我們藉助的是阿里的一款開源中介軟體做資料同步,結合了canal+mysql+rocketmq來進行實現的。
canal會模擬成一臺mysql的slave去接收mysql的master節點返回的binlog資訊,然後將這些binlog資料解析成一個json字串,再投遞到mq當中。在rocketmq的接收端會做訊息的監聽,一旦有接收到訊息就會寫入到es中。
面試官: 嗯嗯,那麼你能簡單講解下在這過程中遇到的困難嗎?
小林: 額,其實這一套環境在我入職的時候就已經搭建好來,我也只是大概知道有這麼一個東西,具體的很多細節也並不是很熟悉....
(此時小林的內心再一次流下了沒有技術的眼淚.......)
面試官似乎有點失望,看了下專案,於是便切換了另一個問題進行詢問。
ps:
當我們使用canal進行binlog監聽的初始化時候,難免需要遇到一些全量同步和增量同步的問題,那麼在這個先全量同步再轉換為增量同步的過渡期間該如何做到程式的自動化銜接呢?
關於這塊的設計方案可以參考下mysql內部是如何重建索引樹的思想。
在mysql進行索引樹重建的時候,會將原先表的所有資料都拷貝存入另外一張表,在拷貝的期間如果有新資料寫入表的話,會建立一份redo log檔案將新寫入的資料存放進去,保證整個過程都是online的,因此這也被稱為Online DDL,redo log在這整個過程中就起到了一個類似緩衝池的角色。
同理在使用canal做日誌訂閱的時候也可以藉助這麼一個“緩衝池”角色的幫助。這個緩衝池可以是一些分散式快取,用於臨時接收資料,當全量同步完成之後,進入一個加鎖的狀態,此時將快取中的資料也一同刷入到db中,最後釋放鎖。由於將redis中的資料刷入到磁碟中是個非常迅速的瞬間,因此整個過程可以看作為幾乎平滑無感知。
那麼你可能也會有所疑惑,mysql表本身已經有初始化資料了,該如何全量將binlog都發送給到canal呢?其實方法有很多種,binlog的產生主要是依靠資料發生變動導致的,假設我們需要同步的表裡麵包含了update_time欄位的話,這裡只需要更新下全表的update_time欄位為原先值+1 就可以產生出全表的binlog資訊了。
整體的設計思路圖如下:
(可惜小林平時在工作中沒有對這塊做過多的梳理)
面試官:好吧,你在平時的工作中有遇到過一些jvm調優相關的內容嗎?
小林:嗯嗯,有的。
面試官:哦,太好了,可以講講你是怎麼去做jvm調優的嗎?
小林:我們一般都搞不定,遇到jvm棧溢位的時候重啟並且增加機器就完事了。
面試官:...... 這確實是一種方案,能不能講些有價值點的思路呢?
小林:嗯嗯,我之前有學習瞭解到過,Java虛擬機器將堆記憶體劃分為新生代、老年代和永久代。
圖片
l通常來講,我們代年輕代物件會被存放在eden區域裡面,這塊區域當記憶體佔比會相對於survivor區要大一些,大部分當物件在經歷一次Minor GC之後都被銷燬了,剩餘當物件會被分入到survivor區裡面,然後在survivor區進入新的垃圾回收,當回收當次數超過了閾值之後(預設是15次),物件就會從年輕代中晉升到老年代。當然如果說survivor區中相同年齡的物件體積大小之和大於了survivor區中一半的空間,那麼此時物件也會直接晉升到老年代中。哦對了,jdk8
之後還多出來了一個叫做元空間的概念。
小林非常流暢地將自己對於jvm的理解講了出來,感覺自己的這番回答似乎很滿意。可是別小瞧對方面試官,人家畢竟是有過十多年經驗的大佬啊。
面試官:嗯嗯,能深入介紹下嗎你對於垃圾收集器使用方面的一些經驗總結嗎?
小林:額....這塊就不是很熟繫了
ps:
對於 JVM 調優來說,主要是 JVM 垃圾收集的優化,一般來說是因為有問題才需要優化,所以對於 JVM GC 來說,如果當我們發現線上的gc回收頻率變得頻繁之後,就是需要進行jvm調優的時候了。而對於jvm的垃圾回收而言,應該是針對不同的垃圾收集器來做優化調整。
例如說cms垃圾收集器,這塊收集器主要是將jvm分為了年輕代,老年代。在年輕代採用了複製整理演算法,在老年代使用的是標記清除演算法,再其進行標記物件的時候會發生stw的情況。而在jdk9之後,可以看出一定的趨勢,G1回收演算法開始在漸漸佔領位置,由於以前的分割槽將jvm的各個模組eden,survivor區都劃分地過大了,因此G1將jvm的區域劃分為了多個零散的region,將原先連續固定的eden區和survivor區給拆解開來分割成多個小模組,這樣一來垃圾回收的停頓時長就會大大降低,減少stw機制造成的影響。
對於 CMS 收集器來說,最重要的是合理地設定年輕代和年老代的大小。年輕代太小的話,會導致頻繁的 Minor GC,並且很有可能存活期短的物件也不能被回收,GC 的效率就不高。
對於 G1 收集器來說,不是太推薦直接設定年輕代的大小,這一點跟 CMS 收集器不一樣,這是因為 G1 收集器會根據演算法動態決定年輕代和年老代的大小。因此對於 G1 收集器,我們更加需要關心的是 Java 堆的總大小(-Xmx)。
面試官: 好吧,那今天的面試就先這樣告一段落吧。
小林: 嗯嗯,我還有機會嗎...
面試官: 我覺得你後邊可以進步和提升的空間還有很大(意思是你太菜了),可以再學習學習哈。
聽完此話後,小林留下了沒有技術的淚水....,唉,看來這個工作還是得繼續找啊....
後來,小林問了下以前大學同學,打探到了新的內推崗位....
&n