服務端緩存技術總結

1、使用場景

什麼狀況適合用緩存?考慮如下兩種場景:html

  • 短期內相同數據重複查詢屢次且數據更新不頻繁,這個時候能夠選擇先從緩存查詢,查詢不到再從數據庫加載並回設到緩存的方式
  • 高併發查詢和更新熱點數據,後端數據庫不堪重負,能夠用緩存來扛。

2、緩存利弊

(1) 加速讀寫:一般來講加速是明顯的,由於緩存一般都是全內存的系統,然後端(多是mysql、甚至是別人的HTTP, RPC接口)都有速度慢和抗壓能力差的特性,經過緩存的使用能夠有效的提升用戶的訪問速度同時優化了用戶的體驗。java

(2) 下降後端負載:經過緩存的添加,若是程序沒有什麼問題,在命中率還能夠的狀況下,能夠幫助後端減小訪問量和複雜計算(join、或者沒法在優化的sql等),在很大程度下降了後端的負載。mysql

弊(代價)

 (1) 數據不一致性:不管你的設計作的多麼好,緩存數據與權威數據源(能夠理解成真實或者後端數據源)必定存在着必定時間窗口的數據不一致性,這個時間窗口的大小可大可小,具體多大還要看一下你的業務容許多大時間窗口的不一致性。redis

 (2) 代碼維護成本:加入緩存後,代碼就會在原數據源基礎上加入緩存的相關代碼,例如原來只是一些sql, 如今要加入k-v緩存,必然增長了代碼的維護成本。算法

 (3) 架構複雜度:加入緩存後,例如加入了redis-cluster,通常來講緩存不會像Mysql有專門的DBA,頗有可能沒有專職的管理人員,因此也增長了架構的複雜度和維護成本。sql

若是要加入選擇了緩存,必定要能給出足夠的理由,不是爲了簡單的show技術和想固然,最好的方法就是用數聽說話:加速比有多少、後端負載下降了多少。數據庫

3、緩存分類

1. 本地緩存

(1) 緩存和應用在一個JVM中,請求緩存快速,沒有網絡傳輸的開銷。編程

(2) 緩存間是不通訊的、獨立的,應用程序和緩存耦合,多個應用程序沒法直接共享緩存,緩存單獨維護,對內存是一種浪費。後端

(3) 弱一致性。緩存

常見本地緩存

(1)本地編程直接實現

成員變量或者局部變量實現,以局部變量map結構緩存部分業務數據,減小頻繁的重複數據庫I/O操做。缺點僅限於類的自身做用域內,類間沒法共享緩存;

