緩存總結(二)

 

四種套路更新緩存

實時刷新緩存 

分佈式之數據庫和緩存雙寫一致性方案解析(重要)

 

 

分類

 

本地緩存(HashMap/ConcurrentHashMap、Ehcache、Guava Cache等),

緩存服務(Redis/Tair/Memcache等)。

 

使用場景

 

什麼情況適合用緩存?考慮以下兩種場景:

1、短時間內相同數據重複查詢多次且數據更新不頻繁,這個時候可以選擇先從緩存查詢,查詢不到再從數據庫加載並回設到緩存的方式。此種場景較適合用單機緩存。

2、高併發查詢熱點數據,後端數據庫不堪重負,可以用緩存來扛。

 

選型考慮

如果數據量小,並且不會頻繁地增長又清空(這會導致頻繁地垃圾回收),那麼可以選擇本地緩存。具體的話,如果需要一些策略的支持(比如緩存滿的逐出策略),可以考慮Ehcache;如不需要,可以考慮HashMap;如需要考慮多線程併發的場景,可以考慮ConcurentHashMap。

其他情況,可以考慮緩存服務。目前從資源的投入度、可運維性、是否能動態擴容以及配套設施來考慮,我們優先考慮Tair。除非目前Tair還不能支持的場合(比如分佈式鎖、Hash類型的value),我們考慮用Redis。

 

設計關鍵點

 

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

 

更新緩存的策略,需要具體問題具體分析。這裏以門店POI的緩存數據爲例,來說明一下緩存服務型的緩存更新策略是怎樣的?目前約10萬個POI數據採用了Tair作爲緩存服務,具體更新的策略有兩個:

1、接收門店變更的消息,準實時更新。

2、給每一個POI緩存數據設置5分鐘的過期時間,過期後從DB加載再回設到DB。這個策略是對第一個策略的有力補充,解決了手動變更DB不發消息、接消息更新程序臨時出錯等問題導致的第一個策略失效的問題。通過這種雙保險機制,有效地保證了POI緩存數據的可靠性和實時性。

 

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

 

對於一個緩存服務,理論上來說,隨着緩存數據的日益增多,在容量有限的情況下,緩存肯定有一天會滿的。如何應對?

 

① 給緩存服務,選擇合適的緩存逐出算法,比如最常見的LRU。

② 針對當前設置的容量,設置適當的警戒值,比如10G的緩存,當緩存數據達到8G的時候,就開始發出報警,提前排查問題或者擴容。

③ 給一些沒有必要長期保存的key,儘量設置過期時間

 

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

 

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

 

緩存被“擊穿”問題

 

對於一些設置了過期時間的key,如果這些key可能會在某些時間點被超高併發地訪問,是一種非常“熱點”的數據。這個時候,需要考慮另外一個問題:緩存被“擊穿”的問題。

概念:緩存在某個時間點過期的時候,恰好在這個時間點對這個Key有大量的併發請求過來,這些請求發現緩存過期一般都會從後端DB加載數據並回設到緩存,這個時候大併發的請求可能會瞬間把後端DB壓垮。

 

如何解決:業界比較常用的做法,是使用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;      
    }
}

 

setnx 賦值判斷原值是否存在,存在不賦值,返回0;不存在才賦值,返回1

setnx name Tom  ---返回值:0,因爲name的原有value爲zlh,存在值則不賦值。

get name  ---返回值:zlh,因爲有值,故上面賦值爲tom失敗,返回0。

 

setnx phone 18501733702   ---返回值:1,賦值成功,因爲原來不存在phone的key與value。

get phone   ---返回值:18501733702,說明上面的setnx賦值成功。

 

。。。

==================================================================

說說通用的緩存策略,有兩種,下面來點圖



 


 

 第一種方案,客戶端使用的比較多,緩存和 DB(或者文件)同步更新,服務端一般都是用第二種方案。

 

=====================================================================

那些年使用緩存踩過的坑--緩存更新策略

https://my.oschina.net/percylee/blog/903295

