靈魂拷問
前幾篇文章咱們介紹了緩存的優點以及數據一致性的問題,在一個面臨高併發系統中,緩存幾乎成了每一個架構師應對高流量的首衝解決方案,可是,一個好的緩存系統,除了和數據庫一致性問題以外,還存在着其餘問題,給總體的系統設計引入了額外的複雜性。而這些複雜性問題的解決方案也直接了影響系統的穩定性,最多見的好比緩存的命中率問題,在一個高併發系統中,核心功能的緩存命中率通常要保持在90%以上甚至更高,若是低於這個命中率,整個系統可能就面臨着隨時被峯值流量擊垮的可能,這個時候咱們就須要優化緩存的使用方式了。mysql
據說你還不會緩存?redis
若是按照傳統的緩存和DB的流程,一個請求到來的時候,首先會查詢緩存中是否存在,若是緩存中不存在則去查詢對應的數據庫。假如系統每秒的請求量爲10000,而緩存的命中率爲60%,則每秒穿透到數據庫的請求數爲4000,對於關係型數據庫mysql來講,每秒4000的請求量對於分了一主三從的Mysql數據庫架構來講也已經足夠大了,再加上主從的同步延遲等諸多因素,這個時候你的mysql已經行走在down機邊緣了。sql
緩存的最終目的,是在保證請求低延遲的狀況下,盡最大努力提升系統的吞吐量
那緩存系統可能會影響系統崩潰的緣由有那些呢?數據庫
緩存穿透是指:當一個請求到來的時候,在緩存中沒有查找到對應的數據(緩存未命中),業務系統不得不從數據庫(這裏其實能夠籠統的成爲後端系統)中加載數據
發生緩存穿透的緣由根據場景分爲兩種:後端
當數據在緩存和數據庫都不存在的時候,若是按照通常的緩存設計,每次請求都會到數據庫查詢一次,而後返回不存在,這種場景下,緩存系統幾乎沒有起任何做用。在正常的業務系統中,發生這種狀況的機率比較小,就算偶爾發生,也不會對數據庫形成根本上的壓力。設計模式
最可怕的是出現一些異常狀況,好比系統中有死循環的查詢或者被黑客攻擊的時候,尤爲是後者,他會故意僞造大量的請求來讀取不存在的數據而形成數據庫的down機,最典型的場景爲:若是系統的用戶id是連續遞增的int型,黑客很容易僞造用戶id來模擬大量的請求。緩存
這種場景通常屬於業務的正常需求,由於緩存系統的容量通常是有限制的,好比咱們最經常使用的Redis作爲緩存,就受到服務器內存大小的限制,因此全部的業務數據不可能都放入緩存系統中,根據互聯網數據的二八規則,咱們能夠優先把訪問最頻繁的熱點數據放入緩存系統,這樣就能利用緩存的優點來抗住主要的流量來源,而剩餘的非熱點數據,就算是有穿透數據庫的可能性,也不會對數據庫形成致命壓力。服務器
換句話說,每一個系統發生緩存穿透是不可避免的,而咱們須要作的是儘可能避免大量的請求發生穿透,那怎麼解決緩存穿透問題呢?解決緩存的穿透問題本質上是要解決怎麼樣攔截請求的問題,通常狀況下會有如下幾種方案:數據結構
當請求的數據在數據庫中不存在的時候,緩存系統能夠把對應的key寫入一個空值,這樣當下次一樣的請求就不會直接穿透數據庫,而直接返回緩存中的空值了。這種方案是最簡單粗暴的,可是要注意幾點:
//獲取用戶信息 public static UserInfo GetUserInfo(int userId) { //從緩存讀取用戶信息 var userInfo = GetUserInfoFromCache(userId); if (userInfo == null) { //回寫空值到緩存,並設置緩存過時時間爲10分鐘 CacheSystem.Set(userId, null,10); } return userInfo; }
布隆過濾器:將全部可能存在的數據哈希到一個足夠大的 bitmap 中,一個必定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力
布隆過濾器有幾個很大的優點
具體能夠參見以前的文章或者百度腦補一下布隆過濾器:
因爲布隆過濾器基於hash算法,因此在時間複雜度上是O(1),在應對高併發的場景下很是合適,不過使用布隆過濾器要求系統在產生數據的時候須要在布隆過濾器同時也寫入數據,並且布隆過濾器也不支持刪除數據,由於多個數據可能會重用同一個位置。
緩存雪崩是指緩存中數據大批量同時過時,形成查詢數據庫數據量巨大,引發數據庫壓力過大致使系統崩潰。
與緩存穿透現象不一樣,緩存穿透是指緩存中不存在數據而形成會對數據庫形成大量查詢,而緩存雪崩是由於緩存中存在數據,可是同時大量過時形成。可是本質上是同樣的,都是對數據庫形成了大量的請求。
不管是穿透仍是雪崩都面臨着一樣的數據會有多個線程同時請求,同時查詢數據庫,同時回寫緩存的一致性問題。舉例來講,當多個線程同時請求用戶id爲1的用戶,這個時候緩存正好失效,那這多個線程同時會查詢數據庫,而後同時會回寫緩存,最可怕的是,這個回寫的過程當中,另一個線程更新了數據庫,就形成了數據不一致,這個問題在以前的文章中着重講過,你們必定要注意。
一樣的數據會被多個線程產生多個請求是產生雪崩的一個緣由,針對這種狀況的解決方案是把多個線程的請求順序化,使其只有一個線程會產生對數據庫的查詢操做,好比最多見的鎖機制(分佈式鎖機制),如今最多見的分佈式鎖是用redis來實現,可是redis實現分佈式鎖也有必定的坑,能夠參見以前的文章(若是使用的是Actor模型的話會在無鎖的模式下更優雅的實現請求順序化)
多個緩存key同時失效的場景是產生雪崩的主要緣由,針對這樣的場景通常能夠利用如下幾種方案來解決
給緩存的每一個key設置不一樣的過時時間是最簡單的防止緩存雪崩的手段,總體思路是給每一個緩存的key在系統設置的過時時間之上加一個隨機值,或者乾脆是直接隨機一個值,有效的平衡key批量過時時間段,消掉單位之間內過時key數量的峯值。
public static int SetUserInfo(int userId) { //讀取用戶信息 var userInfo = GetUserInfoFromDB(userId); if (userInfo != null) { //回寫到緩存,並設置緩存過時時間爲隨機時間 var cacheExpire = new Random().Next(1, 100); CacheSystem.Set(userId, userInfo, cacheExpire); return cacheExpire; } return 0; }
這種場景下,能夠把緩存設置爲永不過時,緩存的更新不是由業務線程來更新,而是由專門的線程去負責。當緩存的key有更新時候,業務方向mq發送一個消息,更新緩存的線程會監聽這個mq來實時響應以便更新緩存中對應的數據。不過這種方式要考慮到緩存淘汰的場景,當一個緩存的key被淘汰以後,其實也能夠向mq發送一個消息,以達到更新線程從新回寫key的操做。
和數據庫同樣,緩存系統的設計一樣須要考慮高可用和擴展性。雖然緩存系統自己的性能已經比較高了,可是對於一些特殊的高併發的熱點數據,仍是會遇到單機的瓶頸。舉個栗子:假如某個明星出軌了,這個信息數據會緩存在某個緩存服務器的節點上,大量的請求會到達這個服務器節點,當到達必定程度的時候一樣會發生down機的狀況。相似於數據庫的主從架構,緩存系統也能夠複製多分緩存副本到其餘服務器上,這樣就能夠將應用的請求分散到多個緩存服務器上,緩解因爲熱點數據出現的單點問題。
和數據庫主從同樣,緩存的多個副本也面臨着數據的一致性問題,同步延遲問題,還有主從服務器相同key的過時時間問題。
至於緩存系統的擴展性一樣的道理,也能夠利用「分片」的原則,利用一致性哈希算法將不一樣的請求路由到不一樣的緩存服務器節點,來達到水平擴展的要求,這一點和應用的水平擴展道理同樣。
經過以上能夠看出,不管是應用服務器的高可用架構仍是數據庫的高可用架構,仍是緩存的高可用其實道理都是相似的,當咱們掌握了其中一種就很容易的擴展到任何場景中。若是這篇文章對你有多幫助,請分享給身邊的朋友,最後歡迎你們留言寫下大家在平常開發中用到的其餘關於緩存高可用,可擴展性,以及防止穿透和雪崩的方案,讓咱們一塊兒進步!!
更多精彩文章