1、前言
隨著互聯網從簡單的單向瀏覽請求,發(fā)展為基于用戶個性信息的定制化以及社交化的請求,這要求產品需要做到以用戶和關系為基礎,對海量數據進行分析和計算。對于后端服務來說,意味著用戶的每次請求都需要查詢用戶的個人信息和大量的關系信息,此外大部分場景還需要對上述信息進行聚合、過濾、排序,最終才能返回給用戶。
CPU 是信息處理、程序運行的最終執(zhí)行單元,如果它的世界也有 “秒” 的概念,假設它的時鐘跳一下為一秒,那么在 CPU(CPU 的一個核心)眼中的時間概念是什么樣的呢?
可見 I/O 的速度與 CPU 和內存相比是要差幾個數量級的,如果數據全部從數據庫獲取,一次請求涉及多次數據庫操作會大大增加響應時間,無法提供好的用戶體驗。
對于大型高并發(fā)場景下的 Web 應用,緩存更為重要,更高的緩存命中率就意味著更好的性能。緩存系統(tǒng)的引入,是提升系統(tǒng)響應時延、提升用戶體驗的唯一途徑,良好的緩存架構設計也是高并發(fā)系統(tǒng)的基石。
緩存的思想基于以下幾點:
-
時間局限性原理 程序有在一段時間內多次訪問同一個數據塊的傾向。例如一個熱門的商品或者一個熱門的新聞會被數以百萬甚至千萬的更多用戶查看。通過緩存,可以高效地重用之前檢索或計算的數據。
-
以空間換取時間 對于大部分系統(tǒng),全量數據通常存儲在 MySQL 或者 Hbase,但是它們的訪問效率太低。所以會開辟一個高速的訪問空間來加速訪問過程,例如 Redis 讀的速度是 110000 次 /s,寫的速度是 81000 次 /s 。
-
性能和成本的 Tradeoff 高速的訪問空間帶來的是成本的提升,在系統(tǒng)設計時要兼顧性能和成本。例如,在相同成本的情況下,SSD 硬盤容量會比內存大 10~30 倍以上,但讀寫延遲卻高 50~100 倍。
引入緩存會給系統(tǒng)帶來以下優(yōu)勢:
-
提升請求性能
-
降低網絡擁塞
-
減輕服務負載
-
增強可擴展性
同樣的,引入緩存也會帶來以下劣勢:
-
毫無疑問會增加系統(tǒng)的復雜性,開發(fā)復雜性和運維復雜性成倍提升。
-
高速的訪問空間會比數據庫存儲的成本高。
-
由于一份數據同時存在緩存和數據庫中,甚至緩存內部也會有多個數據副本,多份數據就會存在數據雙寫的不一致問題,同時緩存體系本身也會存在可用性問題和分區(qū)的問題。
在緩存系統(tǒng)的設計架構中,還有很多坑,很多的明槍暗箭,如果設計不當會導致很多嚴重的后果。設計不當,輕則請求變慢、性能降低,重則會數據不一致、系統(tǒng)可用性降低,甚至會導致緩存雪崩,整個系統(tǒng)無法對外提供服務。
2、緩存的主要存儲模式
三種模式各有優(yōu)劣,適用于不同的業(yè)務場景,不存在最佳模式。
● Cache Aside(旁路緩存)
寫:?更新 db 時,刪除緩存,當下次讀取數據庫時,驅動緩存的更新。
讀:?讀的時候先讀緩存,緩存未命中,那么就讀數據庫,并且將數據回種到緩存,同時返回相應結果
特點:懶加載思想,以數據庫中的數據為準。在稍微復雜點的緩存場景,緩存都不簡單是數據庫中直接取出來的,可能還需要從其他表查詢一些數據,然后進行一些復雜的運算,才能最終計算出值。這種存儲模式適合于對數據一致性要求比較高的業(yè)務,或者是緩存數據更新比較復雜、代價比較高的業(yè)務。例如:一個緩存涉及多個表的多個字段,在 1 分鐘內被修改了 100 次,但是這個緩存在 1 分鐘內就被讀取了 1 次。如果使用這種存儲模式只刪除緩存的話,那么 1 分鐘內,這個緩存不過就重新計算一次而已,開銷大幅度降低。
●?Read/Write Through(讀寫穿透)
寫:?緩存存在,更新數據庫,緩存不存在,同時更新緩存和數據庫
讀:?緩存未命中,由緩存服務加載數據并且寫入緩存
特點:
讀寫穿透對熱數據友好,特別適合有冷熱數據區(qū)分的場合。
1)簡化應用程序代碼
在緩存方法中,應用程序代碼仍然很復雜,并且直接依賴于數據庫,如果多個應用程序處理相同的數據,甚至會出現代碼重復。讀寫穿透模式將一些數據訪問代碼從應用程序轉移到緩存層,這極大地簡化了應用程序并更清晰地抽象了數據庫操作。
2)具有更好的讀取可伸縮性
在多數情況下,緩存數據過期以后,多個并行用戶線程最終會打到數據庫,再加上數以百萬計的緩存項和數千個并行用戶請求,數據庫上的負載會顯著增加。讀寫穿透可以保證應用程序永遠不會為這些緩存項訪問數據庫,這也可以讓數據庫負載保持在最小值。
3)具有更好的寫性能
讀寫穿透模式可以讓應用程序快速更新緩存并返回,之后它讓緩存服務在后臺更新數據庫。當數據庫寫操作的執(zhí)行速度不如緩存更新的速度快時,還可以指定限流機制,將數據庫寫操作安排在非高峰時間進行,減輕數據庫的壓力。
4)過期時自動刷新緩存
讀寫穿透模式允許緩存在過期時自動從數據庫重新加載對象。這意味著應用程序不必在高峰時間訪問數據庫,因為最新數據總是在緩存中。
●?Write Behind Caching(異步緩存寫入)
寫:只更新緩存,緩存服務異步更新數據庫。
讀:緩存未命中由封裝好的緩存服務加載數據并且寫入緩存。
特點:寫性能最高,定期異步刷新數據庫數據,數據丟失的概率大,適合寫頻率高,并且寫操作需要合并的場景。使用異步緩存寫入模式,數據的讀取和更新通過緩存進行,與讀寫穿透模式不同,更新的數據并不會立即傳到數據庫。相反,在緩存服務中一旦進行更新操作,緩存服務就會跟蹤臟記錄列表,并定期將當前的臟記錄集刷新到數據庫中。作為額外的性能改善,緩存服務會合并這些臟記錄,合并意味著如果相同的記錄被更新,或者在緩沖區(qū)內被多次標記為臟數據,則只保證最后一次更新。對于那些值更新非常頻繁,例如金融市場中的股票價格等場景,這種方式能夠很大程度上改善性能。如果股票價格每秒鐘變化 100 次,則意味著在 30 秒內會發(fā)生 30 x 100 次更新,合并將其減少至只有一次。
3、緩存 7 大經典問題
問題的常用解決方案
? 1??緩存集中失效
緩存集中失效大多數情況出現在高并發(fā)的時候,如果大量的緩存數據集中在一個時間段失效,查詢請求會打到數據庫,數據庫壓力凸顯。比如同一批火車票、飛機票,當可以售賣時,系統(tǒng)會一次性加載到緩存,并且過期時間設置為預先配置的固定時間,那過期時間到期后,系統(tǒng)就會因為熱點數據的集中沒有命中而出現性能變慢的情況。
解決方案:
-
使用基準時間 + 隨機時間,降低過期時間的重復率,避免集體失效。即相同業(yè)務數據設置緩存失效時間時,在原來設置的失效時間基礎上,再加上一個隨機值,讓數據分散過期,同時對數據庫的請求也會分散開,避免瞬時全部過期對數據庫造成過大壓力。
? 2??緩存穿透
緩存穿透是指一些異常訪問,每次都去查詢壓根兒就不存在的 key,導致每次請求都會打到數據庫上去。例如查詢不存在的用戶,查詢不存在的商品 id。如果是用戶偶爾錯誤輸入,問題不大。但如果是一些特殊用戶,控制一批肉雞,持續(xù)的訪問緩存不存在的 key,會嚴重影響系統(tǒng)的性能,影響正常用戶的訪問,甚至可能會讓數據庫直接宕機。我們在設計系統(tǒng)時,通常只考慮正常的訪問請求,所以這種情況往往容易被忽略。
解決方案:
-
第一種方案就是,查詢到不存在的數據時,首次查詢數據庫,即便數據庫沒有數據,仍然回種這個 key 到緩存,并使用一個特殊約定的 value 表示這個 key 的值為空。后面再次出現對這個 key 的請求時,直接返回 null。為了健壯性,設置空緩存 key 時,一定要設置過期時間,以防止之后該 key 被寫入了數據。
-
第二種方案是,構建一個 BloomFilter 緩存過濾器,記錄全量數據,這樣訪問數據時,可以直接通過 BloomFilter 判斷這個 key 是否存在,如果不存在直接返回即可,壓根兒不需要查詢緩存或數據庫。比如,可以使用基于數據庫增量日志解析框架(阿里的 canal),通過消費增量數據寫入到 BloomFilter 過濾器。BloomFilter 的所有操作也是在內存里實現,性能很高,要達到 1%?的誤判率,平均單條記錄占用 1.2 字節(jié)即可。同時需要注意的是 BloomFilter 只有新增沒有刪除操作,對于已經刪除的 key 可以配合上述緩存空值解決方案一起使用。Redis 提供了自定義參數的布隆顧慮器,可以使用 bf.reserve 進行創(chuàng)建,需要設置參數 error_rate(錯誤率)和 innitial_size。error_rate 越低需要的空間越大,innitial_size 表示預計放入的元素數量,當實際數量超過這個值以后,誤判率會上升。
? 3??緩存雪崩
緩存雪崩是緩存機器因為某種原因全部或者部分宕機,導致大量的數據落到數據庫,最終把數據庫打死。例如某個服務,恰好在請求高峰期間緩存服務宕機,本來打到緩存的請求,這是時候全部打到數據庫,數據庫扛不住在報警以后也會宕機,重啟數據庫以后,新的請求會再次把數據庫打死。
解決方案:
-
事前:緩存采用高可用架構設計,redis 使用集群部署方式。對重要業(yè)務數據的數據庫訪問添加開關,當發(fā)現數據庫出現阻塞、響應慢超過閾值的時候,關閉開關,將一部分或者全都的數據庫請求執(zhí)行 failfast 操作。
-
事中:引入多級緩存架構,增加緩存副本,比如新增本地 ehcache 緩存。引入限流降級組件,對緩存進行實時監(jiān)控和實時報警。通過機器替換、服務替換進行及時恢復;也可以通過各種自動故障轉移策略,自動關閉異常接口、停止邊緣服務、停止部分非核心功能措施,確保在極端場景下,核心功能的正常運行。
-
事后:redis 持久化,支持同時開啟兩種持久化方式,我們可以綜合使用 AOF 和 RDB 兩種持久化機制,用 AOF 來保證數據不丟失,作為數據恢復的第一選擇;用 RDB 來做不同程度的冷備,在 AOF 文件都丟失或損壞不可用的時候,還可以使用 RDB 來進行快速的數據恢復。同時把 RDB 數據備份到遠端的云服務,如果服務器內存和磁盤的數據同時丟失,依然可以從遠端拉取數據做災備恢復操作。
? 4??緩存數據不一致
同一份數據,既在緩存里又在數據庫里,肯定會出現數據庫與緩存中的數據不一致現象。如果引入多級緩存架構,緩存會存在多個副本,多個副本之間也會出現緩存不一致現象。緩存機器的帶寬被打滿,或者機房網絡出現波動時,緩存更新失敗,新數據沒有寫入緩存,就會導致緩存和 DB 的數據不一致。緩存 rehash 時,某個緩存機器反復異常,多次上下線,更新請求多次 rehash。這樣,一份數據存在多個節(jié)點,且每次 rehash 只更新某個節(jié)點,導致一些緩存節(jié)點產生臟數據。再比如,數據發(fā)生了變更,先刪除了緩存,然后要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發(fā)現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨后數據變更的程序完成了數據庫的修改,數據庫和緩存中的數據不一樣了。
解決方案:
-
設置 key 的過期時間盡量短,讓緩存更早的過期,從 db 加載新數據,這樣無法保證數據的強一致性,但是可以保證最終一致性。
cache 更新失敗以后引入重試機制,比如連續(xù)重試失敗以后,可以將操作寫入重試隊列,當緩存服務可用時,將這些 key 從緩存中刪除,當這些 key 被重新查詢時,重新從數據庫回種。
延時雙刪除策略,首先刪除緩存中的數據,在寫數據庫,休眠一秒以后(具體時間需要根據具體業(yè)務邏輯的耗時進行調整)再次刪除緩存。這樣可以將一秒內造成的所有臟數據再次刪除。
緩存最終一致性,使客戶端數據與緩存解耦,應用直接寫數據到數據庫中。數據庫更新 binlog 日志,利用 Canal 中間件讀取 binlog 日志。Canal 借助于限流組件按頻率將數據發(fā)到 MQ 中,應用監(jiān)控 MQ 通道,將 MQ 的數據更新到 Redis 緩存中。
更新數據的時候,根據數據的唯一標識,將操作路由之后,發(fā)送到一個 jvm 內部隊列中。讀取數據的時候,如果發(fā)現數據不在緩存中,那么將重新執(zhí)行 “讀取數據 + 更新緩存” 的操作,根據唯一標識路由之后,也發(fā)送到同一個 jvm 內部隊列中。該方案對于讀請求進行了非常輕度的異步化,使用一定要注意讀超時的問題,每個讀請求必須在超時時間范圍內返回。因此需要根據自己的業(yè)務情況進行測試,可能需要部署多個服務,每個服務分攤一些數據的更新操作。如果一個內存隊列里居然會擠壓 100 個業(yè)務數據的修改操作,每個操作操作要耗費 10ms 去完成,那么最后一個讀請求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到數據,這個時候就導致讀請求的長時阻塞。
? 5??競爭并發(fā)
當系統(tǒng)的線上流量特別大的時候,緩存中會出現數據并發(fā)競爭的現象。在高并發(fā)的場景下,如果緩存數據正好過期,各個并發(fā)請求之間又沒有任何協調動作,這樣并發(fā)請求就會打到數據庫,對數據造成較大的壓力,嚴重的可能會導致緩存 “雪崩”。另外高并發(fā)競爭也會導致數據不一致問題,例如多個 redis 客戶端同時 set 同一個 key 時,key 最開始的值是 1,本來按順序修改為 2,3,4,最后是 4,但是順序變成了 4,3,2,最后變成了 2。
解決方案:
分布式鎖 + 時間戳
可以基于 redis 或者 zookeeper 實現一個分布式鎖,當一個 key 被高并發(fā)訪問時,讓請求去搶鎖。也可以引入消息中間件,把 Redis.set 操作放在消息隊列中。總之,將并行讀寫改成串行讀寫的方式,從而來避免資源競爭。對于 key 的操作的順序性問題,可以通過設置一個時間戳來解決。大部分場景下,要寫入緩存的數據都是從數據庫中查詢出來的。在數據寫入數據庫時,可以維護一個時間戳字段,這樣數據被查詢出來時都會帶一個時間戳。寫緩存的時候,可以判斷一下當前數據的時間戳是否比緩存里的數據的時間戳要新,這樣就避免了舊數據對新數據的覆蓋。
? 6??熱點 Key 問題
對于大多數互聯網系統(tǒng),數據是分冷熱的,訪問頻率高的 key 被稱為熱 key,比如熱點新聞、熱點的評論。而在突發(fā)事件發(fā)生時,瞬間會有大量用戶去訪問這個突發(fā)熱點信息,這個突發(fā)熱點信息所在的緩存節(jié)點由于超大流量而達到理網卡、帶寬、CPU 的極限,從而導致緩存訪問變慢、卡頓、甚至宕機。接下來數據請求到數據庫,最終導致整個服務不可用。比如微博中數十萬、數百萬的用戶同時去吃一個新瓜,秒殺、雙 11、618 、春節(jié)等線上促銷活動,明星結婚、離婚、出軌這種特殊突發(fā)事件。
解決方案:
要解決這種極熱 key 的問題,首先要找出這些?熱 key 。對于重要節(jié)假日、線上促銷活動、憑借經驗可以提前評估出可能的熱 key 來。而對于突發(fā)事件,無法提前評估,可以通過 Spark 或者 Flink,進行流式計算,及時發(fā)現新發(fā)布的熱點 key。而對于之前已發(fā)出的事情,逐步發(fā)酵成為熱 key 的,則可以通過 Hadoop 進行離線跑批計算,找出最近歷史數據中的高頻熱 key。還可以通過客戶端進行統(tǒng)計或者上報。找到熱 key 后,就有很多解決辦法了。首先可以將這些熱 key 進行分散處理。redis cluster 有固定的 16384 個 hash slot,對每個 key 計算 CRC16 值,然后對 16384 取模,可以獲取 key 對應的 hash slot。比如一個熱 key 名字叫 hotkey,可以被分散為 hotkey#1、hotkey#2、hotkey#3,……h(huán)otkey#n,這 n 個 key 就會分散存在多個緩存節(jié)點,然后 client 端請求時,隨機訪問其中某個后綴的熱 key,這樣就可以把熱 key 的請求打散。
其次,也可以 key 的名字不變,對緩存提前進行多副本 + 多級結合的緩存架構設計。比如利用 ehcache,或者一個 HashMap 都可以。在你發(fā)現熱 key 以后,把熱 key 加載到系統(tǒng)的 JVM 中,之后針對熱 key 的請求,可以直接從 jvm 中獲取數據。再次,如果熱 key 較多,還可以通過監(jiān)控體系對緩存的 SLA 實時監(jiān)控,通過快速擴容來減少熱 key 的沖擊。
? 7??大 Key 問題
有些時候開發(fā)人員設計不合理,在緩存中會形成特別大的對象,這些大對象會導致數據遷移卡頓,另外在內存分配方面,如果一個 key 特別大,當需要擴容時,會一次性申請更大的一塊內存,這也會導致卡頓。如果大對象被刪除,內存會被一次性回收,卡頓現象會再次發(fā)生。在平時的業(yè)務開發(fā)中,要盡量避免大 key 的產生。如果發(fā)現系統(tǒng)的緩存大起大落,極有可能是大 key 引起的,這就需要開發(fā)人員定位出大 key 的來源,然后進行相關的業(yè)務代碼重構。Redis 官方已經提供了相關的指令進行大 key 的掃描,可以直接使用。
解決方案:?
-
如果數據存在 Redis 中,比如業(yè)務數據存 set 格式,大 key 對應的 set 結構有幾千幾萬個元素,這種寫入 Redis 時會消耗很長的時間,導致 Redis 卡頓。此時,可以擴展新的數據結構,同時讓 client 在這些大 key 寫緩存之前,進行序列化構建,然后通過 restore 一次性寫入。文章來源:http://www.zghlxwxcb.cn/news/detail-574958.html
-
將大 key 分拆為多個 key,盡量減少大 key 的存在。同時由于大 key 一旦穿透到 DB,加載耗時很大,所以可以對這些大 key 進行特殊照顧,比如設置較長的過期時間,比如緩存內部在淘汰 key 時,同等條件下,盡量不淘汰這些大 key。文章來源地址http://www.zghlxwxcb.cn/news/detail-574958.html
到了這里,關于全面解析緩存應用經典問題的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!