一位七牛的資深架構師曾經說過這樣一句話:前端
Nginx+業務邏輯層+數據庫+緩存層+消息隊列,這種模型幾乎能適配絕大部分的業務場景。程序員
這麼多年過去了,這句話或深或淺地影響了個人技術選擇,以致於後來我花了不少時間去重點學習緩存相關的技術。web
我在10年前開始使用緩存,從本地緩存、到分佈式緩存、再到多級緩存,踩過不少坑。下面我結合本身使用緩存的歷程,談談我對緩存的認識。sql
1. 頁面級緩存數據庫
我使用緩存的時間很早,2010年左右使用過 OSCache,當時主要用在 JSP 頁面中用於實現頁面級緩存。僞代碼相似這樣:後端
<cache:cache key="foobar" scope="session"> some jsp content </cache:cache>`
中間的那段 JSP 代碼將會以 key="foobar" 緩存在 session 中,這樣其餘頁面就能共享這段緩存內容。 在使用 JSP 這種遠古技術的場景下,經過引入 OSCache 以後 ,頁面的加載速度確實提高很快。數組
但隨着先後端分離以及分佈式緩存的興起,服務端的頁面級緩存已經不多使用了。可是在前端領域,頁面級緩存仍然很流行。緩存
2. 對象緩存性能優化
2011年左右,開源中國的紅薯哥寫了不少篇關於緩存的文章。他提到:開源中國天天百萬的動態請求,只用 1 臺 4 Core 8G 的服務器就扛住了,得益於緩存框架 Ehcache。服務器
這讓我很是神往,一個簡單的框架竟能將單機性能作到如此這般,讓我欲欲躍試。因而,我參考紅薯哥的示例代碼,在公司的餘額提現服務上第一次使用了 Ehcache。
邏輯也很簡單,就是將成功或者失敗狀態的訂單緩存起來,這樣下次查詢的時候,不用再查詢支付寶服務了。僞代碼相似這樣:
添加緩存以後,優化的效果很明顯 , 任務耗時從原來的40分鐘減小到了5~10分鐘。
上面這個示例就是典型的「對象緩存」,它是本地緩存最多見的應用場景。相比頁面緩存,它的粒度更細、更靈活,經常使用來緩存不多變化的數據,好比:全局配置、狀態已完結的訂單等,用於提高總體的查詢速度。
3. 刷新策略
2018年,我和個人小夥伴自研了配置中心,爲了讓客戶端以最快的速度讀取配置, 本地緩存使用了 Guava,總體架構以下圖所示:
那本地緩存是如何更新的呢?有兩種機制:
客戶端啓動定時任務,從配置中心拉取數據。
當配置中心有數據變化時,主動推送給客戶端。這裏我並無使用websocket,而是使用了 RocketMQ Remoting 通信框架。
後來我閱讀了 Soul 網關的源碼,它的本地緩存更新機制以下圖所示,共支持 3 種策略:
▍ zookeeper watch機制
soul-admin 在啓動的時候,會將數據全量寫入 zookeeper,後續數據發生變動時,會增量更新 zookeeper 的節點。與此同時,soul-web 會監聽配置信息的節點,一旦有信息變動時,會更新本地緩存。
▍ websocket 機制
websocket 和 zookeeper 機制有點相似,當網關與 admin 首次創建好 websocket 鏈接時,admin 會推送一次全量數據,後續若是配置數據發生變動,則將增量數據經過 websocket 主動推送給 soul-web。
▍ http 長輪詢機制
http請求到達服務端後,並非立刻響應,而是利用 Servlet 3.0 的異步機制響應數據。當配置發生變化時,服務端會挨個移除隊列中的長輪詢請求,告知是哪一個 Group 的數據發生了變動,網關收到響應後,再次請求該 Group 的配置數據。
不知道你們發現了沒?
長輪詢是一個有意思的話題 , 這種模式在 RocketMQ 的消費者模型也一樣被使用,接近準實時,而且能夠減小服務端的壓力。
關於分佈式緩存, memcached 和 Redis 應該是最經常使用的技術選型。相信程序員朋友都很是熟悉了,我這裏分享兩個案例。
1. 合理控制對象大小及讀取策略
2013年,我服務一家彩票公司,咱們的比分直播模塊也用到了分佈式緩存。當時,遇到了一個 Young GC 頻繁的線上問題,經過 jstat 工具排查後,發現新生代每隔兩秒就被佔滿了。
進一步定位分析,原來是某些 key 緩存的 value 太大了,平均在 300K左右,最大的達到了500K。這樣在高併發下,就很容易 致使 GC 頻繁。
找到了根本緣由後,具體怎麼改呢? 我當時也沒有清晰的思路。 因而,我去同行的網站上研究他們是怎麼實現相同功能的,包括: 360彩票,澳客網。我發現了兩點:
一、數據格式很是精簡,只返回給前端必要的數據,部分數據經過數組的方式返回
二、使用 websocket,進入頁面後推送全量數據,數據發生變化推送增量數據
再回到個人問題上,最終是用什麼方案解決的呢?當時,咱們的比分直播模塊緩存格式是 JSON 數組,每一個數組元素包含 20 多個鍵值對, 下面的 JSON 示例我僅僅列了其中 4 個屬性。
[{ "playId":"2399", "guestTeamName":"小牛", "hostTeamName":"湖人", "europe":"123" }]
這種數據結構,通常狀況下沒有什麼問題。可是當字段數多達 20 多個,並且天天的比賽場次很是多時,在高併發的請求下其實很容易引起問題。
基於工期以及風險考慮,最終咱們採用了比較保守的優化方案:
1)修改新生代大小,從原來的 2G 修改爲 4G
2)將緩存數據的格式由 JSON 改爲數組,以下所示:
[["2399","小牛","湖人","123"]]
修改完成以後, 緩存的大小從平均 300k 左右降爲 80k 左右,YGC 頻率降低很明顯,同時頁面響應也變快了不少。
但過了一會,cpu load 會在瞬間波動得比較高。可見,雖然咱們減小了緩存大小,可是讀取大對象依然對系統資源是極大的損耗,致使 Full GC 的頻率也不低。
3)爲了完全解決這個問題,咱們使用了更精細化的緩存讀取策略。
咱們把緩存拆成兩個部分,第一部分是全量數據,第二部分是增量數據(數據量很小)。頁面第一次請求拉取全量數據,當比分有變化的時候,經過 websocket 推送增量數據。
第 3 步完成後,頁面的訪問速度極快,服務器的資源使用也不多,優化的效果很是優異。
通過此次優化,我理解到: 緩存雖然能夠提高總體速度,可是在高併發場景下,緩存對象大小依然是須要關注的點,稍不留神就會產生事故。另外咱們也須要合理地控制讀取策略,最大程度減小 GC 的頻率 , 從而提高總體性能。
2. 分頁列表查詢
列表如何緩存是我很是渴望和你們分享的技能點。這個知識點也是我 2012 年從開源中國上學到的,下面我以「查詢博客列表」的場景爲例。
咱們先說第 1 種方案:對分頁內容進行總體緩存。這種方案會 按照頁碼和每頁大小組合成一個緩存key,緩存值就是博客信息列表。 假如某一個博客內容發生修改, 咱們要從新加載緩存,或者刪除整頁的緩存。
這種方案,緩存的顆粒度比較大,若是博客更新較爲頻繁,則緩存很容易失效。下面我介紹下第 2 種方案:僅對博客進行緩存。流程大體以下:
1)先從數據庫查詢當前頁的博客id列表,sql相似:
select id from blogs limit 0,10
2)批量從緩存中獲取博客id列表對應的緩存數據 ,並記錄沒有命中的博客id,若沒有命中的id列表大於0,再次從數據庫中查詢一次,並放入緩存,sql相似:
select id from blogs where id in (noHitId1, noHitId2)
3)將沒有緩存的博客對象存入緩存中
4)返回博客對象列表
理論上,要是緩存都預熱的狀況下,一次簡單的數據庫查詢,一次緩存批量獲取,便可返回全部的數據。另外,關於 緩 存批量獲取,如何實現?
第 1 種方案適用於數據極少發生變化的場景,好比排行榜,首頁新聞資訊等。
第 2 種方案適用於大部分的分頁場景,並且能和其餘資源整合在一塊兒。舉例:在搜索系統裏,咱們能夠經過篩選條件查詢出博客 id 列表,而後經過如上的方式,快速獲取博客列表。
首先要明確爲何要使用多級緩存?
本地緩存速度極快,可是容量有限,並且沒法共享內存。分佈式緩存容量可擴展,但在高併發場景下,若是全部數據都必須從遠程緩存種獲取,很容易致使帶寬跑滿,吞吐量降低。
有句話說得好,緩存離用戶越近越高效!
使用多級緩存的好處在於:高併發場景下, 能提高整個系統的吞吐量,減小分佈式緩存的壓力。
2018年,我服務的一家電商公司須要進行 app 首頁接口的性能優化。我花了大概兩天的時間完成了整個方案,採起的是兩級緩存模式,同時利用了 guava 的惰性加載機制,總體架構以下圖所示:
緩存讀取流程以下:
一、業務網關剛啓動時,本地緩存沒有數據,讀取 Redis 緩存,若是 Redis 緩存也沒數據,則經過 RPC 調用導購服務讀取數據,而後再將數據寫入本地緩存和 Redis 中;若 Redis 緩存不爲空,則將緩存數據寫入本地緩存中。
二、因爲步驟1已經對本地緩存預熱,後續請求直接讀取本地緩存,返回給用戶端。
三、Guava 配置了 refresh 機制,每隔一段時間會調用自定義 LoadingCache 線程池(5個最大線程,5個核心線程)去導購服務同步數據到本地緩存和 Redis 中。
優化後,性能表現很好,平均耗時在 5ms 左右。最開始我覺得出現問題的概率很小,但是有一天晚上,忽然發現 app 端首頁顯示的數據時而相同,時而不一樣。
也就是說: 雖然 LoadingCache 線程一直在調用接口更新緩存信息,可是各個 服務器本地緩存中的數據並不是完成一致。 說明了兩個很重要的點:
一、惰性加載仍然可能形成多臺機器的數據不一致
二、 LoadingCache 線程池數量配置的不太合理, 致使了線程堆積
最終,咱們的解決方案是:
一、惰性加載結合消息機制來更新緩存數據,也就是:當導購服務的配置發生變化時,通知業務網關從新拉取數據,更新緩存。
二、適當調大 LoadigCache 的線程池參數,並在線程池埋點,監控線程池的使用狀況,當線程繁忙時能發出告警,而後動態修改線程池參數。
緩存是很是重要的一個技術手段。若是能從原理到實踐,不斷深刻地去掌握它,這應該是技術人員最享受的事情。
這篇文章屬於緩存系列的開篇,更可能是把我 10 多年工做中遇到的典型問題娓娓道來,並無很是深刻地去探討原理性的知識。
我想我更應該和朋友交流的是:如何體系化的學習一門新技術。
後續我會連載一些緩存相關的內容:包括緩存的高可用機制、codis 的原理等,歡迎你們繼續關注。
關於緩存,若是你有本身的心得體會或者想深刻了解的內容,歡迎評論區留言。
做者簡介:985碩士,前亞馬遜工程師,現58轉轉技術總監
歡迎掃描下方的二維碼,關注個人我的公衆號:IT人的職場進階