2020Java面試彙總(四)----緩存篇

1.why-cache?

面試題:

  • 項目中緩存是如何使用的?(結合自身公司項目)
  • 爲什麼要用緩存?
  • 緩存使用不當會造成什麼後果?

這些問題是互聯網公司必問問題,要是一個人連緩存都不太清楚,那確實比較尷尬。只要問到緩存,上來第一個問題,肯定是先問問你項目哪裏用了緩存?爲啥要用?不用行不行?如果用了以後可能會有什麼不良的後果?這就是看看你對緩存這個東西背後有沒有思考,如果你就是傻乎乎的瞎用,沒法給面試官一個合理的解答,那面試官對你印象肯定不太好,覺得你平時思考太少,就知道幹活兒。

爲什麼要用緩存:高性能高併發

場景一:前端發一個請求,在你一頓操作後,半天才從數據庫查詢出數據,耗時6000ms,但是這個結果在接下來的幾個小時內都不會改變數值,或者變了也不會需要立即反饋給用戶,那麼咋辦?當然是選擇緩存,把花費6000ms查出來的數據丟緩存中,下次在去查的時候,直接從緩存中緩存,2ms搞定,性能提高3000倍。

==》就是說對於一些需要複雜操作耗時查出來的結果,且確定後面不怎麼變化,但是有很多讀請求,那麼直接將查詢出來的結果放在緩存中,後面直接讀緩存就好。

場景二:mysql 這麼重的數據庫,壓根兒設計不是讓你玩兒高併發的,雖然也可以玩兒,但是天然支持不好。mysql 單機支撐到 2000QPS 也開始容易報警了。

所以要是你有個系統,高峯期一秒鐘過來的請求有 1萬,那一個 mysql 單機絕對會死掉。你這個時候就只能上緩存,把很多數據放緩存,別放 mysql。緩存功能簡單,說白了就是 key-value 式操作,單機支撐的併發量輕鬆一秒幾萬十幾萬,支撐高併發 so easy。單機承載併發量是 mysql 單機的幾十倍。

==》緩存是走內存的,內存天然就支撐高併發。

用了緩存之後會有什麼不良後果?

常見的緩存問題有以下幾個:

  • 緩存與數據庫雙寫不一致
  • 緩存雪崩,緩存穿透,緩存擊穿
  • 緩存併發競爭

2.緩存與數據庫雙寫不一致?

面試題

如何保證緩存和數據庫的雙寫一致性?

現在保證雙寫一致的問題最經典的緩存+數據庫讀寫的模式,就是 Cache Aside Pattern。

  • 讀的時候,先讀緩存,緩存沒有的話,就讀數據庫,然後取出數據後放入緩存,同時返回響應。

  • 更新的時候(兩種策略,在併發的情況下,根據場景和代價進行選擇)

    • 先更新數據庫,然後再刪除緩存。(代價小)
    • 先刪除緩存,在更新數據庫

爲什麼是刪除緩存,而不是更新緩存?

場景:舉個栗子,一個緩存涉及的表的字段,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼緩存更新 20 次、100 次;但是這個緩存在 1 分鐘內只被讀取了 1 次,有大量的冷數據。實際上,如果你只是刪除緩存的話,那麼在 1 分鐘內,這個緩存不過就重新計算一次而已,開銷大幅度降低。用到緩存纔去算緩存。

(lazy加載思想,細品)

兩種更新策略在特殊場景下都會產生不一致的問題:但是我們需要去分析不一致後對生產的影響

先更新數據庫,然後再刪除緩存

場景:大量併發請發,同時包含讀和寫操作,首先一個讀操作,沒有正確讀取到緩存的數據,從而去數據庫讀取,同時一個寫操作,寫完數據後,讓緩存失效,然後之前的讀操作將舊數據寫入緩存中,造成髒數據。

該情況出現的概率非常低,首先這個條件發生是需要讀緩存的時候,緩存失效,而且併發一個寫的操作,而實際數據庫的寫操作比讀操作慢的多,而且還要鎖表,而讀操作必需在寫操作前進入數據庫操作,而又要晚於寫操作更新緩存,所有的這些條件都具備的概率基本並不大。 所以此策略的代價小

先刪除緩存,在更新數據庫

場景:兩個併發操作,一個是更新操作,另一個是查詢操作,更新操作刪除緩存後,查詢操作沒有命中緩存,先把老數據讀出來後放到緩存中,然後更新操作更新了數據庫。於是,在緩存中的數據還是老的數據,導致緩存中的數據是髒的,而且還一直這樣髒下去了。 產生髒數據的概覽較大