靜態變量實現,實現類間共享。那麼如何解決本地緩存的實時性問題,實現自動更新緩存?目前大量使用的是結合ZooKeeper的自動發現機制,實時變動本地靜態變量緩存。

                                來自美團點評技術中心

                                               (上圖來自美團點評技術中心博客

這類緩存實現,優勢是能直接在heap區內讀寫,最快也最方便;缺點一樣是受heap區域影響,緩存的數據量很是有限,同時緩存時間受GC影響(JVM在進行垃圾回收時,會致使全部的工做線程暫停(stop the world),GC成爲影響Java程序性能的重要因素)。主要知足單機場景下的小數據量緩存需求,同時對緩存數據的變動無需太敏感感知,如上通常配置管理、基礎靜態數據等場景。

(2) Ehcache

Ehcache是如今最流行的純Java開源緩存框架,配置簡單、結構清晰、功能強大,是一個很是輕量級的緩存實現,咱們經常使用的Hibernate(Hibernate二級緩存)裏面就集成了相關緩存功能。

須要注意的是,雖然Ehcache支持磁盤的持久化,可是因爲存在兩級緩存介質,在一級內存中的緩存,若是沒有主動的刷入磁盤持久化的話,在應用異常down機等情形下,依然會出現緩存數據丟失,爲此能夠根據須要將緩存刷到磁盤,將緩存條目刷到磁盤的操做能夠經過cache.flush()方法來執行,須要注意的是,對於對象的磁盤寫入,前提是要將對象進行序列化。

(3)Guava Cache

繼承了ConcurrentHashMap的思路,使用多個segments方式的細粒度鎖,在保證線程安全的同時,支持高併發場景需求。Cache相似於Map,它是存儲鍵值對的集合,不一樣的是它還須要處理evict、expire、dynamic load等算法邏輯,須要一些額外信息來實現這些操做。

2. Standalone(單機)

                                 

(1) 緩存和應用是獨立部署的。

(2) 緩存能夠是單臺。(例如memcache/redis單機等等)

(3) 強一致性

(4) 無高可用、無分佈式。

(5) 跨進程、跨網絡

3. Distributed(分佈式)

                                

例如Redis-Cluster, memcache集羣等等

(1) 緩存和應用是獨立部署的。

(2) 多個實例。(例如memcache/redis等等)

(3) 強一致性或者最終一致性

(4) 支持Scale Out、高可用。

(5) 跨進程、跨網絡

memcache集羣

memcached是應用較廣的開源分佈式緩存產品之一,它自己其實不提供分佈式解決方案。在服務端,memcached集羣環境實際就是一個個memcached服務器的堆積,環境搭建較爲簡單;cache的分佈式主要是在客戶端實現,經過客戶端的路由處理來達到分佈式解決方案的目的。

redis集羣

與memcached客戶端支持分佈式方案不一樣,Redis更傾向於在服務端構建分佈式存儲。

Redis Cluster是一個實現了分佈式且容許單點故障的Redis高級版本,它沒有中心節點,具備線性可伸縮的功能。

4、選型考慮

  • 若是數據量小,而且不會頻繁地增加又清空(這會致使頻繁地垃圾回收),那麼能夠選擇本地緩存。具體的話,若是須要一些策略的支持(好比緩存滿的逐出策略),能夠考慮Ehcache;如不須要,能夠考慮HashMap;如須要考慮多線程併發的場景,能夠考慮ConcurentHashMap。
  • 其餘狀況,能夠考慮緩存服務。目前從資源的投入度、可運維性、是否能動態擴容以及配套設施來考慮,咱們優先考慮Tair。除非目前Tair還不能支持的場合(好比分佈式鎖、Hash類型的value),咱們考慮用Redis。

5、設計關鍵點

何時更新緩存?如何保障更新的可靠性和實時性?

  • (被動)接收變動消息,準實時的更新。
  • (主動)設置過時時間,過時以後從DB撈數據而且回設到緩存,這個策略是對第一個策略的有力補充,解決了手動變動DB不發消息、接消息更新程序臨時出錯等問題致使的第一個策略失效的問題。經過這種雙保險機制,有效地保證了緩存數據的可靠性和實時性。

緩存是否會滿,緩存滿了怎麼辦?

對於一個緩存服務,理論上來講,隨着緩存數據的日益增多,在容量有限的狀況下,緩存確定有一天會滿的。如何應對?
① 給緩存服務,選擇合適的緩存逐出算法,好比最多見的LRU。
② 針對當前設置的容量,設置適當的警惕值,好比10G的緩存,當緩存數據達到8G的時候,就開始發出報警,提早排查問題或者擴容。
③ 給一些沒有必要長期保存的key,儘可能設置過時時間。

緩存是否容許丟失?丟失了怎麼辦?

根據業務場景判斷,是否容許丟失。若是不容許,就須要帶持久化功能的緩存服務來支持,好比Redis或者Tair。更細節的話,能夠根據業務對丟失時間的容忍度,還能夠選擇更具體的持久化策略,好比Redis的RDB或者AOF

簡單理解:

RDB持久化,把當前進程數據生成快照保存到硬盤的過程。

AOF持久化,以獨立日誌的方式記錄每次寫命令,重啓時再從新執行AOF文件中的命令達到恢復數據的目的。

6、緩存算法

緩存容量超過預設,如何踢掉「無用」的數據。

FIFO(first in first out)

先進先出策略,最早進入緩存的數據在緩存空間不夠的狀況下(超出最大元素限制)會被優先被清除掉,以騰出新的空間接受新的數據。策略算法主要比較緩存元素的建立時間。在數據實效性要求場景下可選擇該類策略,優先保障最新數據可用。

LFU(less frequently used)

最少使用策略,不管是否過時,根據元素的被使用次數判斷,清除使用次數較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數)。在保證高頻數據有效性場景下,可選擇這類策略。

LRU(least recently used)

最近最少使用策略,不管是否過時,根據元素最後一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時間。在熱點數據場景下較適用,優先保證熱點數據的有效性。與LFU的存在必定區別

                                        

                                                    圖-LRU示意圖

能夠想象,要清理哪些數據,不是由開發者決定(只能決定大體方向:以上策略算法),數據的一致性是最差的。

通常來講咱們都須要配置超過最大緩存後的更新策略(例如:LRU)以及最大內存,這樣能夠保證系統能夠繼續運行(例如redis可能存在OOM問題)(極端狀況下除外,數據一致性要求極高)

