程序員修神之路--緩存架構(gòu)不夠好,系統(tǒng)容易癱瘓
- 緩存能大幅度提高系統(tǒng)性能,也能大幅度提高系統(tǒng)癱瘓幾率
 - 怎么樣防止緩存系統(tǒng)被穿透?
 - 緩存的雪崩是不是可以完全避免?
 
前幾篇文章我們介紹了緩存的優(yōu)勢(shì)以及數(shù)據(jù)一致性的問(wèn)題,在一個(gè)面臨高并發(fā)系統(tǒng)中,緩存幾乎成了每個(gè)架構(gòu)師應(yīng)對(duì)高流量的首沖解決方案,但是,一個(gè)好的緩存系統(tǒng),除了和數(shù)據(jù)庫(kù)一致性問(wèn)題之外,還存在著其他問(wèn)題,給整體的系統(tǒng)設(shè)計(jì)引入了額外的復(fù)雜性。而這些復(fù)雜性問(wèn)題的解決方案也直接了影響系統(tǒng)的穩(wěn)定性,最常見(jiàn)的比如緩存的命中率問(wèn)題,在一個(gè)高并發(fā)系統(tǒng)中,核心功能的緩存命中率一般要保持在90%以上甚至更高,如果低于這個(gè)命中率,整個(gè)系統(tǒng)可能就面臨著隨時(shí)被峰值流量擊垮的可能,這個(gè)時(shí)候我們就需要優(yōu)化緩存的使用方式了。
如果按照傳統(tǒng)的緩存和DB的流程,一個(gè)請(qǐng)求到來(lái)的時(shí)候,首先會(huì)查詢(xún)緩存中是否存在,如果緩存中不存在則去查詢(xún)對(duì)應(yīng)的數(shù)據(jù)庫(kù)。假如系統(tǒng)每秒的請(qǐng)求量為10000,而緩存的命中率為60%,則每秒穿透到數(shù)據(jù)庫(kù)的請(qǐng)求數(shù)為4000,對(duì)于關(guān)系型數(shù)據(jù)庫(kù)mysql來(lái)說(shuō),每秒4000的請(qǐng)求量對(duì)于分了一主三從的Mysql數(shù)據(jù)庫(kù)架構(gòu)來(lái)說(shuō)也已經(jīng)足夠大了,再加上主從的同步延遲等諸多因素,這個(gè)時(shí)候你的mysql已經(jīng)行走在down機(jī)邊緣了。
“緩存的最終目的,是在保證請(qǐng)求低延遲的情況下,盡最大努力提高系統(tǒng)的吞吐量
那緩存系統(tǒng)可能會(huì)影響系統(tǒng)崩潰的原因有那些呢?
緩存穿透
“緩存穿透是指:當(dāng)一個(gè)請(qǐng)求到來(lái)的時(shí)候,在緩存中沒(méi)有查找到對(duì)應(yīng)的數(shù)據(jù)(緩存未命中),業(yè)務(wù)系統(tǒng)不得不從數(shù)據(jù)庫(kù)(這里其實(shí)可以籠統(tǒng)的成為后端系統(tǒng))中加載數(shù)據(jù)
緩存穿透
發(fā)生緩存穿透的原因根據(jù)場(chǎng)景分為兩種:
請(qǐng)求的數(shù)據(jù)在緩存和數(shù)據(jù)中都不存在
當(dāng)數(shù)據(jù)在緩存和數(shù)據(jù)庫(kù)都不存在的時(shí)候,如果按照一般的緩存設(shè)計(jì),每次請(qǐng)求都會(huì)到數(shù)據(jù)庫(kù)查詢(xún)一次,然后返回不存在,這種場(chǎng)景下,緩存系統(tǒng)幾乎沒(méi)有起任何作用。在正常的業(yè)務(wù)系統(tǒng)中,發(fā)生這種情況的概率比較小,就算偶爾發(fā)生,也不會(huì)對(duì)數(shù)據(jù)庫(kù)造成根本上的壓力。
最可怕的是出現(xiàn)一些異常情況,比如系統(tǒng)中有死循環(huán)的查詢(xún)或者被黑客攻擊的時(shí)候,尤其是后者,他會(huì)故意偽造大量的請(qǐng)求來(lái)讀取不存在的數(shù)據(jù)而造成數(shù)據(jù)庫(kù)的down機(jī),最典型的場(chǎng)景為:如果系統(tǒng)的用戶(hù)id是連續(xù)遞增的int型,黑客很容易偽造用戶(hù)id來(lái)模擬大量的請(qǐng)求。
請(qǐng)求的數(shù)據(jù)在緩存中不存在,在數(shù)據(jù)庫(kù)中存在
這種場(chǎng)景一般屬于業(yè)務(wù)的正常需求,因?yàn)榫彺嫦到y(tǒng)的容量一般是有限制的,比如我們最常用的Redis做為緩存,就受到服務(wù)器內(nèi)存大小的限制,所以所有的業(yè)務(wù)數(shù)據(jù)不可能都放入緩存系統(tǒng)中,根據(jù)互聯(lián)網(wǎng)數(shù)據(jù)的二八規(guī)則,我們可以?xún)?yōu)先把訪(fǎng)問(wèn)最頻繁的熱點(diǎn)數(shù)據(jù)放入緩存系統(tǒng),這樣就能利用緩存的優(yōu)勢(shì)來(lái)抗住主要的流量來(lái)源,而剩余的非熱點(diǎn)數(shù)據(jù),就算是有穿透數(shù)據(jù)庫(kù)的可能性,也不會(huì)對(duì)數(shù)據(jù)庫(kù)造成致命壓力。
換句話(huà)說(shuō),每個(gè)系統(tǒng)發(fā)生緩存穿透是不可避免的,而我們需要做的是盡量避免大量的請(qǐng)求發(fā)生穿透,那怎么解決緩存穿透問(wèn)題呢?解決緩存的穿透問(wèn)題本質(zhì)上是要解決怎么樣攔截請(qǐng)求的問(wèn)題,一般情況下會(huì)有以下幾種方案:
回寫(xiě)空值
當(dāng)請(qǐng)求的數(shù)據(jù)在數(shù)據(jù)庫(kù)中不存在的時(shí)候,緩存系統(tǒng)可以把對(duì)應(yīng)的key寫(xiě)入一個(gè)空值,這樣當(dāng)下次同樣的請(qǐng)求就不會(huì)直接穿透數(shù)據(jù)庫(kù),而直接返回緩存中的空值了。這種方案是最簡(jiǎn)單粗暴的,但是要注意幾點(diǎn):
- 當(dāng)有大量的空值被寫(xiě)入緩存系統(tǒng)中,同樣會(huì)占用內(nèi)存,不過(guò)理論上不會(huì)太多,完全取決于key的數(shù)量。而且根據(jù)緩存淘汰策略,可能會(huì)淘汰正常的數(shù)據(jù)緩存項(xiàng)
 - 空值的過(guò)期時(shí)間應(yīng)該短一些,比如正常的數(shù)據(jù)緩存過(guò)期時(shí)間可能為2小時(shí),可以考慮空值的過(guò)期時(shí)間為10分鐘,這樣做一是為了盡快釋放服務(wù)器的內(nèi)存空間,二是如果業(yè)務(wù)產(chǎn)生相應(yīng)的真實(shí)數(shù)據(jù),可以讓緩存的空值快速失效,盡快做到緩存和數(shù)據(jù)庫(kù)一致。
 