對於高併發下,如何保證雙寫一致性的更多內容可以看這篇文件。

https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md

3.緩存雪崩,緩存穿透,緩存擊穿

什麼是緩存雪崩?

簡介:緩存同一時間大面積的失效,所以,後面的請求都會落到數據庫上,造成數據庫短時間內承受大量請求而崩掉。

場景:對於系統 A,假設每天高峯期每秒 5000 個請求,本來緩存在高峯期可以扛住每秒 4000 個請求,但是緩存機器意外發生了全盤宕機。緩存掛了,此時 1 秒 5000 個請求全部落數據庫,數據庫必然扛不住,它會報一下警,然後就掛了。此時,如果沒有采用什麼特別的方案來處理這個故障,DBA 很着急,重啓數據庫,但是數據庫立馬又被新的流量給打死了。

有哪些解決辦法?

  • 事前:儘量保證整個 redis 集羣的高可用性,主從+哨兵,Redis cluster,避免全盤崩潰 , 發現機器宕機儘快補上。選擇合適的內存淘汰策略。
  • 事中:本地 ehcache 緩存 + hystrix | Sentinel限流&降級,避免 MySQL 崩掉
  • 事後:利用 redis 持久化機制保存的數據儘快恢復緩存

用戶發送一個請求,系統 A 收到請求後,先查本地 ehcache 緩存,如果沒查到再查 Redis。如果 ehcache 和 Redis 都沒有,再查數據庫,將數據庫中的結果,寫入 ehcache 和 Redis 中。

對高頻請求,做準對性的限流處理,甚至可以使用sentinel的hotkey熱點規則,對某一熱點進行限流處理,熔斷降級,限流,返回友好提示。

好處:

  • 數據庫絕對不會死,限流組件確保了每秒只有多少個請求能通過。
  • 只要數據庫不死,就是說,對用戶來說,2/5 的請求都是可以被處理的。
  • 只要有 2/5 的請求可以被處理,就意味着你的系統沒死,對用戶來說,可能就是點擊幾次刷不出來頁面,但是多點幾次,就可以刷出來了。

什麼是緩存穿透?

緩存穿透說簡單點就是大量請求的 key 根本不存在於緩存中,導致請求直接到了數據庫上,根本沒有經過緩存這一層。舉個例子:某個黑客故意製造我們緩存中不存在的 key 發起大量請求,導致大量請求落到數據庫。

場景:對於系統A,假設一秒 5000 個請求,結果其中 4000 個請求是黑客發出的惡意攻擊。黑客發出的那 4000 個攻擊,緩存中查不到,每次你去數據庫裏查,也查不到。比如數據庫 id 是從 1 開始的,結果黑客發過來的請求 id 全部都是負數。這樣的話,緩存中不會有,請求每次都“視緩存於無物”,直接查詢數據庫。這種惡意攻擊場景的緩存穿透就會直接把數據庫給打死。

有哪些解決辦法?

最基本的就是首先做好參數校驗,一些不合法的參數請求直接拋出異常信息返回給客戶端。比如查詢的數據庫 id 不能小於 0、傳入的郵箱格式不對的時候直接返回錯誤消息給客戶端等等。

1)緩存無效 key : 如果緩存和數據庫都查不到某個 key 的數據就寫一個到 redis 中去並設置過期時間,具體命令如下:SET key value EX 10086。這種方式可以解決請求的 key 變化不頻繁的情況,如果黑客惡意攻擊,每次構建不同的請求 key,會導致 redis 中緩存大量無效的 key 。很明顯,這種方案並不能從根本上解決此問題。如果非要用這種方式來解決穿透問題的話,儘量將無效的 key 的過期時間設置短一點比如 1 分鐘。

**2)布隆過濾器:**布隆過濾器是一個非常神奇的數據結構,通過它我們可以非常方便地判斷一個給定數據是否存在於海量數據中。我們需要的就是判斷 key 是否合法,有沒有感覺布隆過濾器就是我們想要找的那個“人”。具體是這樣做的:把所有可能存在的請求的值都存放在布隆過濾器中,當用戶請求過來,我會先判斷用戶發來的請求的值是否存在於布隆過濾器中。不存在的話,直接返回請求參數錯誤信息給客戶端,存在的話纔會走下面的流程。

布隆過濾器將我們給定的值通過多次hash計算,存入位數組中。hash碰撞是不可避免的,因此,布隆過濾器說某個key值存在是,這個值可能不存在,存在誤判。但是如果說這個值不存在時,那就一定不存在。所以我們可以用這個特性來解決緩存穿透問題。