超時剔除

通常來講業務能夠容忍一段時間內(例如一個小時),緩存數據和真實數據(例如:mysql, hbase等等)數據不一致(通常來講,緩存能夠提升訪問速度下降後端負載),那麼咱們能夠對一個數據設置必定時間的過時時間,在數據過時後,再從真實數據源獲取數據,從新放到緩存中,繼續設置過時時間。一段時間內(取決於過時時間)存在數據一致性問題,即緩存數據和真實數據源數據不一致。

主動更新

具備強一致性,維護成本高。業務對於數據的一致性要求很高,須要在真實數據更新後,當即更新緩存數據。具體作法:例如能夠利用消息系統或者其餘方式(好比數據庫觸發器,或者其餘數據源的listener機制來完成)通知緩存更新。

存在的問題:若是主動更新發生了問題,那麼這條數據極可能很長時間不會更新了。

通常來講咱們須要把超時剔除和主動更新組合使用,那樣即便主動更新出了問題,也能保證過時時間後,緩存就被清除了(不至於永遠都是髒數據)。

7、緩存使用中的坑與對策

緩存粒度

假如我如今須要對視頻的信息作一個緩存,也就是須要對select * from video where id=?的每一個id在redis裏作一份緩存,這樣cache層就能夠幫助我抗住不少的訪問量(注:這裏不討論一致性和架構等等問題,只討論緩存的粒度問題)。

咱們假設視頻表有100個屬性(這個真有,有些人可能不可思議),那麼問題來了,須要緩存什麼維度呢,也就是有兩種選擇吧:

(1)cache(id)=select * from video where id=#id  

(2)cache(id)=select importantColumn1, importantColumn2 .. importantColumnN from video where id=#id  

以上這兩種方式在通用性、空間佔用和代碼維護方面均存在較大差別。

緩存粒度問題是一個容易被忽視的問題,若是使用不當,可能會形成不少無用空間的浪費,可能會形成網絡帶寬的浪費,可能會形成代碼通用性較差等狀況,必須學會綜合數據通用性、空間佔用比、代碼維護性 三點評估取捨因素權衡使用。

緩存穿透

緩存穿透是指查詢一個必定不存在的數據,因爲緩存不命中,而且出於容錯考慮, 若是從存儲層查不到數據則不寫入緩存,這將致使這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。查一個壓根就不存在的值, 若是不作兼容,永遠會查詢storage。

如何解決?

方案一

                                   

        (1) 如上圖所示,當第②步MISS後,仍然將空對象保留到Cache中(多是保留幾分鐘或者一段時間,具體問題具體分析),下次新的Request(同一個key)將會從Cache中獲取到數據,保護了後端的Storage。

        (2) 適用場景:數據命中不高,數據頻繁變化實時性高(一些亂轉業務)

        (3) 維護成本:代碼比較簡單,可是有兩個問題:

            第一是空值作了緩存,意味着緩存系統中存了更多的key-value,也就是須要更多空間(有人說空值沒多少,可是架不住多啊),解決方法是咱們能夠設置一個較短的過時時間。

            第二是數據會有一段時間窗口的不一致,假如,Cache設置了5分鐘過時,此時Storage確實有了這個數據的值,那此段時間就會出現數據不一致,解決方法是咱們能夠利用消息或者其餘方式,清除掉Cache中的數據。

方案二

bloomfilter或者壓縮filter(bitmap等等)提早攔截。

                                

                                            圖-布隆過濾器解決緩存穿透示意圖

方案三(技術分享)

存在問題的策略

                                

解決後的策略

                                

緩存雪崩

若是Cache層因爲某些緣由(宕機、cache服務掛了或者不響應了)總體crash掉了,也就意味着全部的請求都會達到Storage層,全部Storage的調用量會暴增,因此它有點扛不住了,甚至也會掛掉。

如何解決?

方案一

保證Cache服務高可用性,和飛機都有多個引擎同樣,若是咱們的cache也是高可用的,即便個別實例掛掉了,影響不會很大(主從切換或者可能會有部分流量到了後端),實現自動化運維。一致性hash算法能夠很好地解決由於cache集羣節點宕機時數據存取變化問題,具備良好的可擴展性。

方案二

其實不管是cache或者是mysql, hbase, 甚至別人的API,都會出現問題,咱們能夠將這些視同爲資源,做爲併發量較大的系統,在服務不可用或者併發量過大會對系統形成影響時,設置必定的降級、限流、隔離等策略。

