緩存的收益與成本
①收益
- 加速讀寫:因為緩存通常都是全內(nèi)存的(例如Redis、Memcache)勉抓,而存儲層通常讀寫性能不夠強悍(例如MySQL)贾漏,通過緩存的使用可以有效地加速讀寫,優(yōu)化用戶體驗藕筋。
- 降低后端負載:幫助后端減少訪問量和復雜計算(例如很復雜的SQL語句)纵散,在很大程度降低了后端的負載。
②成本 - 數(shù)據(jù)不一致性:緩存層和存儲層的數(shù)據(jù)存在著一定時間窗口的不一致性隐圾,時間窗口跟更新策略有關(guān)伍掀。
- 代碼維護成本:加入緩存后,需要同時處理緩存層和存儲層的邏輯暇藏,增大了開發(fā)者維護代碼的成本蜜笤。
- 運維成本:以Redis Cluster為例,加入后無形中增加了運維成本盐碱。
使用場景 - 開銷大的復雜計算:以MySQL為例子把兔,一些復雜的操作或者計算(例如大量聯(lián)表操作沪伙、一些分組計算),如果不加緩存县好,不但無法滿足高并發(fā)量围橡,同時也會給MySQL帶來巨大的負擔。
- 加速請求響應:即使查詢單條后端數(shù)據(jù)足夠快缕贡,那么依然可以使用緩存翁授,以Redis為例子,每秒可以完成數(shù)萬次讀寫晾咪,并且提供的批量操作可以優(yōu)化整個IO鏈的響應時間
緩存更新策略
內(nèi)存溢出淘汰策略
當Redis所用內(nèi)存達到maxmemory上限(used_memory>maxmemory)時會觸發(fā)相應的溢出控制策略收擦。具體策略受maxmemory-policy參數(shù)控制。
Redis支持6種策略:
1)noeviction:默認策略禀酱,不會刪除任何數(shù)據(jù)炬守,拒絕所有寫入操作并返回客戶端錯誤信息(error)OOM command not allowed when used memory,此時Redis只響應讀操作剂跟。
2)volatile-lru:根據(jù)LRU算法刪除設置了超時屬性(expire)的鍵减途,直到騰出足夠空間為止。如果沒有可刪除的鍵對象曹洽,回退到noeviction策略鳍置。
3)volatile-random:隨機刪除過期鍵,直到騰出足夠空間為止送淆。
4)allkeys-lru:根據(jù)LRU算法刪除鍵税产,不管數(shù)據(jù)有沒有設置超時屬性,直到騰出足夠空間為止偷崩。
5)allkeys-random:隨機刪除所有鍵辟拷,直到騰出足夠空間為止。
6)volatile-ttl:根據(jù)鍵值對象的ttl屬性阐斜,刪除最近將要過期數(shù)據(jù)衫冻。如果沒有,回退到noeviction策略
內(nèi)存溢出控制策略可以采用config set maxmemory-policy{policy}動態(tài)配置谒出。
寫命令導致當內(nèi)存溢出時會頻繁執(zhí)行回收內(nèi)存成本很高隅俘,如果Redis有從節(jié)點,回收內(nèi)存操作對應的刪除命令會同步到從節(jié)點笤喳,導致寫放大的問題为居。
過期刪除
- 惰性刪除
Redis的每個庫都有一個過期字典,過期字典中保存所有key的過期時間杀狡。當客戶端讀取一個key時會先到過期字典內(nèi)查詢key是否已經(jīng)過期蒙畴,如果已經(jīng)超過鍵,會執(zhí)行刪除操作并返回空呜象。忍抽,這種策略是出于節(jié)省CPU成本考慮八孝,但是單獨用這種方式存在內(nèi)存泄露的問題,當過期鍵一直沒有訪問將無法得到及時刪除鸠项,從而導致內(nèi)存不能及時釋放干跛。 -
定時刪除
Redis內(nèi)部維護一個定時任務,默認每秒運行10次祟绊。通過hz修改運行次數(shù)楼入。定時任務中刪除過期鍵邏輯采用了自適應算法,根據(jù)鍵的過期比例牧抽、使用快慢兩種速率模式回收鍵嘉熊。ServerCron
慢模式:定時任務執(zhí)行時間超過25毫秒自動退出
快模式:上次執(zhí)行時間超過25毫秒,則采用快模式扬舒,快模式下超時時間為1毫秒且2秒內(nèi)只能運行1次阐肤。
應用方更新
a、應用程序先從cache取數(shù)據(jù)讲坎,沒有得到孕惜,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后晨炕,放到緩存中衫画。
b、先刪除緩存瓮栗,再更新數(shù)據(jù)庫
這個操作有一個比較大的問題削罩,更新數(shù)據(jù)的請求在對緩存刪除完之后,又收到一個讀請求费奸,這個時候由于緩存被刪除所以直接會讀庫弥激,讀操作的數(shù)據(jù)是老的并且會被加載進入緩存當中,后續(xù)讀請求全部訪問的老數(shù)據(jù)愿阐。
c微服、先更新數(shù)據(jù)庫,再刪除緩存(推薦)
為什么不是寫完數(shù)據(jù)庫后更新緩存换况?主要是怕兩個并發(fā)的寫操作導致臟數(shù)據(jù)。
緩存粒度
通用性
緩存全部數(shù)據(jù)比部分數(shù)據(jù)更加通用盗蟆,但從實際經(jīng)驗看戈二,很長時間內(nèi)應用只需要幾個重要的屬性。
占用空間
緩存全部數(shù)據(jù)要比部分數(shù)據(jù)占用更多的空間喳资,存在以下問題:
- 全部數(shù)據(jù)會造成內(nèi)存的浪費觉吭。
- 全部數(shù)據(jù)可能每次傳輸產(chǎn)生的網(wǎng)絡流量會比較大,耗時相對較大仆邓,在極端情況下會阻塞網(wǎng)絡鲜滩。
- 全部數(shù)據(jù)的序列化和反序列化的CPU開銷更大伴鳖。
代碼維護
全部數(shù)據(jù)的優(yōu)勢更加明顯,而部分數(shù)據(jù)一旦要加新字段需要修改業(yè)務代碼徙硅,而且修改后通常還需要刷新緩存數(shù)據(jù)榜聂。
緩存穿透
緩存穿透是指查詢一個根本不存在的數(shù)據(jù),緩存層和持久層都不會命中嗓蘑,通常出于容錯的考慮须肆,如果從持久層查不到數(shù)據(jù)則不寫入緩存層。
緩存穿透將導致不存在的數(shù)據(jù)每次請求都要到持久層去查詢桩皿,失去了緩存保護后端持久的意義豌汇。
緩存穿透問題可能會使后端存儲負載加大,由于很多后端持久層不具備高并發(fā)性泄隔,甚至可能造成后端存儲宕掉拒贱。通常可以在程序中統(tǒng)計總調(diào)用數(shù)佛嬉、緩存層命中數(shù)逻澳、如果同一個Key的緩存命中率很低,可能就是出現(xiàn)了緩存穿透問題巷燥。
造成緩存穿透的基本原因有兩個赡盘。第一,自身業(yè)務代碼或者數(shù)據(jù)出現(xiàn)問題缰揪,第二陨享,一些惡意攻擊、爬蟲等造成大量空命中钝腺。
①緩存空對象
緩存空對象會有兩個問題:第一抛姑,空值做了緩存,意味著緩存層中存了更多的鍵艳狐,需要更多的內(nèi)存空間定硝,比較有效的方法是針對這類數(shù)據(jù)設置一個較短的過期時間,讓其自動剔除毫目。第二蔬啡,緩存層和存儲層的數(shù)據(jù)會有一段時間窗口的不一致,可能會對業(yè)務有一定影響镀虐。例如過期時間設置為5分鐘箱蟆,如果此時存儲層添加了這個數(shù)據(jù),那此段時間就會出現(xiàn)緩存層和存儲層數(shù)據(jù)的不一致刮便,此時可以利用消息系統(tǒng)或者其他方式清除掉緩存層中的空對象空猜。
②布隆過濾器攔截
在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截辈毯,當收到一個對key請求時先用布隆過濾器驗證是key否存在坝疼,如果存在在進入緩存層、存儲層谆沃《坌祝可以使用bitmap做布隆過濾器。這種方法適用于數(shù)據(jù)命中不高管毙、數(shù)據(jù)相對固定腿椎、實時性低的應用場景,代碼維護較為復雜夭咬,但是緩存空間占用少啃炸。
布隆過濾器實際上是一個很長的二進制向量和一系列隨機映射函數(shù)。布隆過濾器可以用于檢索一個元素是否在一個集合中卓舵。它的優(yōu)點是空間效率和查詢時間都遠遠超過一般的算法南用,缺點是有一定的誤識別率和刪除困難。
算法描述:
- 初始狀態(tài)時掏湾,BloomFilter是一個長度為m的位數(shù)組裹虫,每一位都置為0。
- 添加元素x時融击,x使用k個hash函數(shù)得到k個hash值筑公,對m取余,對應的bit位設置為1尊浪。
- 判斷y是否屬于這個集合匣屡,對y使用k個哈希函數(shù)得到k個哈希值,對m取余拇涤,所有對應的位置都是1捣作,則認為y屬于該集合(哈希沖突,可能存在誤判)鹅士,否則就認為y不屬于該集合券躁。可以通過增加哈希函數(shù)和增加二進制位數(shù)組的長度來降低誤報率掉盅。
方案對比:
緩存雪崩
由于緩存層承載著大量請求也拜,有效地保護了存儲層,但是如果緩存層由于某些原因
不可用或者大量緩存由于超時時間相同在同一時間段失效趾痘,大量請求直接到達存儲層慢哈,存儲層壓力過大導致系統(tǒng)雪崩。
解決方案:
- 可以把緩存層設計成高可用的扼脐,即使個別節(jié)點岸军、個別機器、甚至是機房宕掉瓦侮,依然可以提供服務艰赞。利用sentinel或cluster實現(xiàn)。
- 采用多級緩存肚吏,本地進程作為一級緩存方妖,redis作為二級緩存,不同級別的緩存設置的超時時間不同罚攀,即使某級緩存過期了党觅,也有其他級別緩存兜底。
- 緩存的過期時間用隨機值斋泄,盡量讓不同的key的過期時間不同杯瞻。
緩存擊穿
系統(tǒng)中存在以下兩個問題時需要引起注意:
- 當前key是一個熱點key(例如一個秒殺活動),并發(fā)量非常大炫掐。
重建緩存不能在短時間完成魁莉,可能是一個復雜計算,例如復雜的SQL募胃、多次IO旗唁、多個依賴等。 - 在緩存失效的瞬間痹束,有大量線程來重建緩存检疫,造成后端負載加大,甚至可能會讓應用崩潰祷嘶。
解決方案:
①分布式互斥鎖
只允許一個線程重建緩存屎媳,其他線程等待重建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)即可抹蚀。set(key,value,timeout)
②永不過期 - 從緩存層面來看剿牺,確實沒有設置過期時間,所以不會出現(xiàn)熱點key過期后產(chǎn)生的問題环壤,也就是“物理”不過期晒来。
- 從功能層面來看,為每個value設置一個邏輯過期時間郑现,當發(fā)現(xiàn)超過邏輯過期時間后湃崩,會使用單獨的線程去更新緩存。
2種方案對比: - 分布式互斥鎖:這種方案思路比較簡單接箫,但是存在一定的隱患攒读,如果構(gòu)建緩存過程出現(xiàn)問題或者時間較長,可能會存在死鎖和線程池阻塞的風險辛友,但是這種方法能夠較好地降低后端存儲負載薄扁,并在一致性上做得比較好剪返。
- “永遠不過期”:這種方案由于沒有設置真正的過期時間,實際上已經(jīng)不存在熱點key產(chǎn)生的一系列危害邓梅,但是會存在數(shù)據(jù)不一致的情況脱盲,同時代碼復雜度會增大。