1. 程式人生 > >高並發處理思路與手段(二):緩存

高並發處理思路與手段(二):緩存

這也 也會 多說 訂閱 如果 選型 穿透 分享圖片 需要

傳統web請求流程

技術分享圖片


一般來說,現在的互聯網應用網站或者APP,它的整體流程可以用我們這個圖裏展示的來表示,用戶請求開始,從這個界面是最裏面的瀏覽器和APP,到網絡轉發,再到應用服務,最後到存儲,這純屬可能是數據庫文件系統,然後再返回到界面呈現內容。

隨著互聯網的普及,內容信息越來越復雜,用戶數和訪問量越來越大,我們的應用需要支撐更多的並發量,同時,我們的應用服務器和數據庫服務器所做的計算也越來越多,但是,往往我們的應用服務器的資源是有限的,而且技術變革是緩慢的,所以每秒能接收請求次數也是有限的,或者說文件的讀寫也是有限的。

如何能有效利用有限的資源來提供盡可能大的吞吐量呢?一個有效的辦法就是引入緩存,打破圖中的標準的流程,每個環節中請求可以從緩存中直接獲取目標數據並返回,從而減少他們的計算量,來有效提升響應速度,讓有限的資源服務更多的用戶,像我們這個圖裏展示的緩存的使用,它其實可以出現在1到4的各個環節中。

緩存的特征

1.命中率:命中數/(命中數+沒有命中數)

首先是命中,命中的還可以簡單的理解為直接通過緩存獲得到需要的數據,有了命中就有不命中,無法通過緩存的獲取想要的數據,需要再次查詢數據庫,或者執行其他操作,原因可能是緩存中根本不存在,或者緩存已經過期了。

通常 命中率=命中數/(命中數+沒有命中數) 來表達,能力越高,表示我們使用緩存的收益越高,應用的性能越好,這時候響應的時間會越短,吞吐量越來越高,抗並發的能力也越強,由此可見在高並發的互聯網系統中,命中率是至關重要的一個指標。

2.最大元素(空間)

它代表的是緩存中可以存放的最大元素的數量,一旦緩存數量超過這個值,或則所占的空間,超過了最大支持的空間,就會促發緩存清空策略,根據不同的場景合理的設置最大元素值,往往可以一定程度上提高緩存的命中率,從而更有效的使用緩存。

像我們剛才所描述的,緩存的存儲空間是有限制的,當緩存空間滿時,如何保證在穩定服務的同時有效的提高命中率呢?這就有緩存的清空策略來處理,適合自身數據特征的清空策略,能有效的提高命中率。常見的清空策略為下面所示

3.清空策略:FIFO,LFU,LRU,過期時間,隨機等

FIFO(First In First out)先進先出策略:是指最先進入緩存的數據,在緩存空間不夠的情況下,或者超出最大源頭限制的時候,會優先被清除掉,以騰出新的空間來接受新的數據,這個策略算法主要是比較緩存元素的創建時間,在數據實時性要求嚴格下,可以選擇該類策略,優先保障最新數據可用。
LFU(Least frequently used):是指無論是否過期,根據元素的被使用次數來判斷,清除使用次數最少的元素來釋放空間

,這個策略的算法主要比較的元素的命中次數,在保證高頻率場景下,可以選擇這裏策略。
LRU(Least recently used): 是指無論是否過期,根據元素最後一次被使用的時間戳,清除最原始用時間戳的元素釋放空間,主要比較元素的最近一次被get使用時間,在熱點數據場景下優先保證熱點數據的有效性
除此之外呢,還有一些簡單的策略,比如根據過期時間來判斷,清理過期時間最長的元素,還可以根據過期時間判斷清理最近要過期的元素,以及隨機清理等等。

4.影響緩存命中率的因素

a.業務場景和業務需求

緩存適合讀多寫少的業務場景,反之使用緩存的意義並不大,命中率還會很低,業務需求也決定了對實時性的要求,直接影響到緩存的過期時間和更新策略,實時性要求越低,就越適合緩存,在相同key和相同請求數的情況下,緩存時間越長命中率就會越高,我們目前遇到的互聯網應用,大多數的業務場景下都是很適合使用緩存的。

