緩存可以有效地加速應用的讀寫速度,同時也能夠下降後端負載,對平常應用的開發相當重要。下面會介紹緩存使用技巧和設計方案,包含以下內容:緩存的收益和成本分析、緩存更新策略的選擇和使用場景、緩存粒度控制法、穿透問題優化、無底洞問題優化、雪崩問題優化、熱點key重建優化。node
下圖左側爲客戶端直接調用存儲層的架構,右側爲比較典型的緩存層+存儲層架構。
程序員
緩存加入後帶來的收益和成本。redis
收益:
①加速讀寫:由於緩存一般都是全內存的,而存儲層一般讀寫性能不夠強悍(例如MySQL),經過緩存的使用能夠有效地加速讀寫,優化用戶體驗。
②下降後端負載:幫助後端減小訪問量和複雜計算(例如很複雜的SQL語句),在很大程度下降了後端的負載。
成本:
①數據不一致性:緩存層和存儲層的數據存在着必定時間窗口的不一致性,時間窗口跟更新策略有關。
②代碼維護成本:加入緩存後,須要同時處理緩存層和存儲層的邏輯,增大了開發者維護代碼的成本。
③運維成本:以Redis Cluster爲例,加入後無形中增長了運維成本。緩存的使用場景基本包含以下兩種:
①開銷大的複雜計算:以MySQL爲例子,一些複雜的操做或者計算(例如大量聯表操做、一些分組計算),若是不加緩存,不但沒法知足高併發量,同時也會給MySQL帶來巨大的負擔。
②加速請求響應:即便查詢單條後端數據足夠快(例如select*from tablewhere id=),那麼依然可使用緩存,以Redis爲例子,每秒能夠完成數萬次讀寫,而且提供的批量操做能夠優化整個IO鏈的響應時間。算法
緩存中的數據會和數據源中的真實數據有一段時間窗口的不一致,須要利用某些策略進行更新,下面會介紹幾種主要的緩存更新策略。
①LRU/LFU/FIFO算法剔除:剔除算法一般用於緩存使用量超過了預設的最大值時候,如何對現有的數據進行剔除。例如Redis使用maxmemory-policy這個配置做爲內存最大值後對於數據的剔除策略。
②超時剔除:經過給緩存數據設置過時時間,讓其在過時時間後自動刪除,例如Redis提供的expire命令。若是業務能夠容忍一段時間內,緩存層數據和存儲層數據不一致,那麼能夠爲其設置過時時間。在數據過時後,再從真實數據源獲取數據,從新放到緩存並設置過時時間。例如一個視頻的描述信息,能夠容忍幾分鐘內數據不一致,可是涉及交易
方面的業務,後果可想而知。
③主動更新:應用方對於數據的一致性要求高,須要在真實數據更新後,當即更新緩存數據。例如能夠利用消息系統或者其餘方式通知緩存更新。編程
有兩個建議:
①低一致性業務建議配置最大內存和淘汰策略的方式使用。後端②高一致性業務能夠結合使用超時剔除和主動更新,
這樣即便主動更新出了問題,也能保證數據過時時間後刪除髒數據。緩存
緩存粒度問題是一個容易被忽視的問題,若是使用不當,可能會形成不少無用空間的浪費,網絡帶寬的浪費,代碼通用性較差等狀況,須要綜合數據通用性、空間佔用比、代碼維護性三點進行取捨。
緩存比較經常使用的選型,緩存層選用Redis,存儲層選用MySQL。
網絡
假如我如今須要對視頻的信息作一個緩存,也就是須要對select * from video where id=?的每一個id在redis裏作一份緩存,這樣cache層就能夠幫助我抗住不少的訪問量(注:這裏不討論一致性和架構等等問題,只討論緩存的粒度問題)。
咱們假設視頻表有100個屬性(這個真有,有些人可能不可思議),那麼問題來了,須要緩存什麼維度呢,也
就是有兩種選擇吧:多線程
catch(id)=select * from video where id=#id catch(id)=select importantColumn1, importantColumn2 .. importantColumnN from video where id=#id 12
其實這個問題就是緩存粒度問題,咱們在緩存設計應該佮預估和考慮呢?下面咱們將從通用性、空間、代碼維護三個角度進行說明。架構
緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,一般出於容錯的考慮,若是從存儲層查不到數據則不寫入緩存層。
一般能夠在程序中分別統計總調用數、緩存層命中數、存儲層命中數,若是發現大量存儲層空命中,可能就是出現了緩存穿透問題。形成緩存穿透的基本緣由有兩個。第一,自身業務代碼或者數據出現問題,第二,一些惡意攻擊、爬蟲等形成大量空命中。下面咱們來看一下如何解決緩存穿透問題。
1.緩存空對象:如圖下所示,當第2步存儲層不命中後,仍然將空對象保留到緩存層中,以後再訪問這個數據將會從緩存中獲取,這樣就保護了後端數據源。
緩存空對象會有兩個問題:第一,空值作了緩存,意味着緩存層中存了更多的鍵,須要更多的內存空間(若是是攻擊,問題更嚴重),比較有效的方法是針對這類數據設置一個較短的過時時間,讓其自動剔除。第二,緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有必定影響。例如過時時間設置爲5分鐘,若是此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時能夠利用消息系統或者其餘方式清除掉緩存層中的空對象。
2.布隆過濾器攔截
以下圖所示,在訪問緩存層和存儲層以前,將存在的key用布隆過濾器提早保存起來,作第一層攔截。例如:一個推薦系統有4億個用戶id,每一個小時算法工程師會根據每一個用戶以前歷史行爲計算出推薦數據放到存儲層中,可是最新的用戶因爲沒有歷史行爲,就會發生緩存穿透的行爲,爲此能夠將全部推薦數據的用戶作成布隆過濾器。若是布隆過濾器認爲該用戶id不存在,那麼就不會訪問存儲層,在必定程度保護了存儲層。
爲了知足業務須要可能會添加大量新的緩存節點,可是發現性能不但沒有好轉反而降低了。 用一句通俗的話解釋就是,更多的節點不表明更高的性能,所謂「無底洞」就是說投入越多不必定產出越多。可是分佈式又是不能夠避免的,由於訪問量和數據量愈來愈大,一個節點根本抗不住,因此如何高效地在分佈式緩存中批量操做是一個難點。
無底洞問題分析:
①客戶端一次批量操做會涉及屢次網絡操做,也就意味着批量操做會隨着節點的增多,耗時會不斷增大。
②網絡鏈接數變多,對節點的性能也有必定影響。
如何在分佈式條件下優化批量操做?咱們來看一下常見的IO優化思路:
命令自己的優化,例如優化SQL語句等。
減小網絡通訊次數。
下降接入成本,例如客戶端使用長連/鏈接池、NIO等。
這裏咱們假設命令、客戶端鏈接已經爲最優,重點討論減小網絡操做次數。下面咱們將結合Redis Cluster的一些特性對四種分佈式的批量操做方式進行說明。
①串行命令:因爲n個key是比較均勻地分佈在Redis Cluster的各個節點上,所以沒法使用mget命令一次性獲取,因此一般來說要獲取n個key的值,最簡單的方法就是逐次執行n個get命令,這種操做時間複雜度較高,它的操做時間=n次網絡時間+n次命令時間,網絡次數是n。很顯然這種方案不是最優的,可是實現起來比較簡單。
②串行IO:Redis Cluster使用CRC16算法計算出散列值,再取對16383的餘數就能夠算出slot值,同時Smart客戶端會保存slot和節點的對應關係,有了這兩個數據就能夠將屬於同一個節點的key進行歸檔,獲得每一個節點的key子列表,以後對每一個節點執行mget或者Pipeline操做,它的操做時間=node次網絡時間+n次命令時間,網絡次數是node的個數,整個過程以下圖所示,很明顯這種方案比第一種要好不少,可是若是節點數太多,仍是有必定的性能問題。
③並行IO:此方案是將方案2中的最後一步改成多線程執行,網絡次數雖然仍是節點個數,但因爲使用多線程網絡時間變爲O(1),這種方案會增長編程的複雜度。
④hash_tag實現:Redis Cluster的hash_tag功能,它能夠將多個key強制分配到一個節點上,它的操做時間=1次網絡時間+n次命令時間。
四種批量操做解決方案對比
因爲緩存層承載着大量請求,有效地保護了存儲層,可是若是緩存層因爲某些緣由不能提供服務,因而全部的請求都會達到存儲層,存儲層的調用量會暴增,形成存儲層也會級聯宕機的狀況。
預防和解決緩存雪崩問題,能夠從如下三個方面進行着手:
開發人員使用「緩存+過時時間」的策略既能夠加速數據讀寫,又保證數據的按期更新,這種模式基本可以知足絕大部分需求。可是有兩個問題若是同時出現,可能就會對應用形成致命的危害:
當前key是一個熱點key(例如一個熱門的娛樂新聞),併發量很是大。
重建緩存不能在短期完成,多是一個複雜計算,例如複雜的SQL、屢次IO、多個依賴等。在緩存失效的瞬間,有大量線程來重建緩存,形成後端負載加大,甚至可能會讓應用崩潰。
要解決這個問題也不是很複雜,可是不能爲了解決這個問題給系統帶來更多的麻煩,因此須要制定以下目標:
減小重建緩存的次數
數據儘量一致
較少的潛在危險
①互斥鎖:此方法只容許一個線程重建緩存,其餘線程等待重建緩存的線程執行完,從新從緩存獲取數據便可,整個過程如圖所示。
②永遠不過時
永遠不過時」包含兩層意思: 從緩存層面來看,確實沒有設置過時時間,因此不會出現熱點key過時後產生的問題,也就是「物理」不過時。 從功能層面來看,爲每一個value設置一個邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存。
從實戰看,此方法有效杜絕了熱點key產生的問題,但惟一不足的就是重構緩存期間,會出現數據不一致的狀況,這取決於應用方是否容忍這種不一致。
兩種熱點key的解決方法