無底洞問題

鍵值數據庫或者緩存系統,因爲一般採用hash函數將key映射到對應的實例,形成key的分佈與業務無關,可是因爲數據量、訪問量的需求,須要使用分佈式後(不管是客戶端一致性哈性、redis-cluster、codis),批量操做好比批量獲取多個key(例如redis的mget操做),一般須要從不一樣實例獲取key值,相比於單機批量操做只涉及到一次網絡操做,分佈式批量操做會涉及到屢次網絡io。

無底洞問題帶來的危害

  (1) 客戶端一次批量操做會涉及屢次網絡操做,也就意味着批量操做會隨着實例的增多,耗時會不斷增大。

  (2) 服務端網絡鏈接次數變多,對實例的性能也有必定影響。

用一句通俗的話總結:更多的機器不表明更多的性能,所謂「無底洞」就是說投入越多不必定產出越多。分佈式又是不能夠避免的,由於咱們的網站訪問量和數據量愈來愈大,一個實例根本坑不住,因此如何高效的在分佈式緩存和存儲批量獲取數據是一個難點。

熱點key問題

在緩存失效的瞬間,有大量線程來構建緩存(緩存的構建是須要必定時間的。(多是一個複雜計算,例如複雜的sql、屢次IO、多個依賴(各類接口)等等)),形成後端負載加大,甚至可能會讓系統崩潰。

如何解決?

方案一

使用互斥鎖(mutex key): 這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其餘線程等待構建緩存的線程執行完,從新從緩存獲取數據就能夠了。

若是是單機,能夠用synchronized或者lock來處理,若是是分佈式環境能夠用分佈式鎖就能夠了(分佈式鎖,能夠用memcache的add, redis的setnx, zookeeper的添加節點操做)

                                

                                                    圖-互斥鎖解決熱點key問題分析

方案二

"提早"使用互斥鎖(mutex key),在value內部設置1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已通過期時候,立刻延長timeout1並從新設置到cache。而後再從數據庫加載數據並設置到cache中。

方案三

「永遠不過時」

 (1) 從redis上看,確實沒有設置過時時間,這就保證了,不會出現熱點key過時問題,也就是「物理」不過時。

 (2) 從功能上看,若是不過時,那不就成靜態的了嗎?因此咱們把過時時間存在key對應的value裏,若是發現要過時了,經過一個後臺的異步線程進行緩存的構建,也就是「邏輯」過時。

從實戰看,這種方法對於性能很是友好,惟一不足的就是構建緩存時候,其他線程(非構建緩存的線程)可能訪問的是老數據,可是對於通常的互聯網功能來講這個仍是能夠忍受。

方案四

hystrix資源保護

方案五

使用mutex

如何解決:業界比較經常使用的作法,是使用mutex。簡單地來講,就是在緩存失效的時候(判斷拿出來的值爲空),不是當即去load db,而是先使用緩存工具的某些帶成功操做返回值的操做(好比Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操做返回成功時,再進行load db的操做並回設緩存;不然,就重試整個get緩存的方法。相似下面的代碼:

public String get(key) {
      String value = redis.get(key);
      if (value == null) { //表明緩存值過時
          //設置3min的超時,防止del操做失敗的時候,下次緩存過時一直不能load db
          if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //表明設置成功
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  //這個時候表明同時候的其餘線程已經load db並回設到緩存了,這時候重試獲取緩存值便可
                      sleep(50);
                      get(key);  //重試
              }
          } else {
              return value;      
          }
  }

 

8、緩存更新的模式

緩存更新模式指的是如何更新數據庫和緩存,特別是在併發環境下避免髒數據等錯誤,如下提供了一些可借鑑的更新模式(或者說是緩存更新的經常使用套路)。

Cache Aside更新模式

                                  

                                                     圖-Cache Aside更新模式

這種方式屬於比較標準的緩存更新模式,即先更新數據庫,再刪除緩存,包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個策略,在實際的系統中也推薦使用這種方式。可是這種方式理論上仍是可能存在問題。以下圖(以Redis和Mysql爲例),查詢操做沒有命中緩存,而後查詢出數據庫的老數據。此時有一個併發的更新操做,更新操做在讀操做以後更新了數據庫中的數據而且刪除了緩存中的數據。然而讀操做將從數據庫中讀取出的老數據更新回了緩存。這樣就會形成數據庫和緩存中的數據不一致,應用程序中讀取的都是原來的數據(髒數據)。可是這種併發的機率極低,由於這個條件須要發生在讀緩存時緩存失效並且有一個併發的寫操做。實際上數據庫的寫操做會比讀操做慢得多,並且還要加鎖,而讀操做必需在寫操做前進入數據庫操做,又要晚於寫操做更新緩存,全部這些條件都具有的機率並不大。可是爲了不這種極端狀況形成髒數據所產生的影響,咱們仍是要爲緩存設置過時時間。

                                  

                                                        圖-Cache Aside更新模式潛在問題分析(低機率事件)

