高併發系統三大利器之緩存

引言

隨着互聯網的高速發展,市面上也出現了愈來愈多的網站和app。咱們判斷一個軟件是否好用,用戶體驗就是一個重要的衡量標準。好比說咱們常常用的微信,打開一個頁面要十幾秒,發個語音要幾分鐘對方纔能收到。相信這樣的軟件你們確定是都不肯意用的。軟件要作到用戶體驗好,響應速度快,緩存就是必不可少的一個神器。緩存又分進程內緩存和分佈式緩存兩種:分佈式緩存如redismemcached等,還有本地(進程內)緩存如ehcacheGuavaCacheCaffeine等。html

緩存特徵

緩存做爲一個數據數據模型對象,那麼它有一些什麼樣的特徵呢?下面咱們分別來介紹下這些特徵。java

命中率

  • 命中率=命中數/(命中數+沒有命中數)當某個請求可以經過訪問緩存而獲得響應時,稱爲緩存命中。緩存命中率越高,緩存的利用率也就越高。

最大空間

  • 緩存中能夠容納最大元素的數量。當緩存存放的數據超過最大空間時,就須要根據淘汰算法來淘汰部分數據存放新到達的數據。

淘汰算法

  • 緩存的存儲空間有限制,當緩存空間被用滿時,如何保證在穩定服務的同時有效提高命中率?這就由緩存淘汰算法來處理,設計適合自身數據特徵的淘汰算法可以有效提高緩存命中率。常見的淘汰算法有:
FIFO(first in first out)
  • 先進先出。最早進入緩存的數據在緩存空間不夠的狀況下(超出最大元素限制)會被優先被清除掉,以騰出新的空間接受新的數據。策略算法主要比較緩存元素的建立時間。適用於保證高頻數據有效性場景,優先保障最新數據可用
LFU(less frequently used)
  • 最少使用,不管是否過時,根據元素的被使用次數判斷,清除使用次數較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數)。適用於保證高頻數據有效性場景
LRU(least recently used)
  • 最近最少使用,不管是否過時,根據元素最後一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時間。比較適用於熱點數據場景,優先保證熱點數據的有效性。

進程緩存

爲何須要引入本地緩存,本地緩存的應用場景有哪些?

本地緩存的話是咱們的應用和緩存都在同一個進程裏面,獲取緩存數據的時候純內存操做,沒有額外的網絡開銷,速度很是快。它適用於緩存一些應用中基本不會變化的數據,好比(國家、省份、城市等)。redis

項目中通常如何適用、怎麼樣加載、怎麼樣更新?

進程緩存的話,通常能夠在應用啓動的時候,把須要的數據加載到系統中。更新緩存的話能夠採起定時更新(實時性不高)。具體實現的話就是在應用中起一個定時任務(ScheduledExecutorServiceTimerTask等),讓它每隔多久去加載變動(數據變動以後能夠修改數據庫最後修改的時間,每次查詢變動數據的時候均可以根據這個最後變動時間加上半小時大於當前時間的數據)的數據從新到緩存裏面來。若是以爲這個比較麻煩的話,還能夠直接所有全量更新(就跟項目啓動加載數據同樣)。這種方式的話,對數據更新可能會有點延遲。可能這臺機器看到的是更新後的數據,那臺機器看到的數據仍是老的(機器發佈時間可能不同)。因此這種方式比較適用於對數據實時性要求不高的數據。若是對實時性有要求的話能夠經過廣播訂閱mq消息。若是有數據更新mq會把更新數據推送到每一臺機器,這種方式的話實時性會比前一種定時更新的方法會好。可是實現起來會比較複雜。
在這裏插入圖片描述算法

本地緩存有哪些實現方式?

常見本地緩存有如下幾種實現方式:
圖片來源於https://juejin.im/post/6844903665845665805
從上述表格咱們看出性能最佳的是Caffeine。關於這個本地緩存的話我仍是強烈推薦的,裏面提供了豐富的api,以及各類各樣的淘汰算法。如需瞭解更加詳細的話能夠看下之前寫的這個篇文章《本地緩存性能之王Caffeine》數據庫

