雖然我們可以通過各種手段來提升存儲系統(tǒng)的性能,但在某些復雜的業(yè)務場景下,單純依靠存儲系統(tǒng)的性能提升不夠的,典型的場景有:
需要經(jīng)過復雜運算后得出的數(shù)據(jù),存儲系統(tǒng)無能為力
例如,一個論壇需要在首頁展示當前有多少用戶同時在線,如果使用 MySQL 來存儲當前用戶狀態(tài),則每次獲取這個總數(shù)都要“count(*)”大量數(shù)據(jù),這樣的操作無論怎么優(yōu)化 MySQL,性能都不會太高。如果要實時展示用戶同時在線數(shù),則 MySQL 性能無法支撐。
讀多寫少的數(shù)據(jù),存儲系統(tǒng)有心無力
絕大部分在線業(yè)務都是讀多寫少。例如,微博、淘寶、微信這類互聯(lián)網(wǎng)業(yè)務,讀業(yè)務占了整體業(yè)務量的 90% 以上。以微博為例:一個明星發(fā)一條微博,可能幾千萬人來瀏覽。如果使用 MySQL 來存儲微博,用戶寫微博只有一條 insert 語句,但每個用戶瀏覽時都要 select 一次,即使有索引,幾千萬條 select 語句對 MySQL 數(shù)據(jù)庫的壓力也會非常大。
緩存就是為了彌補存儲系統(tǒng)在這些復雜業(yè)務場景下的不足,其基本原理是將可能重復使用的數(shù)據(jù)放到內(nèi)存中,一次生成、多次使用,避免每次使用都去訪問存儲系統(tǒng)。
緩存能夠帶來性能的大幅提升,以 Memcache 為例,單臺 Memcache 服務器簡單的 key-value 查詢能夠達到 TPS 50000 以上,其基本的架構(gòu)是:
?(http://pic001.cnblogs.com/img/dudu/200809/2008092816494460.png?)

緩存雖然能夠大大減輕存儲系統(tǒng)的壓力,但同時也給架構(gòu)引入了更多復雜性。架構(gòu)設計時如果沒有針對緩存的復雜性進行處理,某些場景下甚至會導致整個系統(tǒng)崩潰。今天,我來逐一分析緩存的架構(gòu)設計要點。
緩存穿透
緩存穿透是指緩存沒有發(fā)揮作用,業(yè)務系統(tǒng)雖然去緩存查詢數(shù)據(jù),但緩存中沒有數(shù)據(jù),業(yè)務系統(tǒng)需要再次去存儲系統(tǒng)查詢數(shù)據(jù)。通常情況下有兩種情況:
1. 存儲數(shù)據(jù)不存在
第一種情況是被訪問的數(shù)據(jù)確實不存在。一般情況下,如果存儲系統(tǒng)中沒有某個數(shù)據(jù),則不會在緩存中存儲相應的數(shù)據(jù),這樣就導致用戶查詢的時候,在緩存中找不到對應的數(shù)據(jù),每次都要去存儲系統(tǒng)中再查詢一遍,然后返回數(shù)據(jù)不存在。緩存在這個場景中并沒有起到分擔存儲系統(tǒng)訪問壓力的作用。
通常情況下,業(yè)務上讀取不存在的數(shù)據(jù)的請求量并不會太大,但如果出現(xiàn)一些異常情況,例如被黑客攻擊,故意大量訪問某些讀取不存在數(shù)據(jù)的業(yè)務,有可能會將存儲系統(tǒng)拖垮。
這種情況的解決辦法比較簡單,如果查詢存儲系統(tǒng)的數(shù)據(jù)沒有找到,則直接設置一個默認值(可以是空值,也可以是具體的值)存到緩存中,這樣第二次讀取緩存時就會獲取到默認值,而不會繼續(xù)訪問存儲系統(tǒng)。
2. 緩存數(shù)據(jù)生成耗費大量時間或者資源
第二種情況是存儲系統(tǒng)中存在數(shù)據(jù),但生成緩存數(shù)據(jù)需要耗費較長時間或者耗費大量資源。如果剛好在業(yè)務訪問的時候緩存失效了,那么也會出現(xiàn)緩存沒有發(fā)揮作用,訪問壓力全部集中在存儲系統(tǒng)上的情況。
典型的就是電商的商品分頁,假設我們在某個電商平臺上選擇“手機”這個類別查看,由于數(shù)據(jù)巨大,不能把所有數(shù)據(jù)都緩存起來,只能按照分頁來進行緩存,由于難以預測用戶到底會訪問哪些分頁,因此業(yè)務上最簡單的就是每次點擊分頁的時候按分頁計算和生成緩存。通常情況下這樣實現(xiàn)是基本滿足要求的,但是如果被競爭對手用爬蟲來遍歷的時候,系統(tǒng)性能就可能出現(xiàn)問題。
具體的場景有:
分頁緩存的有效期設置為 1 天,因為設置太長時間的話,緩存不能反應真實的數(shù)據(jù)。
通常情況下,用戶不會從第 1 頁到最后 1 頁全部看完,一般用戶訪問集中在前 10 頁,因此第 10 頁以后的緩存過期失效的可能性很大。
競爭對手每周來爬取數(shù)據(jù),爬蟲會將所有分類的所有數(shù)據(jù)全部遍歷,從第 1 頁到最后 1 頁全部都會讀取,此時很多分頁緩存可能都失效了。
由于很多分頁都沒有緩存數(shù)據(jù),從數(shù)據(jù)庫中生成緩存數(shù)據(jù)又非常耗費性能(order by limit 操作),因此爬蟲會將整個數(shù)據(jù)庫全部拖慢。
這種情況并沒有太好的解決方案,因為爬蟲會遍歷所有的數(shù)據(jù),而且什么時候來爬取也是不確定的,可能是每天都來,也可能是每周,也可能是一個月來一次,我們也不可能為了應對爬蟲而將所有數(shù)據(jù)永久緩存。通常的應對方案要么就是識別爬蟲然后禁止訪問,但這可能會影響 SEO 和推廣;要么就是做好監(jiān)控,發(fā)現(xiàn)問題后及時處理,因為爬蟲不是攻擊,不會進行暴力破壞,對系統(tǒng)的影響是逐步的,監(jiān)控發(fā)現(xiàn)問題后有時間進行處理。
緩存雪崩
緩存雪崩是指當緩存失效(過期)后引起系統(tǒng)性能急劇下降的情況。當緩存過期被清除后,業(yè)務系統(tǒng)需要重新生成緩存,因此需要再次訪問存儲系統(tǒng),再次進行運算,這個處理步驟耗時幾十毫秒甚至上百毫秒。而對于一個高并發(fā)的業(yè)務系統(tǒng)來說,幾百毫秒內(nèi)可能會接到幾百上千個請求。由于舊的緩存已經(jīng)被清除,新的緩存還未生成,并且處理這些請求的線程都不知道另外有一個線程正在生成緩存,因此所有的請求都會去重新生成緩存,都會去訪問存儲系統(tǒng),從而對存儲系統(tǒng)造成巨大的性能壓力。這些壓力又會拖慢整個系統(tǒng),嚴重的會造成數(shù)據(jù)庫宕機,從而形成一系列連鎖反應,造成整個系統(tǒng)崩潰。
緩存雪崩的常見解決方法有兩種:更新鎖機制和后臺更新機制。
1. 更新鎖
對緩存更新操作進行加鎖保護,保證只有一個線程能夠進行緩存更新,未能獲取更新鎖的線程要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認值。
對于采用分布式集群的業(yè)務系統(tǒng),由于存在幾十上百臺服務器,即使單臺服務器只有一個線程更新緩存,但幾十上百臺服務器一起算下來也會有幾十上百個線程同時來更新緩存,同樣存在雪崩的問題。因此分布式集群的業(yè)務系統(tǒng)要實現(xiàn)更新鎖機制,需要用到分布式鎖,如 ZooKeeper。
2. 后臺更新
由后臺線程來更新緩存,而不是由業(yè)務線程來更新緩存,緩存本身的有效期設置為永久,后臺線程定時更新緩存。
后臺定時機制需要考慮一種特殊的場景,當緩存系統(tǒng)內(nèi)存不夠時,會“踢掉”一些緩存數(shù)據(jù),從緩存被“踢掉”到下一次定時更新緩存的這段時間內(nèi),業(yè)務線程讀取緩存返回空值,而業(yè)務線程本身又不會去更新緩存,因此業(yè)務上看到的現(xiàn)象就是數(shù)據(jù)丟了。解決的方式有兩種:
后臺線程除了定時更新緩存,還要頻繁地去讀取緩存(例如,1 秒或者 100 毫秒讀取一次),如果發(fā)現(xiàn)緩存被“踢了”就立刻更新緩存,這種方式實現(xiàn)簡單,但讀取時間間隔不能設置太長,因為如果緩存被踢了,緩存讀取間隔時間又太長,這段時間內(nèi)業(yè)務訪問都拿不到真正的數(shù)據(jù)而是一個空的緩存值,用戶體驗一般。
業(yè)務線程發(fā)現(xiàn)緩存失效后,通過消息隊列發(fā)送一條消息通知后臺線程更新緩存??赡軙霈F(xiàn)多個業(yè)務線程都發(fā)送了緩存更新消息,但其實對后臺線程沒有影響,后臺線程收到消息后更新緩存前可以判斷緩存是否存在,存在就不執(zhí)行更新操作。這種方式實現(xiàn)依賴消息隊列,復雜度會高一些,但緩存更新更及時,用戶體驗更好。
后臺更新既適應單機多線程的場景,也適合分布式集群的場景,相比更新鎖機制要簡單一些。
后臺更新機制還適合業(yè)務剛上線的時候進行緩存預熱。緩存預熱指系統(tǒng)上線后,將相關的緩存數(shù)據(jù)直接加載到緩存系統(tǒng),而不是等待用戶訪問才來觸發(fā)緩存加載。
緩存熱點
雖然緩存系統(tǒng)本身的性能比較高,但對于一些特別熱點的數(shù)據(jù),如果大部分甚至所有的業(yè)務請求都命中同一份緩存數(shù)據(jù),則這份數(shù)據(jù)所在的緩存服務器的壓力也很大。例如,某明星微博發(fā)布“我們”來宣告戀愛了,短時間內(nèi)上千萬的用戶都會來圍觀。
緩存熱點的解決方案就是復制多份緩存副本,將請求分散到多個緩存服務器上,減輕緩存熱點導致的單臺緩存服務器壓力。以微博為例,對于粉絲數(shù)超過 100 萬的明星,每條微博都可以生成 100 份緩存,緩存的數(shù)據(jù)是一樣的,通過在緩存的 key 里面加上編號進行區(qū)分,每次讀緩存時都隨機讀取其中某份緩存。
緩存副本設計有一個細節(jié)需要注意,就是不同的緩存副本不要設置統(tǒng)一的過期時間,否則就會出現(xiàn)所有緩存副本同時生成同時失效的情況,從而引發(fā)緩存雪崩效應。正確的做法是設定一個過期時間范圍,不同的緩存副本的過期時間是指定范圍內(nèi)的隨機值。
實現(xiàn)方式
由于緩存的各種訪問策略和存儲的訪問策略是相關的,因此上面的各種緩存設計方案通常情況下都是集成在存儲訪問方案中,可以采用“程序代碼實現(xiàn)”的中間層方式,也可以采用獨立的中間件來實現(xiàn)。
小結(jié)
今天我為你講了高性能架構(gòu)設計中緩存設計需要注意的幾個關鍵點,這些關鍵點本身在技術上都不復雜,但可能對業(yè)務產(chǎn)生很大的影響,輕則系統(tǒng)響應變慢,重則全站宕機,架構(gòu)師在設計架構(gòu)的時候要特別注意這些細節(jié),希望這些設計關鍵點和技術方案對你有所幫助。
這就是今天的全部內(nèi)容,留一道思考題給你吧,分享一下你所在的業(yè)務發(fā)生過哪些因為緩存導致的線上問題?采取了什么樣的解決方案?效果如何?
歡迎你把答案寫到留言區(qū),和我一起討論。相信經(jīng)過深度思考的回答,也會讓你對知識的理解更加深刻。(編輯亂入:精彩的留言有機會獲得豐厚福利哦?。?/p>