- //獲取用戶(hù)信息
 - public static UserInfo GetUserInfo(int userId)
 - {
 - //從緩存讀取用戶(hù)信息
 - var userInfo = GetUserInfoFromCache(userId);
 - if (userInfo == null)
 - {
 - //回寫(xiě)空值到緩存,并設(shè)置緩存過(guò)期時(shí)間為10分鐘
 - CacheSystem.Set(userId, null,10);
 - }
 - return userInfo;
 - }
 
布隆過(guò)濾器
“布隆過(guò)濾器:將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的 bitmap 中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被這個(gè)bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢(xún)壓力
布隆過(guò)濾器有幾個(gè)很大的優(yōu)勢(shì)
- 占用內(nèi)存非常小
 - 對(duì)于判斷一個(gè)數(shù)據(jù)不存在百分百正確
 
由于布隆過(guò)濾器基于hash算法,所以在時(shí)間復(fù)雜度上是O(1),在應(yīng)對(duì)高并發(fā)的場(chǎng)景下非常合適,不過(guò)使用布隆過(guò)濾器要求系統(tǒng)在產(chǎn)生數(shù)據(jù)的時(shí)候需要在布隆過(guò)濾器同時(shí)也寫(xiě)入數(shù)據(jù),而且布隆過(guò)濾器也不支持刪除數(shù)據(jù),因?yàn)槎鄠€(gè)數(shù)據(jù)可能會(huì)重用同一個(gè)位置。
image
緩存雪崩
“緩存雪崩是指緩存中數(shù)據(jù)大批量同時(shí)過(guò)期,造成查詢(xún)數(shù)據(jù)庫(kù)數(shù)據(jù)量巨大,引起數(shù)據(jù)庫(kù)壓力過(guò)大導(dǎo)致系統(tǒng)崩潰。
與緩存穿透現(xiàn)象不同,緩存穿透是指緩存中不存在數(shù)據(jù)而造成會(huì)對(duì)數(shù)據(jù)庫(kù)造成大量查詢(xún),而緩存雪崩是因?yàn)榫彺嬷写嬖跀?shù)據(jù),但是同時(shí)大量過(guò)期造成。但是本質(zhì)上是一樣的,都是對(duì)數(shù)據(jù)庫(kù)造成了大量的請(qǐng)求。
無(wú)論是穿透還是雪崩都面臨著同樣的數(shù)據(jù)會(huì)有多個(gè)線(xiàn)程同時(shí)請(qǐng)求,同時(shí)查詢(xún)數(shù)據(jù)庫(kù),同時(shí)回寫(xiě)緩存的一致性問(wèn)題。舉例來(lái)說(shuō),當(dāng)多個(gè)線(xiàn)程同時(shí)請(qǐng)求用戶(hù)id為1的用戶(hù),這個(gè)時(shí)候緩存正好失效,那這多個(gè)線(xiàn)程同時(shí)會(huì)查詢(xún)數(shù)據(jù)庫(kù),然后同時(shí)會(huì)回寫(xiě)緩存,最可怕的是,這個(gè)回寫(xiě)的過(guò)程中,另外一個(gè)線(xiàn)程更新了數(shù)據(jù)庫(kù),就造成了數(shù)據(jù)不一致,這個(gè)問(wèn)題在之前的文章中著重講過(guò),大家一定要注意。
同樣的數(shù)據(jù)會(huì)被多個(gè)線(xiàn)程產(chǎn)生多個(gè)請(qǐng)求是產(chǎn)生雪崩的一個(gè)原因,針對(duì)這種情況的解決方案是把多個(gè)線(xiàn)程的請(qǐng)求順序化,使其只有一個(gè)線(xiàn)程會(huì)產(chǎn)生對(duì)數(shù)據(jù)庫(kù)的查詢(xún)操作,比如最常見(jiàn)的鎖機(jī)制(分布式鎖機(jī)制),現(xiàn)在最常見(jiàn)的分布式鎖是用redis來(lái)實(shí)現(xiàn),但是redis實(shí)現(xiàn)分布式鎖也有一定的坑。
多個(gè)緩存key同時(shí)失效的場(chǎng)景是產(chǎn)生雪崩的主要原因,針對(duì)這樣的場(chǎng)景一般可以利用以下幾種方案來(lái)解決
設(shè)置不同過(guò)期時(shí)間
給緩存的每個(gè)key設(shè)置不同的過(guò)期時(shí)間是最簡(jiǎn)單的防止緩存雪崩的手段,整體思路是給每個(gè)緩存的key在系統(tǒng)設(shè)置的過(guò)期時(shí)間之上加一個(gè)隨機(jī)值,或者干脆是直接隨機(jī)一個(gè)值,有效的平衡key批量過(guò)期時(shí)間段,消掉單位之間內(nèi)過(guò)期key數(shù)量的峰值。
- public static int SetUserInfo(int userId)
 - {
 - //讀取用戶(hù)信息
 - var userInfo = GetUserInfoFromDB(userId);
 - if (userInfo != null)
 - {
 - //回寫(xiě)到緩存,并設(shè)置緩存過(guò)期時(shí)間為隨機(jī)時(shí)間
 - var cacheExpire = new Random().Next(1, 100);
 - CacheSystem.Set(userId, userInfo, cacheExpire);
 - return cacheExpire;
 - }
 - return 0;
 - }
 