更多關於布隆過濾器的內容可以看我的這篇原創:《不瞭解布隆過濾器?一文給你整的明明白白!》 ,強烈推薦。

什麼是緩存擊穿?

緩存擊穿,就是說某個 key 非常熱點,訪問非常頻繁,處於集中式高併發訪問的情況,當這個 key 在失效的瞬間,大量的請求就擊穿了緩存,直接請求數據庫,就像是在一道屏障上鑿開了一個洞。

不同場景下的解決方式可如下:

  • 若緩存的數據是基本不會發生更新的,則可嘗試將該熱點數據設置爲永不過期。
  • 若緩存的數據更新不頻繁,且緩存刷新的整個流程耗時較少的情況下,則可以採用基於 Redis、zookeeper 等分佈式中間件的分佈式互斥鎖,或者本地互斥鎖以保證僅少量的請求能請求數據庫並重新構建緩存,其餘線程則在鎖釋放後能訪問到新緩存。
  • 若緩存的數據更新頻繁或者在緩存刷新的流程耗時較長的情況下,可以利用定時線程在緩存過期前主動地重新構建緩存或者延後緩存的過期時間,以保證所有的請求能一直訪問到對應的緩存。

4.緩存併發競爭

面試題:

Redis 的併發競爭問題是什麼?如何解決這個問題?瞭解 Redis 事務的 CAS 方案嗎?

這個也是線上非常常見的一個問題,就是多客戶端同時併發寫一個 key,可能本來應該先到的數據後到了,導致數據版本錯了;或者是多客戶端同時獲取一個 key,修改值之後再寫回去,只要順序錯了,數據就錯了。

而且 Redis 自己就有天然解決這個問題的 CAS 類的樂觀鎖方案。

解決方案:

第一種方案:分佈式鎖+時間戳

這種情況準備一個分佈式鎖(redis分佈式鎖或者zookeeper分佈式鎖:用一個狀態值表示鎖,對鎖的佔用和釋放通過狀態值來標識),通過進行加鎖,實現把並行讀寫改成串行讀寫,從而避免資源競爭。

如果需要順序執行,我們就需要帶上時間,當操作獲取到鎖的時候,同時獲取緩存中的時間,如果發現自己的時間戳早於緩存中的時間戳,那麼就不做set操作。

第二種方案:消息隊列

在高併發的情況下,結合消息隊列,也是實現從並行到串行的轉變,把Redis.set操作放在隊列中使其串行化,必須的一個一個執行,從而解決併發競爭問題。這種方式在一些高併發的場景中算是一種通用的解決方案。

5.redis 和 memcached

區別

  1. redis 支持更豐富的數據類型(支持更復雜的應用場景):Redis 不僅僅支持簡單的 k/v 類型的數據,同時還提供 list,set,zset,hash 等數據結構的存儲。memcache 支持簡單的數據類型,String。
  2. Redis 支持數據的持久化,可以將內存中的數據保持在磁盤中,重啓的時候可以再次加載進行使用,而 Memecache 把數據全部存在內存之中。
  3. 集羣模式:memcached 沒有原生的集羣模式,需要依靠客戶端來實現往集羣中分片寫入數據;但是 redis 目前是原生支持 cluster 模式的.
  4. Memcached 是多線程,非阻塞 IO 複用的網絡模型;Redis 使用單線程的多路 IO 複用模型。(重要)

Redis的線程模型

Redis 內部使用文件事件處理器 file event handler ,這個文件事件處理器是單線程的,所以 Redis 才叫做單線程的模型。它採用 IO 多路複用機制同時監聽多個 socket,將產生事件的 socket 壓入內存隊列中,事件分派器根據 socket 上的事件類型來選擇對應的事件處理器進行處理。

爲啥 Redis 單線程模型也能效率這麼高?

  • 純內存操作。
  • 核心是基於非阻塞的 IO 多路複用機制。
  • C 語言實現,一般來說,C 語言實現的程序“距離”操作系統更近,執行速度相對會更快。
  • 單線程反而避免了多線程的頻繁上下文切換問題,預防了多線程可能產生的競爭問題。

6.Redis 的過期策略都有哪些?內存淘汰機制都有哪些?

Redis 過期策略是:定期刪除+惰性刪除

所謂定期刪除,指的是 Redis 默認是每隔 100ms 就隨機抽取一些設置了過期時間的 key,檢查其是否過期,如果過期就刪除。

