高并發請求的緩存設計策略

前幾天,我司出了個簍子。當時正值某喜聞樂見的關鍵比賽結束,一堆人打開我司app準備看點東西,結果從來沒有感受到過這么多關注量的該功能瞬間幸福到眩暈,觸發了熔斷,結果就是大量興致沖沖打開app準備看該比賽結果的人被迫刷了十分鐘三天前的野外跑酷,負責內容的人火大到直接罵娘。

雖然這個業務不是我負責,但是也跟相關的人聊了下情況,感慨了一下,于是有了這一篇文章。

1.為何需要緩存?

在高并發請求時,為何我們頻繁提到緩存技術?最直接的原因是,目前磁盤IO和網絡IO相對于內存IO的成百上千倍的性能劣勢。
做個簡單計算,如果我們需要某個數據,該數據從數據庫磁盤讀出來需要0.1s,從交換機傳過來需要0.05s,那么每個請求完成最少0.15s(當然,事實上磁盤和網絡IO也沒有這么慢,這里只是舉例),該數據庫服務器每秒只能響應67個請求;而如果該數據存在于本機內存里,讀出來只需要10us,那么每秒鐘能夠響應100,000個請求。

通過將高頻使用的數據存在離cpu更近的位置,以減少數據傳輸時間,從而提高處理效率,這就是緩存的意義。

2.在哪里用緩存?

一切地方。例如:

  • 我們從硬盤讀數據的時候,其實操作系統還額外把附近的數據都讀到了內存里
  • 例如,CPU在從內存里讀數據的時候,也額外讀了許多數據到各級cache里
  • 各個輸入輸出之間用buffer保存一批數據統一發送和接受,而不是一個byte一個byte的處理

上面這是系統層面,在軟件系統設計層面,很多地方也用了緩存:

  • 瀏覽器會緩存頁面的元素,這樣在重復訪問網頁時,就避開了要從互聯網上下載數據(例如大圖片)
  • web服務會把靜態的東西提前部署在CDN上,這也是一種緩存
  • 數據庫會緩存查詢,所以同一條查詢第二次就是要比第一次快
  • 內存數據庫(如redis)選擇把大量數據存在內存而非硬盤里,這可以看作是一個大型緩存,只是把整個數據庫緩存了起來
  • 應用程序把最近幾次計算的結果放在本地內存里,如果下次到來的請求還是原請求,就跳過計算直接返回結果

3.本次事故分析

回到本文開始的問題上,該系統是怎么設計的呢?底層是數據庫,中間放了一層redis,前面的業務系統所需的數據都直接從redis里取,然后計算出結果返回給app;數據庫和redis的同步另外有程序保證,避免redis的穿透,防止了程序里出現大量請求從redis里找不到,于是又一窩蜂的去查數據庫,直接壓垮數據庫的情況。從這個角度講,其實這一步是做的還可以的。

但是這個系統有兩個問題:
1.業務系統需要的數據雖然都在redis里,但是是分開存放的。什么意思呢,比如我前臺發起一個請求,后臺先去redis里取一下標題,然后再取一下作者,然后再取一下內容,再取一下評論,再取一下轉發數等等……結果前臺一次請求,后臺要請求redis十幾次。高并發的時候,壓力一下被放大十幾倍,redis響應、網絡響應必然會變慢。
2.其實做業務的那波人也意識到了這個情況可能發生,所以做了熔斷機制,另起了一個緩存池,里面放了一些備用數據,如果主業務超時,直接從緩存池里取數據返回。但是他們設計的時候沒想周全,這個備選池的數據過期時間設計的太長了,里面居然還有三天前更新進去的數據,最終導致了一大波用戶刷出來三天前的野外生態小視頻……

說到這,不知道讀者有沒有意識到他們最致命的一個問題:這個業務系統完全沒有考慮本地緩存(也就是在業務服務器內存里做緩存)。比如像我們這種app,一旦大量用戶同一時間涌進來,必定都是奔著少數幾個內容去的,這種特別集中的高頻次極少量數據訪問,又不需要對每個用戶做特化的,簡直就是在臉上寫上“請緩存我”。
這時候,如果能在業務端做一層本地緩存,直接把算好的數據本地存一份,那么就會極大減少網絡和redis的壓力,不至于當場觸發熔斷了。

4.淺談緩存的那些坑

緩存很有用,但是緩存用不好也會埋很多坑:

緩存穿透

緩存穿透是說收到了一個請求,但是該請求緩存里沒有,只能去數據庫里查詢,然后放進緩存。這里面有兩個風險,一個是同時有好多請求訪問同一個數據,然后業務系統把這些請求全發到了數據庫;第二個是有人惡意構造一個邏輯上不存在的數據,然后大量發送這個請求,這樣每次請求都會被發送到數據庫,可能導致數據掛掉。

怎么應對這種情況呢?對于惡意訪問,一個思路是事先做校驗,對惡意數據直接過濾掉,不要發到數據庫層;第二個思路是緩存空結果,就是對查詢不存在的數據仍然記錄一條該數據不存在在緩存里,這樣能有效的減少查詢數據庫的次數。

那么非惡意訪問呢?這個要結合緩存擊穿來講。

緩存擊穿

上面提到的某個數據沒有,然后好多請求都被發到數據庫其實可以歸為緩存擊穿的范疇:對于熱點數據,當數據失效的一瞬間,所有請求都被下放到數據庫去請求更新緩存,數據庫被壓垮。

怎么防范這種問題呢?一個思路是全局鎖,就是所有訪問某個數據的請求都共享一個鎖,獲得鎖的那個才有資格去訪問數據庫,其他線程必須等待。但是現在的業務都是分布式的,本地鎖沒法控制其他服務器也等待,所以要用到全局鎖,比如用redis的setnx實現全局鎖。

另一個思路是對即將過期的數據主動刷新,做法可以有很多,比如起一個線程輪詢數據,比如把所有數據劃分為不同的緩存區間,定期分區間刷新數據等等。這第二個思路又和我們接下來要講的緩存雪崩有關系。

緩存雪崩

緩存雪崩是指比如我們給所有的數據設置了同樣的過期時間,然后在某一個歷史性時刻,整個緩存的數據全部過期了,然后瞬間所有的請求都被打到了數據庫,數據庫就崩了。

解決思路要么是分治,劃分更小的緩存區間,按區間過期;要么是給每個key的過期時間加個隨機值,避免同時過期,達到錯峰刷新緩存的目的。

緩存刷新

說到刷新緩存,其實也有坑的。比如我之前的一份工作里,有一次大活動,正是如火如荼的時候,所有的廣告位突然都變空白了。后來追查原因,所有的廣告素材都在緩存里,然后起了個程序,專門負責刷新緩存,每次把當前的素材全量刷新。

壞就壞在這個全量上。因為大活動的時候流量極大,廣告更新壓力也很大,把負責提供更新素材的程序壓崩了。刷新緩存的程序在請求時,收到了一個返回結果Null。接下來就喜聞樂見了,刷新程序根據這個null,清空了整個緩存,所有廣告素材都失效了。

總之,想要做好高并發系統的緩存,就要考慮到各種邊角情況,小心設計,任何細小的疏忽都可能導致系統崩潰。

最后,歡迎關注我的公眾號,一起交流技術,或者職業生涯?

image_1cfhqfqgt17r89b4156i1bni1hqq9.png-40.3kB

posted @ 2018-06-09 16:30  老白講互聯網  閱讀(22070)  評論(39編輯  收藏
最新chease0ldman老人|无码亚洲人妻下载|大香蕉在线看好吊妞视频这里有精品www|亚洲色情综合网