高並發系統三大利器之快取
引言
隨著互聯網的高速發展,市面上也出現了越來越多的網站和app
。我們判斷一個軟體是否好用,用戶體驗就是一個重要的衡量標準。比如說我們經常用的微信,打開一個頁面要十幾秒,發個語音要幾分鐘對方才能收到。相信這樣的軟體大家肯定是都不願意用的。軟體要做到用戶體驗好,響應速度快,快取就是必不可少的一個神器。快取又分進程內快取和分散式快取兩種:分散式快取如redis
、memcached
等,還有本地(進程內)快取如ehcache
、GuavaCache
、Caffeine
等。
快取特徵
快取作為一個數據數據模型對象,那麼它有一些什麼樣的特徵呢?下面我們分別來介紹下這些特徵。
命中率
- 命中率=命中數/(命中數+沒有命中數)當某個請求能夠通過訪問快取而得到響應時,稱為快取命中。快取命中率越高,快取的利用率也就越高。
最大空間
- 快取中可以容納最大元素的數量。當快取存放的數據超過最大空間時,就需要根據淘汰演算法來淘汰部分數據存放新到達的數據。
淘汰演算法
- 快取的存儲空間有限制,當快取空間被用滿時,如何保證在穩定服務的同時有效提升命中率?這就由快取淘汰演算法來處理,設計適合自身數據特徵的淘汰演算法能夠有效提升快取命中率。常見的淘汰演算法有:
FIFO(first in first out)
- 先進先出。最先進入快取的數據在快取空間不夠的情況下(超出最大元素限制)會被優先被清除掉,以騰出新的空間接受新的數據。策略演算法主要比較快取元素的創建時間。適用於保證高頻數據有效性場景,優先保障最新數據可用。
LFU(less frequently used)
- 最少使用,無論是否過期,根據元素的被使用次數判斷,清除使用次數較少的元素釋放空間。策略演算法主要比較元素的
hitCount
(命中次數)。適用於保證高頻數據有效性場景。
LRU(least recently used)
- 最近最少使用,無論是否過期,根據元素最後一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略演算法主要比較元素最近一次被get使用時間。比較適用於熱點數據場景,優先保證熱點數據的有效性。
進程快取
為什麼需要引入本地快取,本地快取的應用場景有哪些?
本地快取的話是我們的應用和快取都在同一個進程裡面,獲取快取數據的時候純記憶體操作,沒有額外的網路開銷,速度非常快。它適用於快取一些應用中基本不會變化的數據,比如(國家、省份、城市等)。
項目中一般如何適用、怎麼樣載入、怎麼樣更新?
進程快取的話,一般可以在應用啟動的時候,把需要的數據載入到系統中。更新快取的話可以採取定時更新(實時性不高)。具體實現的話就是在應用中起一個定時任務(ScheduledExecutorService、TimerTask等),讓它每隔多久去載入變更(數據變更之後可以修改資料庫最後修改的時間,每次查詢變更數據的時候都可以根據這個最後變更時間加上半小時大於當前時間的數據)的數據重新到快取裡面來。如果覺得這個比較麻煩的話,還可以直接全部全量更新(就跟項目啟動載入數據一樣)。這種方式的話,對數據更新可能會有點延遲。可能這台機器看到的是更新後的數據,那台機器看到的數據還是老的(機器發布時間可能不一樣)。所以這種方式比較適用於對數據實時性要求不高的數據。如果對實時性有要求的話可以通過廣播訂閱mq
消息。如果有數據更新mq
會把更新數據推送到每一台機器,這種方式的話實時性會比前一種定時更新的方法會好。但是實現起來會比較複雜。
本地快取有哪些實現方式?
常見本地快取有以下幾種實現方式:
從上述表格我們看出性能最佳的是Caffeine
。關於這個本地快取的話我還是強烈推薦的,裡面提供了豐富的api
,以及各種各樣的淘汰演算法。如需了解更加詳細的話可以看下以前寫的這個篇文章《本地快取性能之王Caffeine》。
本地快取缺點
- 本地快取與業務系統耦合再一起,應用之間無法直接共享快取的內容。需要每個應用節點單獨的維護自己的快取。每個節點都需要一份一樣的快取,對伺服器記憶體造成一種浪費。本地快取機器重啟、或者宕機都會丟失。
分散式快取
- 分散式快取是與應用分離的快取組件或服務,其最大的優點是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享快取。常見的分散式快取有
redis
、MemCache
等。
分散式快取的應用
在高並發的環境下,比如春節搶票大戰,一到放票的時間節點,分分鐘大量用戶以及黃牛的各種搶票軟體流量進入12306
,這時候如果每個用戶的訪問都去資料庫實時查詢票的庫存,大量讀的請求湧入到資料庫,瞬間Db
就會被打爆,cpu
直接上升100%
,服務馬上就要宕機或者假死。即使進行了分庫分表也是無法避免的。為了減輕db的壓力以及提高系統的響應速度。一般都會在資料庫前面加上一層快取,甚至可能還會有多級快取。
快取常見問題
快取雪崩
指大量快取同一時間段集體失效,或者快取整體不能提供服務,導致大量的請求全部到達資料庫
對數據CPU
和記憶體造成巨大壓力,嚴重的會造成資料庫宕機。因此而形成的一系列連鎖反應造成整個系統奔潰。
解決這個問題可以從以下方面入手:
- 保證快取的高可用。使用redis的集群模式,即使個別
redis
節點下線,快取還是可以用。一般稍微大點的公司還可能會在多個機房部署Redis。
這樣即使某個機房突然停電,或者光纖又被挖斷了,這時候快取還是可以使用。 - 使用多級快取。不同級別快取時間過時時間不一樣,即使某個級別快取過期了,還有其他快取級別
兜底。比如我們Redis快取過期了,我們還有本地快取。這樣的話即使沒有命中redis,有可能會命中本地快取。 - 快取永不過期。Redis中保存的key永久不失效,這樣的話就不會出現大量快取同時失效的問題,但是這種做法會浪費更多的存儲空間,一般應該也不會推薦這種做法。
- 使用隨機過期時間。為每一個key都合理的設計一個過期時間,這樣可以避免大量的key再同一時刻集體失效。
- 非同步重建快取。這樣的話需要維護每個
key
的過期時間,定時去輪詢這些key
的過期時間。例如一個key
的value
設置的過期時間是30min
,那我們可以為這個key設置它自己的一個過期時間為20min
。所以當這個key
到了20min的時候我們就可以重新去構建這個key
的快取,同時也更新這個key
的一個過期時間。
快取穿透
指查詢一個不存在的數據,每次通過介面或者去查詢資料庫都查不到這個數據,比如黑客的惡意攻擊,比如知道一個訂單號後,然後就偽造一些不存在的訂單號,然後並發來請求你這個訂單詳情。這些訂單號在快取中都查詢不到,然後會導致把這些查詢請求全部打到資料庫或者SOA介面。這樣的話就會導致資料庫宕機或者你的服務大量超時。
這種查詢不存在的數據就是快取擊穿。
解決這個問題可以從以下方面入手:
- 快取空值,對於這些不存在的請求,仍然給它快取一個空的結果,這種方式簡單粗暴,但是如果後續這個請求有新值了需要把原來快取的空值刪除掉(所以一般過期時間可以稍微設置的比較短)。
- 通過布隆過濾器。查詢快取之前先去布隆過濾器查詢下這個數據是否存在。如果數據不存在,然後直接返回空。這樣的話也會減少底層系統的查詢壓力。
- 快取沒有直接返回。 這種方式的話要根據自己的實際業務來進行選擇。比如固定的數據,一些省份資訊或者城市資訊,可以全部快取起來。這樣的話數據有變化的情況,快取也需要跟著變化。實現起來可能比較複雜。
快取擊穿
是指快取裡面的一個熱點key
(拼多多的五菱宏光神車的秒殺)在某個時間點過期。針對於這一個key有大量並發請求過來然後都會同時去資料庫請求數據,瞬間對資料庫造成巨大的壓力。
這個的話可以用快取雪崩的幾種解決方法來避免:
- 快取永不過期。Redis中保存的key永久不失效,這樣的話就不會出現大量快取同時失效的問題,但是這種做法會浪費更多的存儲空間,一般應該也不會推薦這種做法。
- 非同步重建快取。這樣的話需要維護每個
key
的過期時間,定時去輪詢這些key
的過期時間。例如一個key
的value
設置的過期時間是30min
,那我們可以為這個key設置它自己的一個過期時間為20min
。所以當這個key到了20min的時候我們就可以重新去構建這個key
的快取,同時也更新這個key
的一個過期時間。 - 互斥鎖重建快取。這種情況的話只能針對於同一個
key
的情況下,比如你有100
個並發請求都要來取A
的快取,這時候我們可以藉助redis分散式鎖來構建快取,讓只有一個請求可以去查詢DB其他99個(沒有獲取到鎖)都在外面等著,等A查詢到數據並且把快取構建好之後其他99
個請求都只需要從快取取就好了。原理就跟我們java
的DCL
(double checked locking)思想有點類似。
快取更新
我們一般的快取更新主要有以下幾種更新策略:
- 先更新快取,再更新資料庫
- 先更新資料庫,再更新快取
- 先刪除快取,再更新資料庫
- 先更新數據源庫,再刪除快取
至於選擇哪種更新策略的話,沒有絕對的選擇,可以根據自己的業務情況來選擇適合自己的不過一般推薦的話是選擇 先更新數據源庫,再刪除快取。關於這幾種更新的介紹可以推薦大家看下部落格園大佬孤獨煙寫的《分散式之資料庫和快取雙寫一致性方案解析》這一篇文章,看完文章評論也可以去看看,評論跟內容一樣精彩。
總結
如果想要真正的設計好一個快取,我們還是必須要掌握很多的知識,對於不同場景,快取有各自不同的用法。比如實際工作中我們對於訂單詳情的一個快取。我們可能會根據訂單的狀態來來構建快取。我們就以機票訂單為例,已出行、或者已經取消的訂單我們基本上是不會去管的(訂單狀態已經終止了),這種的話數據基本也不會變了,所以對於這種訂單我們設置的過期時間是不是就可以久一點,比如7天或者30天。對於未出行即將起飛的訂單,這時候顧客是不是就會頻繁的去刷新訂單看看,看看有沒有晚點什麼的,或者登機口是在哪。對於這種實時性要求比較高的訂單我們過期時間還是要設置的比較短的,如果是需要更改訂單的狀態查詢的時候可以直接不走快取,直接查詢master
庫。畢竟這種更改訂單狀態的操作還是比較有限的。大多數情況都是用來展示的。展示的話是可以允許實時性要求沒那麼高。總的來說需要開具體的業務,沒有通用的方案。看你的業務需求的容忍度,畢竟脫離了業務來談技術都是耍流氓,是業務驅動技術。
結束
- 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎並感謝您的關注。
站在巨人的肩膀上摘蘋果:
//juejin.im/post/6844903665845665805
//tech.meituan.com/2017/03/17/cache-about.html
//www.cnblogs.com/rjzheng/p/9041659.html#!comments