假設 Redis 裏放了 10w 個 key,都設置了過期時間,你每隔幾百毫秒,就檢查 10w 個 key,那 Redis 基本上就死了,cpu 負載會很高的,消耗在你的檢查過期 key 上了。注意,這裏可不是每隔 100ms 就遍歷所有的設置過期時間的 key,那樣就是一場性能上的災難

但是問題是,定期刪除可能會導致很多過期 key 到了時間並沒有被刪除掉,那咋整呢?所以就是惰性刪除了。這就是說,在你獲取某個 key 的時候,Redis 會檢查一下 ,這個 key 如果設置了過期時間那麼是否過期了?如果過期了此時就會刪除,不會給你返回任何東西。

獲取 key 的時候,如果此時 key 已經過期,就刪除,不會返回任何東西。

但是實際上這還是有問題的,如果定期刪除漏掉了很多過期 key,然後你也沒及時去查,也就沒走惰性刪除,此時會怎麼樣?如果大量過期 key 堆積在內存裏,導致 Redis 內存塊耗盡了,咋整?

答案是:走內存淘汰機制

內存淘汰機制 (MySQL 裏有 2000w 數據,Redis 中只存 20w 的數據,如何保證 Redis 中的數據都是熱點數據?)

Redis 內存淘汰機制有以下幾個:

  • volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰 (最常用)
  • volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
  • volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
  • allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)
  • allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
  • no-eviction:禁止驅逐數據,也就是說當內存不足以容納新寫入數據時,新寫入操作會報錯。這個應該沒人使用吧!

4.0 版本後增加以下兩種:

  • volatile-lfu:從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰
  • allkeys-lfu:當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的 key

7.如何保證Redis的高可用和高併發?

面試題

其實問這個問題,主要是考考你,redis 單機能承載多高併發?如果單機扛不住如何擴容扛更多的併發?redis 會不會掛?既然 redis 會掛那怎麼保證 redis 是高可用的?

如果你用 redis 緩存技術的話,肯定要考慮如何用 redis 來加多臺機器,保證 redis 是高併發的,還有就是如何讓 redis 保證自己不是掛掉以後就直接死掉了,即 redis 高可用。

高併發

主從(master-slave)架構,一主多從,主負責寫,並且將數據複製到其它的 slave 節點,從節點負責讀。所有的讀請求全部走從節點。這樣也可以很輕鬆實現水平擴容,支撐讀高併發

下面的文章很好的講訴了redis主從架構,以及主從的核心原理

https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-master-slave.md

高可用

Redis 的高可用架構,叫做 failover 故障轉移,也可以叫做主備切換。

master node 在故障時,自動檢測,並且將某個 slave node 自動切換爲 master node 的過程,叫做主備切換。這個過程,實現了 Redis 的主從架構下的高可用。

Redis 哨兵集羣實現高可用

哨兵的介紹

sentinel,中文名是哨兵。哨兵是 Redis 集羣架構中非常重要的一個組件,主要有以下功能:

  • 集羣監控:負責監控 Redis master 和 slave 進程是否正常工作。
  • 消息通知:如果某個 Redis 實例有故障,那麼哨兵負責發送消息作爲報警通知給管理員。
  • 故障轉移:如果 master node 掛掉了,會自動轉移到 slave node 上。
  • 配置中心:如果故障轉移發生了,通知 client 客戶端新的 master 地址。

哨兵用於實現 Redis 集羣的高可用,本身也是分佈式的,作爲一個哨兵集羣去運行,互相協同工作。

  • 故障轉移時,判斷一個 master node 是否宕機了,需要大部分的哨兵都同意纔行,涉及到了分佈式選舉的問題。
  • 即使部分哨兵節點掛掉了,哨兵集羣還是能正常工作的,因爲如果一個作爲高可用機制重要組成部分的故障轉移系統本身是單點的,那就很坑爹了。

哨兵的核心知識

  • 哨兵至少需要 3 個實例,來保證自己的健壯性。
  • 哨兵 + Redis 主從的部署架構,是不保證數據零丟失的,只能保證 Redis 集羣的高可用性。
  • 對於哨兵 + Redis 主從這種複雜的部署架構,儘量在測試環境和生產環境,都進行充足的測試和演練。

Redis 基於哨兵的高可用性

8.Redis 持久化

面試題

Redis 的持久化有哪幾種方式?不同的持久化機制都有什麼優缺點?持久化機制具體底層是如何實現的?