后臺(tái)單獨(dú)線(xiàn)程更新
這種場(chǎng)景下,可以把緩存設(shè)置為永不過(guò)期,緩存的更新不是由業(yè)務(wù)線(xiàn)程來(lái)更新,而是由專(zhuān)門(mén)的線(xiàn)程去負(fù)責(zé)。當(dāng)緩存的key有更新時(shí)候,業(yè)務(wù)方向mq發(fā)送一個(gè)消息,更新緩存的線(xiàn)程會(huì)監(jiān)聽(tīng)這個(gè)mq來(lái)實(shí)時(shí)響應(yīng)以便更新緩存中對(duì)應(yīng)的數(shù)據(jù)。不過(guò)這種方式要考慮到緩存淘汰的場(chǎng)景,當(dāng)一個(gè)緩存的key被淘汰之后,其實(shí)也可以向mq發(fā)送一個(gè)消息,以達(dá)到更新線(xiàn)程重新回寫(xiě)key的操作。
緩存的可用性和擴(kuò)展性
和數(shù)據(jù)庫(kù)一樣,緩存系統(tǒng)的設(shè)計(jì)同樣需要考慮高可用和擴(kuò)展性。雖然緩存系統(tǒng)本身的性能已經(jīng)比較高了,但是對(duì)于一些特殊的高并發(fā)的熱點(diǎn)數(shù)據(jù),還是會(huì)遇到單機(jī)的瓶頸。舉個(gè)栗子:假如某個(gè)明星出軌了,這個(gè)信息數(shù)據(jù)會(huì)緩存在某個(gè)緩存服務(wù)器的節(jié)點(diǎn)上,大量的請(qǐng)求會(huì)到達(dá)這個(gè)服務(wù)器節(jié)點(diǎn),當(dāng)?shù)竭_(dá)一定程度的時(shí)候同樣會(huì)發(fā)生down機(jī)的情況。類(lèi)似于數(shù)據(jù)庫(kù)的主從架構(gòu),緩存系統(tǒng)也可以復(fù)制多分緩存副本到其他服務(wù)器上,這樣就可以將應(yīng)用的請(qǐng)求分散到多個(gè)緩存服務(wù)器上,緩解由于熱點(diǎn)數(shù)據(jù)出現(xiàn)的單點(diǎn)問(wèn)題。
和數(shù)據(jù)庫(kù)主從一樣,緩存的多個(gè)副本也面臨著數(shù)據(jù)的一致性問(wèn)題,同步延遲問(wèn)題,還有主從服務(wù)器相同key的過(guò)期時(shí)間問(wèn)題。
至于緩存系統(tǒng)的擴(kuò)展性同樣的道理,也可以利用“分片”的原則,利用一致性哈希算法將不同的請(qǐng)求路由到不同的緩存服務(wù)器節(jié)點(diǎn),來(lái)達(dá)到水平擴(kuò)展的要求,這一點(diǎn)和應(yīng)用的水平擴(kuò)展道理一樣。
寫(xiě)在最后
通過(guò)以上可以看出,無(wú)論是應(yīng)用服務(wù)器的高可用架構(gòu)還是數(shù)據(jù)庫(kù)的高可用架構(gòu),還是緩存的高可用其實(shí)道理都是類(lèi)似的,當(dāng)我們掌握了其中一種就很容易的擴(kuò)展到任何場(chǎng)景中。如果這篇文章對(duì)你有多幫助,請(qǐng)分享給身邊的朋友,最后歡迎大家留言寫(xiě)下你們?cè)谌粘i_(kāi)發(fā)中用到的其他關(guān)于緩存高可用,可擴(kuò)展性,以及防止穿透和雪崩的方案,讓我們一起進(jìn)步!!
本文轉(zhuǎn)載自微信公眾號(hào)「架構(gòu)師修行之路」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系架構(gòu)師修行之路公眾號(hào)。


















 
 
 




 
 
 
 