b.緩存的設計(粒度和策略)

通常情況下呢,緩存的粒度越小,命中率就會越高,當換成單個對象的時候,比如單個用戶信息,只有當該對象的對應的數據發生變化時後,我們才需要更新緩存或者移除緩存,而當緩存一個集合的時候,我們要獲得所有用戶數據,其中任何一個對象對應的數據發生變化時,我們都需要更新或移除緩存,還有另一個情況,假設其他地方也需要獲取該對象對應的數據時,比如說其他地方也需要獲取單個用戶信息,如果緩存的是單個對象,那麽就可以直接命中緩存,否則的話就無法直接命中。

c.緩存容量和基礎設施

緩存容量有限就會引起緩存失效和淘汰,目前多個緩存中間件多采用LRU算法。技術選型也很重要,建議采用分布式緩存。

5.緩存分類和應用場景

a.本地緩存

實現方式:編程實現(成員變量、局部變量、靜態變量)、Guava Cache

優點:是應用進程的cache是在同一個進程中內部請求緩存非常的快速,沒有過多的網絡開銷。

缺點:是各個應用要單獨維護自己的緩存,無法共享。在單應用中使用較為好。

b.分布式緩存

實現方式:Memcache, Redis

優點:是自身是一個獨立的應用,與本地應用是隔離的,多個應用可以共享。

Guava Cache

技術分享圖片

Memcache

技術分享圖片

Redis

更多redis知識參閱:https://www.cnblogs.com/shamo89/tag/redis/

技術分享圖片

高並發場景下緩存常見問題

1.緩存一致性

當數據時效性要求很高時,需要保證緩存中的數據與數據庫中的保持一致,而且需要保證緩存節點和副本中的數據也保持一致,不能出現差異現象。這就比較依賴緩存的過期和更新策略。一般會在數據發生更改的時,主動更新緩存中的數據或者移除對應的緩存。
技術分享圖片

2.緩存並發問題

緩存過期後將嘗試從後端數據庫獲取數據,這是一個看似合理的流程。但是,在高並發場景下,有可能多個請求並發的去從數據庫獲取數據,對後端數據庫造成極大的沖擊,甚至導致 “雪崩”現象。此外,當某個緩存key在被更新時,同時也可能被大量請求在獲取,這也會導致一致性的問題。那如何避免類似問題呢?我們會想到類似“鎖”的機制,在緩存更新或者過期的情況下,先嘗試獲取到鎖,當更新或者從數據庫獲取完成後再釋放鎖,其他的請求只需要犧牲一定的等待時間,即可直接從緩存中繼續獲取數據。
技術分享圖片

3.緩存穿透問題

緩存穿透在有些地方也稱為“擊穿”。很多朋友對緩存穿透的理解是:由於緩存故障或者緩存過期導致大量請求穿透到後端數據庫服務器,從而對數據庫造成巨大沖擊。

這其實是一種誤解。真正的緩存穿透應該是這樣的:

在高並發場景下,如果某一個key被高並發訪問,沒有被命中,出於對容錯性考慮,會嘗試去從後端數據庫中獲取,從而導致了大量請求達到數據庫,而當該key對應的數據本身就是空的情況下,這就導致數據庫中並發的去執行了很多不必要的查詢操作,從而導致巨大沖擊和壓力。

可以通過下面的幾種常用方式來避免緩存穿透問題:

a.緩存空對象

對查詢結果為空的對象也進行緩存,如果是集合,可以緩存一個空的集合(非null),如果是緩存單個對象,可以通過字段標識來區分。這樣避免請求穿透到後端數據庫。同時,也需要保證緩存數據的時效性。這種方式實現起來成本較低,比較適合命中不高,但可能被頻繁更新的數據

b.單獨過濾處理