本地緩存缺點

  • 本地緩存與業務系統耦合再一塊兒,應用之間沒法直接共享緩存的內容。須要每一個應用節點單獨的維護本身的緩存。每一個節點都須要一份同樣的緩存,對服務器內存形成一種浪費。本地緩存機器重啓、或者宕機都會丟失。

分佈式緩存

  • 分佈式緩存是與應用分離的緩存組件或服務,其最大的優勢是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享緩存。常見的分佈式緩存有redisMemCache等。

分佈式緩存的應用

在高併發的環境下,好比春節搶票大戰,一到放票的時間節點,分分鐘大量用戶以及黃牛的各類搶票軟件流量進入12306,這時候若是每一個用戶的訪問都去數據庫實時查詢票的庫存,大量讀的請求涌入到數據庫,瞬間Db就會被打爆,cpu直接上升100%,服務立刻就要宕機或者假死。即便進行了分庫分表也是沒法避免的。爲了減輕db的壓力以及提升系統的響應速度。通常都會在數據庫前面加上一層緩存,甚至可能還會有多級緩存。api

緩存常見問題

緩存雪崩

指大量緩存同一時間段集體失效,或者緩存總體不能提供服務,致使大量的請求所有到達數據庫
對數據CPU和內存形成巨大壓力,嚴重的會形成數據庫宕機。所以而造成的一系列連鎖反應形成整個系統奔潰。
解決這個問題能夠從如下方面入手:緩存

  • 保證緩存的高可用。使用redis的集羣模式,即便個別redis節點下線,緩存仍是能夠用。通常稍微大點的公司還可能會在多個機房部署Redis。
    這樣即便某個機房忽然停電,或者光纖又被挖斷了,這時候緩存仍是可使用。
  • 使用多級緩存。不一樣級別緩存時間過期時間不同,即便某個級別緩存過時了,還有其餘緩存級別
    兜底。好比咱們Redis緩存過時了,咱們還有本地緩存。這樣的話即便沒有命中redis,有可能會命中本地緩存。
  • 緩存永不過時。Redis中保存的key永久不失效,這樣的話就不會出現大量緩存同時失效的問題,可是這種作法會浪費更多的存儲空間,通常應該也不會推薦這種作法。
  • 使用隨機過時時間。爲每個key都合理的設計一個過時時間,這樣能夠避免大量的key再同一時刻集體失效。
  • 異步重建緩存。這樣的話須要維護每一個key的過時時間,定時去輪詢這些key的過時時間。例如一個keyvalue設置的過時時間是30min,那咱們能夠爲這個key設置它本身的一個過時時間爲20min。因此當這個key到了20min的時候咱們就能夠從新去構建這個key的緩存,同時也更新這個key的一個過時時間。
緩存穿透

指查詢一個不存在的數據,每次經過接口或者去查詢數據庫都查不到這個數據,好比黑客的惡意攻擊,好比知道一個訂單號後,而後就僞造一些不存在的訂單號,而後併發來請求你這個訂單詳情。這些訂單號在緩存中都查詢不到,而後會致使把這些查詢請求所有打到數據庫或者SOA接口。這樣的話就會致使數據庫宕機或者你的服務大量超時。
這種查詢不存在的數據就是緩存擊穿。
解決這個問題能夠從如下方面入手:服務器

  • 緩存空值,對於這些不存在的請求,仍然給它緩存一個空的結果,這種方式簡單粗暴,可是若是後續這個請求有新值了須要把原來緩存的空值刪除掉(因此通常過時時間能夠稍微設置的比較短)。
  • 經過布隆過濾器。查詢緩存以前先去布隆過濾器查詢下這個數據是否存在。若是數據不存在,而後直接返回空。這樣的話也會減小底層系統的查詢壓力。
  • 緩存沒有直接返回。 這種方式的話要根據本身的實際業務來進行選擇。好比固定的數據,一些省份信息或者城市信息,能夠所有緩存起來。這樣的話數據有變化的狀況,緩存也須要跟着變化。實現起來可能比較複雜。