兩種方式:

  • RDB:RDB 持久化機制,是對 Redis 中的數據執行週期性的持久化。
  • AOF:AOF 機制對每條寫入命令作爲日誌,以 append-only 的模式寫入一個日誌文件中,在 Redis 重啓的時候,可以通過回放 AOF 日誌中的寫入指令來重新構建整個數據集。

RDB 優缺點

  • RDB 會生成多個數據文件,每個數據文件都代表了某一個時刻中 Redis 的數據,這種多個數據文件的方式,非常適合做冷備,可以將這種完整的數據文件發送到一些遠程的安全存儲上去,比如說 Amazon 的 S3 雲服務上去,在國內可以是阿里雲的 ODPS 分佈式存儲上,以預定好的備份策略來定期備份 Redis 中的數據。
  • RDB 對 Redis 對外提供的讀寫服務,影響非常小,可以讓 Redis 保持高性能,因爲 Redis 主進程只需要 fork 一個子進程,讓子進程執行磁盤 IO 操作來進行 RDB 持久化即可。
  • 相對於 AOF 持久化機制來說,直接基於 RDB 數據文件來重啓和恢復 Redis 進程,更加快速。
  • 如果想要在 Redis 故障時,儘可能少的丟失數據,那麼 RDB 沒有 AOF 好。一般來說,RDB 數據快照文件,都是每隔 5 分鐘,或者更長時間生成一次,這個時候就得接受一旦 Redis 進程宕機,那麼會丟失最近 5 分鐘的數據。
  • RDB 每次在 fork 子進程來執行 RDB 快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,或者甚至數秒。

AOF 優缺點

  • AOF 可以更好的保護數據不丟失,一般 AOF 會每隔 1 秒,通過一個後臺線程執行一次 fsync 操作,最多丟失 1 秒鐘的數據。
  • AOF 日誌文件以 append-only 模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復。
  • AOF 日誌文件即使過大的時候,出現後臺重寫操作,也不會影響客戶端的讀寫。因爲在 rewrite log 的時候,會對其中的指令進行壓縮,創建出一份需要恢復數據的最小日誌出來。在創建新日誌文件的時候,老的日誌文件還是照常寫入。當新的 merge 後的日誌文件 ready 的時候,再交換新老日誌文件即可。
  • AOF 日誌文件的命令通過可讀較強的方式進行記錄,這個特性非常適合做災難性的誤刪除的緊急恢復。比如某人不小心用 flushall 命令清空了所有數據,只要這個時候後臺 rewrite 還沒有發生,那麼就可以立即拷貝 AOF 文件,將最後一條 flushall 命令給刪了,然後再將該 AOF 文件放回去,就可以通過恢復機制,自動恢復所有數據。
  • 對於同一份數據來說,AOF 日誌文件通常比 RDB 數據快照文件更大。
  • AOF 開啓後,支持的寫 QPS 會比 RDB 支持的寫 QPS 低,因爲 AOF 一般會配置成每秒 fsync 一次日誌文件,當然,每秒一次 fsync ,性能也還是很高的。(如果實時寫入,那麼 QPS 會大降,Redis 性能會大大降低)
  • 以前 AOF 發生過 bug,就是通過 AOF 記錄的日誌,進行數據恢復的時候,沒有恢復一模一樣的數據出來。所以說,類似 AOF 這種較爲複雜的基於命令日誌 / merge / 回放的方式,比基於 RDB 每次持久化一份完整的數據快照文件的方式,更加脆弱一些,容易有 bug。不過 AOF 就是爲了避免 rewrite 過程導致的 bug,因此每次 rewrite 並不是基於舊的指令日誌進行 merge 的,而是基於當時內存中的數據進行指令的重新構建,這樣健壯性會好很多。

RDB 和 AOF 到底該如何選擇

  • 不要僅僅使用 RDB,因爲那樣會導致你丟失很多數據;
  • 也不要僅僅使用 AOF,因爲那樣有兩個問題:第一,你通過 AOF 做冷備,沒有 RDB 做冷備來的恢復速度更快;第二,RDB 每次簡單粗暴生成數據快照,更加健壯,可以避免 AOF 這種複雜的備份和恢復機制的 bug;
  • Redis 支持同時開啓開啓兩種持久化方式,我們可以綜合使用 AOF 和 RDB 兩種持久化機制,用 AOF 來保證數據不丟失,作爲數據恢復的第一選擇; 用 RDB 來做不同程度的冷備,在 AOF 文件都丟失或損壞不可用的時候,還可以使用 RDB 來進行快速的數據恢復。

參考:
https://github.com/doocs/advanced-java
https://github.com/Snailclimb/JavaGuide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章