常見的錯誤作法及緣由分析以下:

先更新數據庫,再更新緩存。這種作法最大的問題就是兩個併發的寫操做致使髒數據。兩個併發更新操做,數據庫先更新的反然後更新緩存,數據庫後更新的反而先更新緩存。這樣就會形成數據庫和緩存中的數據不一致,應用程序中讀取的都是髒數據。

                                  

                                                        圖-先更新數據庫再更新緩存錯誤分析

先刪除緩存,再更新數據庫。這個邏輯是錯誤的,由於兩個併發的讀和寫操做致使髒數據。以下圖(以Redis和Mysql爲例)。假設更新操做先刪除了緩存,此時正好有一個併發的讀操做,沒有命中緩存後從數據庫中取出老數據而且更新回緩存,這個時候更新操做也完成了數據庫更新。此時,數據庫和緩存中的數據不一致,應用程序中讀取的都是原來的數據(髒數據)。

                                

                                                       圖-先刪除緩存再更新數據庫錯誤分析

Read/Write Through更新模式

咱們能夠看到,在上面的Cache Aside套路中,咱們的應用代碼須要維護兩個數據存儲,一個是緩存(Cache),一個是數據庫(Repository)。因此,應用程序比較囉嗦。而Read/Write Through套路是把更新數據庫(Repository)的操做由緩存本身代理了,因此,對於應用層來講,就簡單不少了。能夠理解爲,應用認爲後端就是一個單一的存儲,而存儲本身維護本身的Cache。

Read Through 套路就是在查詢操做中更新緩存,也就是說,當緩存失效的時候(過時或LRU換出),Cache Aside是由調用方負責把數據加載入緩存,而Read Through則用緩存服務本身來加載,從而對應用方是透明的。

Write Through 套路和Read Through相仿,不過是在更新數據時發生。當有數據更新的時候,若是沒有命中緩存,直接更新數據庫,而後返回。若是命中了緩存,則更新緩存,而後再由Cache本身更新數據庫這是一個同步操做

Write Behind Caching更新模式

在更新數據的時候,只更新緩存,不更新數據庫,而咱們的緩存會異步地批量更新數據庫。這個設計的好處就是讓數據的I/O操做飛快無比(由於直接操做內存),異步還能夠合併對同一個數據的屢次操做,因此性能的提升是至關可觀的。可是,其帶來的問題是數據不是強一致性的,並且可能會丟失。

和Read/Write Through更新模式相似,區別是Write Behind Caching更新模式的數據持久化操做是異步的,可是Read/Write Through更新模式的數據持久化操做是同步的。

9、二級緩存

                                

10、典型使用場景舉例

場景一:同一熱賣商品高併發讀/寫請求

讀請求:本地緩存結合分佈式緩存集羣,爲縮短本地緩存和分佈式緩存之間的數據不一致窗口期,能夠引入消息隊列。

如何進行緩存的更新?

更新時主動發消息,藉助緩存更新程序進行更新;

設置key過時時間(防止消息丟失或者緩存更新程序更新失敗);

藉助DataBus實時更新(解決緩存和DB的不一致性)

寫請求:可直接在緩存中進行庫存信息操做,如何防止超賣?引入用ZK實現的分佈式鎖

防止超賣的核心在於:不容許同一商品的庫存記錄在同一時刻被不一樣的兩個數據庫事務修改。

如何作到?利用數據庫的事務鎖機制;分佈式鎖(基於ZK或者Redis)

場景二:小/大庫存商品秒殺典型架構

小庫存:數據庫樂觀鎖實現庫存信息修改;

大庫存:直接在緩存中修改庫存,使用分佈式鎖防止超賣;

場景三:緩存業務接口查詢數據避免重複的數據庫查詢

具體能夠採起切面的形式以無侵入方式實現,對接口總體返回結果進行緩存。

參考連接

http://carlosfu.iteye.com/blog/2269678(感謝大神的系列文章)

https://tech.meituan.com/cache_about.html

相關文章
相關標籤/搜索