緩存擊穿

是指緩存裏面的一個熱點key(拼多多的五菱宏光神車的秒殺)在某個時間點過時。針對於這一個key有大量併發請求過來而後都會同時去數據庫請求數據,瞬間對數據庫形成巨大的壓力。
這個的話能夠用緩存雪崩的幾種解決方法來避免:微信

  • 緩存永不過時。Redis中保存的key永久不失效,這樣的話就不會出現大量緩存同時失效的問題,可是這種作法會浪費更多的存儲空間,通常應該也不會推薦這種作法。
  • 異步重建緩存。這樣的話須要維護每一個key的過時時間,定時去輪詢這些key的過時時間。例如一個keyvalue設置的過時時間是30min,那咱們能夠爲這個key設置它本身的一個過時時間爲20min。因此當這個key到了20min的時候咱們就能夠從新去構建這個key的緩存,同時也更新這個key的一個過時時間。
  • 互斥鎖重建緩存。這種狀況的話只能針對於同一個key的狀況下,好比你有100個併發請求都要來取A的緩存,這時候咱們能夠藉助redis分佈式鎖來構建緩存,讓只有一個請求能夠去查詢DB其餘99個(沒有獲取到鎖)都在外面等着,等A查詢到數據而且把緩存構建好以後其餘99個請求都只須要從緩存取就行了。原理就跟咱們javaDCL(double checked locking)思想有點相似。
    在這裏插入圖片描述

緩存更新

咱們通常的緩存更新主要有如下幾種更新策略:網絡

  • 先更新緩存,再更新數據庫
  • 先更新數據庫,再更新緩存
  • 先刪除緩存,再更新數據庫
  • 先更新數據源庫,再刪除緩存
    至於選擇哪一種更新策略的話,沒有絕對的選擇,能夠根據本身的業務狀況來選擇適合本身的不過通常推薦的話是選擇 先更新數據源庫,再刪除緩存。關於這幾種更新的介紹能夠推薦你們看下博客園大佬孤獨煙寫的《分佈式之數據庫和緩存雙寫一致性方案解析》這一篇文章,看完文章評論也能夠去看看,評論跟內容同樣精彩。

總結

若是想要真正的設計好一個緩存,咱們仍是必需要掌握不少的知識,對於不一樣場景,緩存有各自不一樣的用法。好比實際工做中咱們對於訂單詳情的一個緩存。咱們可能會根據訂單的狀態來來構建緩存。咱們就以機票訂單爲例,已出行、或者已經取消的訂單咱們基本上是不會去管的(訂單狀態已經終止了),這種的話數據基本也不會變了,因此對於這種訂單咱們設置的過時時間是否是就能夠久一點,好比7天或者30天。對於未出行即將起飛的訂單,這時候顧客是否是就會頻繁的去刷新訂單看看,看看有沒有晚點什麼的,或者登機口是在哪。對於這種實時性要求比較高的訂單咱們過時時間仍是要設置的比較短的,若是是須要更改訂單的狀態查詢的時候能夠直接不走緩存,直接查詢master庫。畢竟這種更改訂單狀態的操做仍是比較有限的。大多數狀況都是用來展現的。展現的話是能夠容許實時性要求沒那麼高。總的來講須要開具體的業務,沒有通用的方案。看你的業務需求的容忍度,畢竟脫離了業務來談技術都是耍流氓,是業務驅動技術。

結束

  • 因爲本身才疏學淺,不免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。

站在巨人的肩膀上摘蘋果:
https://juejin.im/post/6844903665845665805
https://tech.meituan.com/2017/03/17/cache-about.html
http://www.javashuo.com/article/p-ftdcqtnl-cx.html

相關文章
相關標籤/搜索