今天講的這個話題,我相信是衆多工程師和團隊的痛。從我剛開始工作,那時候構建本地緩存,到後續memcache, Redis的出現,到現在各種分佈式集羣的緩存,例如redis Cluster等產品的出現,緩存越來越發達和複雜了,緩存對我們的系統也越發重要,現在很難相信一個後端服務裏沒有緩存的存在。在這篇文章裏,我會和大家分享一下過去踩到的緩存坑,然後試圖給出一些解決方案,大家可以一起討論,最終拿出更好的方法。由於篇幅有限,所以這裏的緩存討論,只侷限於後端服務的緩存,並且不涉及具體的框架,對於H5,iOS和Android等前端緩存的討論,會在以後的文章裏呈現出來。

 

案例1,緩存和DB的同步更新不在同一個事務裏並且沒有重試補償機制

 

爲了減少系統間的依賴,不同系統的數據更新往往不放在同一個事務裏,採用MQ來進行通信。大家可以看下圖,後臺系統CRM更新產品數據到DB,Product系統收到異步消息通知後,更新最新數據到緩存。這是一個最常見的緩存應用場景,我相信很多團隊都是這樣用的。在這個Case裏容易出現的問題在於,如果批處理任務收到消息後服務crash掉了,緩存沒有正常更新,就出現了與DB的數據不同步,前端系統一直不能讀到最新數據,導致業務異常

  

解決方案:

1.  失敗消息一定要建立一定時間間隔的重試機制

2.  系統要有緩存更新的報警機制,方便更新失敗或者重試超時後,可以人工介入進行補償。

 

 

案例2, 同一數據被1個以上的服務執行寫操作,其中一個服務的緩存數據沒有版本控制

 

這也是兩個不同服務更新數據過程中很常見的情況,見下圖,CRM系統更新了某個用戶的Profile, 保存更新數據庫後,通過MQ通知用戶系統更新緩存,由於是異步更新延遲,在緩存更新前,用戶系統收到前端的指令,讀取了當前緩存裏的用戶數據,做了修改,並更新到DB中。出現的結果就是數據庫裏的CRM的更新被錯誤覆蓋。

  

解決方案:

緩存裏的數據有一個標誌位可以作爲更新數據庫數據的依據(Update_time or Version), 如果緩存裏數據時間與數據庫時間不能匹配,意味着另外一個服務更新了該數據,那麼就先從DB裏讀取最新數據版本,然後在新版本上提交數據。

  

案例3, 併發查詢緩存中同一數據,如果緩存沒命中,導致DB瞬時被打爆做促銷活動的時候,存在大量用戶的併發訪問某一個特定商品,該商品數據緩存失效,或者做了數據更改,但是對應緩存還沒有更新,那麼所有這些訪問將同時直接被作用到DB上。

  

解決方案:

做一個計數器或者鎖(沒有特別複雜邏輯的話,可以直接用HashMap),如果發現某個KEY緩存沒有命中,那麼在計數器+1, 然後訪問數據庫,拿到結果更新緩存,清理掉計數器中的key。 在這個過程中,如果有第二個線程或者更多的線程需要訪問這個KEY時,發現計數器的值>1 或者被加鎖, 那麼wait, 直到計數器清理掉,當然,這個技術器閾值是可以在配置文件裏配置的,不一定是1。

  

案例4, 緩存沒有設置默認值,被攻擊,緩存一直保持在被“穿透”狀態

這個情況,和案例3比較類似,都是緩存無法命中,但不一樣的地方在於,數據的KEY值是無法控制的所以沒法簡單的用計數器和鎖來處理, 比方,被人爲攻擊,製造的大量的無效userID訪問。

 

解決方案:

所有沒有在緩存的KEY,全部分配一個默認VALUE “UNKOWN-KEY” ,具體是什麼情況下,將默認值分配給沒有命中的KEY, 這個可以根據自己的業務系統來定,比方說,可以根據特定的IP段,或者沒有命中的總次數等,然後我們就可以決定是否繼續訪問DB還是直接返回默認值給前端,拒絕本次數據訪問。這種做法的核心在於,每次數據訪問,都會有緩存結果返回,根據系統的情況來決定是否要進一步訪問DB。

  

總結,今天列舉的這幾個案例,歸納起來,可以總結爲以下幾點:

 

1. 保證緩存同步

2. 減少緩存併發

3. 杜絕緩存穿透

  

緩存與背後的DB是相互依存的關係,緩存系統的設計原則,就是將訪問的異常處理或者壓力盡可能的前置處理掉,將DB還原成它最初本來的存儲功能

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章