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

緩存雖然能夠大大減輕存儲(chǔ)系統(tǒng)的壓力,但同時(shí)也給架構(gòu)引入了更多復(fù)雜性。架構(gòu)設(shè)計(jì)時(shí)如果沒有針對(duì)緩存的復(fù)雜性進(jìn)行處理,某些場景下甚至?xí)?dǎo)致整個(gè)系統(tǒng)崩潰。今天,我來逐一分析緩存的架構(gòu)設(shè)計(jì)要點(diǎn)。
緩存穿透
緩存穿透是指緩存沒有發(fā)揮作用,業(yè)務(wù)系統(tǒng)雖然去緩存查詢數(shù)據(jù),但緩存中沒有數(shù)據(jù),業(yè)務(wù)系統(tǒng)需要再次去存儲(chǔ)系統(tǒng)查詢數(shù)據(jù)。通常情況下有兩種情況:
1. 存儲(chǔ)數(shù)據(jù)不存在
第一種情況是被訪問的數(shù)據(jù)確實(shí)不存在。一般情況下,如果存儲(chǔ)系統(tǒng)中沒有某個(gè)數(shù)據(jù),則不會(huì)在緩存中存儲(chǔ)相應(yīng)的數(shù)據(jù),這樣就導(dǎo)致用戶查詢的時(shí)候,在緩存中找不到對(duì)應(yīng)的數(shù)據(jù),每次都要去存儲(chǔ)系統(tǒng)中再查詢一遍,然后返回?cái)?shù)據(jù)不存在。緩存在這個(gè)場景中并沒有起到分擔(dān)存儲(chǔ)系統(tǒng)訪問壓力的作用。
通常情況下,業(yè)務(wù)上讀取不存在的數(shù)據(jù)的請(qǐng)求量并不會(huì)太大,但如果出現(xiàn)一些異常情況,例如被黑客攻擊,故意大量訪問某些讀取不存在數(shù)據(jù)的業(yè)務(wù),有可能會(huì)將存儲(chǔ)系統(tǒng)拖垮。
這種情況的解決辦法比較簡單,如果查詢存儲(chǔ)系統(tǒng)的數(shù)據(jù)沒有找到,則直接設(shè)置一個(gè)默認(rèn)值(可以是空值,也可以是具體的值)存到緩存中,這樣第二次讀取緩存時(shí)就會(huì)獲取到默認(rèn)值,而不會(huì)繼續(xù)訪問存儲(chǔ)系統(tǒng)。
2. 緩存數(shù)據(jù)生成耗費(fèi)大量時(shí)間或者資源
第二種情況是存儲(chǔ)系統(tǒng)中存在數(shù)據(jù),但生成緩存數(shù)據(jù)需要耗費(fèi)較長時(shí)間或者耗費(fèi)大量資源。如果剛好在業(yè)務(wù)訪問的時(shí)候緩存失效了,那么也會(huì)出現(xiàn)緩存沒有發(fā)揮作用,訪問壓力全部集中在存儲(chǔ)系統(tǒng)上的情況。
典型的就是電商的商品分頁,假設(shè)我們在某個(gè)電商平臺(tái)上選擇“手機(jī)”這個(gè)類別查看,由于數(shù)據(jù)巨大,不能把所有數(shù)據(jù)都緩存起來,只能按照分頁來進(jìn)行緩存,由于難以預(yù)測用戶到底會(huì)訪問哪些分頁,因此業(yè)務(wù)上最簡單的就是每次點(diǎn)擊分頁的時(shí)候按分頁計(jì)算和生成緩存。通常情況下這樣實(shí)現(xiàn)是基本滿足要求的,但是如果被競爭對(duì)手用爬蟲來遍歷的時(shí)候,系統(tǒng)性能就可能出現(xiàn)問題。
具體的場景有:
分頁緩存的有效期設(shè)置為 1 天,因?yàn)樵O(shè)置太長時(shí)間的話,緩存不能反應(yīng)真實(shí)的數(shù)據(jù)。
通常情況下,用戶不會(huì)從第 1 頁到最后 1 頁全部看完,一般用戶訪問集中在前 10 頁,因此第 10 頁以后的緩存過期失效的可能性很大。
競爭對(duì)手每周來爬取數(shù)據(jù),爬蟲會(huì)將所有分類的所有數(shù)據(jù)全部遍歷,從第 1 頁到最后 1 頁全部都會(huì)讀取,此時(shí)很多分頁緩存可能都失效了。
由于很多分頁都沒有緩存數(shù)據(jù),從數(shù)據(jù)庫中生成緩存數(shù)據(jù)又非常耗費(fèi)性能(order by limit 操作),因此爬蟲會(huì)將整個(gè)數(shù)據(jù)庫全部拖慢。
這種情況并沒有太好的解決方案,因?yàn)榕老x會(huì)遍歷所有的數(shù)據(jù),而且什么時(shí)候來爬取也是不確定的,可能是每天都來,也可能是每周,也可能是一個(gè)月來一次,我們也不可能為了應(yīng)對(duì)爬蟲而將所有數(shù)據(jù)永久緩存。通常的應(yīng)對(duì)方案要么就是識(shí)別爬蟲然后禁止訪問,但這可能會(huì)影響 SEO 和推廣;要么就是做好監(jiān)控,發(fā)現(xiàn)問題后及時(shí)處理,因?yàn)榕老x不是攻擊,不會(huì)進(jìn)行暴力破壞,對(duì)系統(tǒng)的影響是逐步的,監(jiān)控發(fā)現(xiàn)問題后有時(shí)間進(jìn)行處理。
緩存雪崩
緩存雪崩是指當(dāng)緩存失效(過期)后引起系統(tǒng)性能急劇下降的情況。當(dāng)緩存過期被清除后,業(yè)務(wù)系統(tǒng)需要重新生成緩存,因此需要再次訪問存儲(chǔ)系統(tǒng),再次進(jìn)行運(yùn)算,這個(gè)處理步驟耗時(shí)幾十毫秒甚至上百毫秒。而對(duì)于一個(gè)高并發(fā)的業(yè)務(wù)系統(tǒng)來說,幾百毫秒內(nèi)可能會(huì)接到幾百上千個(gè)請(qǐng)求。由于舊的緩存已經(jīng)被清除,新的緩存還未生成,并且處理這些請(qǐng)求的線程都不知道另外有一個(gè)線程正在生成緩存,因此所有的請(qǐng)求都會(huì)去重新生成緩存,都會(huì)去訪問存儲(chǔ)系統(tǒng),從而對(duì)存儲(chǔ)系統(tǒng)造成巨大的性能壓力。這些壓力又會(huì)拖慢整個(gè)系統(tǒng),嚴(yán)重的會(huì)造成數(shù)據(jù)庫宕機(jī),從而形成一系列連鎖反應(yīng),造成整個(gè)系統(tǒng)崩潰。
緩存雪崩的常見解決方法有兩種:更新鎖機(jī)制和后臺(tái)更新機(jī)制。
1. 更新鎖
對(duì)緩存更新操作進(jìn)行加鎖保護(hù),保證只有一個(gè)線程能夠進(jìn)行緩存更新,未能獲取更新鎖的線程要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認(rèn)值。
對(duì)于采用分布式集群的業(yè)務(wù)系統(tǒng),由于存在幾十上百臺(tái)服務(wù)器,即使單臺(tái)服務(wù)器只有一個(gè)線程更新緩存,但幾十上百臺(tái)服務(wù)器一起算下來也會(huì)有幾十上百個(gè)線程同時(shí)來更新緩存,同樣存在雪崩的問題。因此分布式集群的業(yè)務(wù)系統(tǒng)要實(shí)現(xiàn)更新鎖機(jī)制,需要用到分布式鎖,如 ZooKeeper。
2. 后臺(tái)更新
由后臺(tái)線程來更新緩存,而不是由業(yè)務(wù)線程來更新緩存,緩存本身的有效期設(shè)置為永久,后臺(tái)線程定時(shí)更新緩存。
后臺(tái)定時(shí)機(jī)制需要考慮一種特殊的場景,當(dāng)緩存系統(tǒng)內(nèi)存不夠時(shí),會(huì)“踢掉”一些緩存數(shù)據(jù),從緩存被“踢掉”到下一次定時(shí)更新緩存的這段時(shí)間內(nèi),業(yè)務(wù)線程讀取緩存返回空值,而業(yè)務(wù)線程本身又不會(huì)去更新緩存,因此業(yè)務(wù)上看到的現(xiàn)象就是數(shù)據(jù)丟了。解決的方式有兩種:
后臺(tái)線程除了定時(shí)更新緩存,還要頻繁地去讀取緩存(例如,1 秒或者 100 毫秒讀取一次),如果發(fā)現(xiàn)緩存被“踢了”就立刻更新緩存,這種方式實(shí)現(xiàn)簡單,但讀取時(shí)間間隔不能設(shè)置太長,因?yàn)槿绻彺姹惶吡?,緩存讀取間隔時(shí)間又太長,這段時(shí)間內(nèi)業(yè)務(wù)訪問都拿不到真正的數(shù)據(jù)而是一個(gè)空的緩存值,用戶體驗(yàn)一般。
業(yè)務(wù)線程發(fā)現(xiàn)緩存失效后,通過消息隊(duì)列發(fā)送一條消息通知后臺(tái)線程更新緩存??赡軙?huì)出現(xiàn)多個(gè)業(yè)務(wù)線程都發(fā)送了緩存更新消息,但其實(shí)對(duì)后臺(tái)線程沒有影響,后臺(tái)線程收到消息后更新緩存前可以判斷緩存是否存在,存在就不執(zhí)行更新操作。這種方式實(shí)現(xiàn)依賴消息隊(duì)列,復(fù)雜度會(huì)高一些,但緩存更新更及時(shí),用戶體驗(yàn)更好。
后臺(tái)更新既適應(yīng)單機(jī)多線程的場景,也適合分布式集群的場景,相比更新鎖機(jī)制要簡單一些。
后臺(tái)更新機(jī)制還適合業(yè)務(wù)剛上線的時(shí)候進(jìn)行緩存預(yù)熱。緩存預(yù)熱指系統(tǒng)上線后,將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng),而不是等待用戶訪問才來觸發(fā)緩存加載。
緩存熱點(diǎn)
雖然緩存系統(tǒng)本身的性能比較高,但對(duì)于一些特別熱點(diǎn)的數(shù)據(jù),如果大部分甚至所有的業(yè)務(wù)請(qǐng)求都命中同一份緩存數(shù)據(jù),則這份數(shù)據(jù)所在的緩存服務(wù)器的壓力也很大。例如,某明星微博發(fā)布“我們”來宣告戀愛了,短時(shí)間內(nèi)上千萬的用戶都會(huì)來圍觀。
緩存熱點(diǎn)的解決方案就是復(fù)制多份緩存副本,將請(qǐng)求分散到多個(gè)緩存服務(wù)器上,減輕緩存熱點(diǎn)導(dǎo)致的單臺(tái)緩存服務(wù)器壓力。以微博為例,對(duì)于粉絲數(shù)超過 100 萬的明星,每條微博都可以生成 100 份緩存,緩存的數(shù)據(jù)是一樣的,通過在緩存的 key 里面加上編號(hào)進(jìn)行區(qū)分,每次讀緩存時(shí)都隨機(jī)讀取其中某份緩存。
緩存副本設(shè)計(jì)有一個(gè)細(xì)節(jié)需要注意,就是不同的緩存副本不要設(shè)置統(tǒng)一的過期時(shí)間,否則就會(huì)出現(xiàn)所有緩存副本同時(shí)生成同時(shí)失效的情況,從而引發(fā)緩存雪崩效應(yīng)。正確的做法是設(shè)定一個(gè)過期時(shí)間范圍,不同的緩存副本的過期時(shí)間是指定范圍內(nèi)的隨機(jī)值。
實(shí)現(xiàn)方式
由于緩存的各種訪問策略和存儲(chǔ)的訪問策略是相關(guān)的,因此上面的各種緩存設(shè)計(jì)方案通常情況下都是集成在存儲(chǔ)訪問方案中,可以采用“程序代碼實(shí)現(xiàn)”的中間層方式,也可以采用獨(dú)立的中間件來實(shí)現(xiàn)。
小結(jié)
你所在的業(yè)務(wù)發(fā)生過哪些因?yàn)榫彺鎸?dǎo)致的線上問題?采取了什么樣的解決方案?效果如何?
評(píng)論1
緩存雪崩問題,我們采取了雙key策略:要緩存的key過期時(shí)間是t,key1沒有過期時(shí)間。每次緩存讀取不到key時(shí)就返回key1的內(nèi)容,然后觸發(fā)一個(gè)事件。這個(gè)事件會(huì)同時(shí)更新key和key1。
評(píng)論2
講一個(gè)頭兩天發(fā)生的事情,我們的一個(gè)業(yè)務(wù)背后是es做db,之前是通過redis做緩存,緩存一段時(shí)間后失效再從es讀取,是業(yè)務(wù)訪問加載緩存的方式。有一天線上es集群機(jī)器單臺(tái)出現(xiàn)問題,返回慢,由于分布式的緣故,漸漸拖滿了所有請(qǐng)求,緩存失效來查詢es發(fā)生了超時(shí),加載失敗,于是下次訪問還是直接訪問es。最終緩存全部失效,qps翻了好多倍,直接雪崩,es集群徹底沒有響應(yīng)了。。。之后我們只好先下線這個(gè)緩存加載功能,讓集群活過來,最終改造緩存加載方式,用后臺(tái)進(jìn)程去更新緩存,而不用業(yè)務(wù)訪問加載。
評(píng)論3?
線上遇到的一個(gè)錯(cuò)誤:業(yè)務(wù)查詢的結(jié)果序列化后放到redis,下次從redis取出來時(shí)報(bào)錯(cuò)。原來是結(jié)果類雖然實(shí)現(xiàn)了Serializable接口,但是沒有重寫serialVersionUID,導(dǎo)致不能成功反序列化。
評(píng)論4?
熱點(diǎn)數(shù)據(jù)存在相當(dāng)?shù)耐话l(fā)性,臨時(shí)的擴(kuò)容似乎也來不及,能否從緩存架構(gòu)角度如何避免類似微博宕機(jī)的事件?
1. 限流
2. 容器化+動(dòng)態(tài)化?
3. 業(yè)務(wù)降級(jí),例如限制評(píng)論
評(píng)論5
數(shù)據(jù)庫與緩存不一致:
1. 同步刷新緩存:當(dāng)更新了某些信息后,立刻讓緩存失效。
這種做法的優(yōu)點(diǎn)是用戶體驗(yàn)好,缺點(diǎn)是修改一個(gè)數(shù)據(jù)可能需要讓很多緩存失效
2. 適當(dāng)容忍不一致:例如某東的商品就是這樣,我查詢的時(shí)候顯示有貨,下單的時(shí)候提示我沒貨了
3. 關(guān)鍵信息不緩存:庫存,價(jià)格等不緩存,因?yàn)檫@類信息查詢簡單,效率高,關(guān)系數(shù)據(jù)庫查詢性能也很高
評(píng)論6?
好的緩存方案應(yīng)該從這幾個(gè)方面入手設(shè)計(jì):
1.什么數(shù)據(jù)應(yīng)該緩存
2.什么時(shí)機(jī)觸發(fā)緩存和以及觸發(fā)方式是什么
3.緩存的層次和粒度( 網(wǎng)關(guān)緩存如 nginx,本地緩存如單機(jī)文件,分布式緩存如redis cluster,進(jìn)程內(nèi)緩存如全局變量)
4.緩存的命名規(guī)則和失效規(guī)則
5.緩存的監(jiān)控指標(biāo)和故障應(yīng)對(duì)方案
6.可視化緩存數(shù)據(jù)如 redis 具體 key 內(nèi)容和大小
評(píng)論7
為什么不用Mysql緩存?
1. mysql第一種緩存叫sql語句結(jié)果緩存,但條件比較苛刻,程序員不可控,我們的dba線上都關(guān)閉這個(gè)功能,具體實(shí)現(xiàn)可以查一下
2. mysql第二種緩存是innodb buffer pool,緩存的是磁盤上的分頁數(shù)據(jù),不是sql的查詢結(jié)果,sql的執(zhí)行過程省不了。而mc,redis這些實(shí)際上都是緩存sql的結(jié)果,兩種緩存方式,性能差很遠(yuǎn)。
因此,可控性,性能是數(shù)據(jù)庫緩存和獨(dú)立緩存的主要區(qū)別
評(píng)論8
1個(gè)問題:關(guān)于后臺(tái)更新,既然緩存服務(wù)器內(nèi)存不足,需要剔除數(shù)據(jù),那么后臺(tái)更新再次觸發(fā)查詢,是否又會(huì)導(dǎo)致其他一些緩存數(shù)據(jù)被剔除,這感覺像是陷入一個(gè)循環(huán)了。所以加內(nèi)存才是根本解決方式
1個(gè)思路:我們之前的web項(xiàng)目,對(duì)于緩存熱點(diǎn)數(shù)據(jù),為了減少服務(wù)器的壓力,在客戶端引入了緩存:CDN加local storage,感覺對(duì)服務(wù)器端壓力分散還是很有效果的。分級(jí)緩存策略