對所有可能對應數據為空的key進行統一的存放,並在請求前做攔截,這樣避免請求穿透到後端數據庫。這種方式實現起來相對復雜,比較適合命中不高,但是更新不頻繁的數據
技術分享圖片

4.緩存的雪崩現象

緩存雪崩就是指由於緩存的原因,導致大量請求到達後端數據庫,從而導致數據庫崩潰,整個系統崩潰,發生災難。導致這種現象的原因有很多種,上面提到的“緩存並發”,“緩存穿透”,“緩存顛簸”等問題,其實都可能會導致緩存雪崩現象發生。這些問題也可能會被惡意攻擊者所利用。還有一種情況,例如某個時間點內,系統預加載的緩存周期性集中失效了,也可能會導致雪崩。為了避免這種周期性失效,可以通過設置不同的過期時間,來錯開緩存過期,從而避免緩存集中失效。

從應用架構角度,我們可以通過限流、降級、熔斷等手段來降低影響,也可以通過多級緩存來避免這種災難。此外,從整個研發體系流程的角度,應該加強壓力測試,盡量模擬真實場景,盡早的暴露問題從而防範。
技術分享圖片

5.緩存無底洞現象

該問題由 facebook 的工作人員提出的, facebook 在 2010 年左右,memcached 節點就已經達3000 個,緩存數千 G 內容。他們發現了一個問題—memcached 連接頻率,效率下降了,於是加 memcached 節點,添加了後,發現因為連接頻率導致的問題,仍然存在,並沒有好轉,稱之為”無底洞現象”。
技術分享圖片

目前主流的數據庫、緩存、Nosql、搜索中間件等技術棧中,都支持“分片”技術,來滿足“高性能、高並發、高可用、可擴展”等要求。有些是在client端通過Hash取模(或一致性Hash)將值映射到不同的實例上,有些是在client端通過範圍取值的方式映射的。當然,也有些是在服務端進行的。但是,每一次操作都可能需要和不同節點進行網絡通信來完成,實例節點越多,則開銷會越大,對性能影響就越大。

主要可以從如下幾個方面避免和優化:

  1. 數據分布方式:有些業務數據可能適合Hash分布,而有些業務適合采用範圍分布,這樣能夠從一定程度避免網絡IO的開銷。
  2. IO優化:可以充分利用連接池,NIO等技術來盡可能降低連接開銷,增強並發連接能力。
  3. 數據訪問方式:一次性獲取大的數據集,會比分多次去獲取小數據集的網絡IO開銷更小。

當然,緩存無底洞現象並不常見。在絕大多數的公司裏可能根本不會遇到。

Redis在股票分時K線圖計算的實踐

技術分享圖片

來公司第一個比較大的業務需求,便是換了新的行情提供商,需要把所有K線的處理都重新倒騰一遍。這裏說說重寫分時K線圖的一點心得。

先交代一下數據提供商的情況:

1)股票分時數據采用訂閱的方式,可以訂閱歷史數據

2)每分鐘每只股票可能會接收到多條分時數據,也可能一條不推。有些股票在開市過程中每分鐘都會有分時數據,有些股票則一個交易日只有幾條數據,甚至沒有,差別很大

3)推送的分時數據有延遲,從監控獲得數據,開市期間大約在60s-120s左右(分時數據看的是趨勢,這是可以忍受的)

4)每個訂閱對實時數據有qps限制,對歷史數據無qps限制(從監控上得到的)

再交代一下股票分時K線數據的情況:

1)股票數目為8500+(美股相關)

2)每只股票1個交易日的分時線包含391條數據(9:30-16:00, America/New_York),每條數據包含這一分鐘的最高價、最低價、均價、昨收價、漲幅、交易量、VWAP等,數據量在150k左右

3)每天存儲的分時數據在120w條左右

4)股票分時線如果某一分鐘沒有點,使用上一分鐘的點代替,但無交易量

接下來重點說一下這裏的設計:(以AAPL為例)

1)由於分時數據有延遲,無法確定每一分鐘的最後一條數據什麽時候會來到。因此開市期間並不去保存分時數據。實時的分時數據存儲在redis中,計算K線時從redis中取

2)redis中分時數據的存儲格式為hash結構,每只股票一個key,為了防止當前交易日使用上個交易日的數據,因此分時數據緩存時是包含日期的,比如:TMT_AAPL0105, key為美東時間,格式:HHmm, value為簡單計算後的分時數據。上一個交易日的分時數據通過定時任務在指定時間清理。(這裏沒有對分時數據做過期設置,是因為剛開市時數據推送量及redis操作量很大,而使用hashes這個結構,是不支持直接傳入expire time的,同時即使某天系統出什麽問題,也只是多占些硬盤的問題,不會對其他造成影響,對這個操作添加監控及報警就可以了)

3)結構有了,如果第三方數據提供商每次都推送數據過來時,都去操作redis更新,那樣redis很可能會吃不消,誰都無法曉得第三方是否會出什麽問題瘋狂推送一下(事實證明,第三方數據提供商也確實這樣有過,可能因網絡故障等補償推送歷史數據)。接下來是緩存的的重點:在內存中通過guava cache緩存了最近幾分鐘的所有symbol的最新數據,key: 股票+HHmm(美東時間), value:一條分時數據,每次推送分時數據過來時,根據股票及分時數據時間進行cache,過期數據(很久以前或非最新數據)或臟數據(相關數據為NaN)直接拋棄,然後通過ScheduledExecutorService維護一個1分鐘1執行的job去將最近幾分鐘的分時數據更新到redis中。這樣既解決了分時數據有延遲的問題,又保證了每分鐘的redis更新數據量。

4)數據存儲結束後,就是處理分時K線的數據了。當請求AAPL時,會先查看是否已經有cache了,比如C_TT_AAPL, 如果有取出cache中數據處理後返回,沒有開始計算。計算會先取出上面存儲的數據,即TMT_AAPL0105,然後計算當前k線的時間點(開市過程中從1個點慢慢增長到391個點),遍歷HHmm去TMT_AAPL0105中取分時數據,如果有直接取出即可,如果沒有使用上一個點生成(第一個記得特殊處理一下,沒有需要拿上個交易日最後一個點補),計算完後轉成json放到redis中,key為計算前查詢的key: C_TT_AAPL,然後將結果返回。開市過程中緩存到下一分鐘開始,閉市了緩存到下個交易日開始。由於這裏緩存的是string,因此直接帶上過期時間,之後就不用care了。如此,最核心的分時K線數據絕大部分都是在操作redis,每只股票每分鐘只需要計算一次即可,當然這是基於分時K線主要看的是趨勢。

5)上面說了,分時數據可以拿歷史數據。這裏我單獨寫了一個job,可以在給定開始時間後,取出期望的歷史數據,然後更新歷史分時數據。當實時推送出問題時,這個job可以用來在秒級獲取歷史數據來修復redis中的分時數據。實際上,這個job在故障中的表現遠遠超出預期,之前因為對實時那裏做優化,幾次都有點問題導致開市後實時數據獲取或處理有問題,這時候啟動這個拿歷史分時的job,幾秒內就可以保證redis中的數據變成最新,保證核心的分時圖數據一直ok。

6)說到這裏,分時數據還沒保存。這裏借助上面拿歷史數據的job,在閉市後(北京時間淩晨5點以後)自動執行,更新redis中數據後開始插入數據庫。由於插入量特別大,通過guava的RateLimiter控制寫入速度在合理的qps值,慢慢更新就好啦

當然這不是全部,還有些細節,比如動態增加股票,就不多說了。此外,我單獨補了許多核心監控,比如定時任務是否正常執行、讀取及更新redis時間、開市期間推送的qps、計算分時K線平均時間、平均每分鐘推送不同股票數目等等,目前達到的結果是:docker上部署的2個4g的服務來處理所有K線圖,分時K線圖數據可以在平均10ms左右返回,大量的分時數據在低峰時間端插入,服務器在開市閉市都沒什麽壓力。

高並發處理思路與手段(二):緩存