首先回顧一下之前講了什么:
具體可以閱讀之前的文章,下面補充三個方面。 緩存穿透是指查詢一個根本不存在的數(shù)據(jù),緩存和數(shù)據(jù)源都不會命中。出于容錯的考慮,如果從數(shù)據(jù)層查不到數(shù)據(jù)則不寫入緩存,即數(shù)據(jù)源返回值為 null 時,不緩存 null。緩存穿透問題可能會使后端數(shù)據(jù)源負載加大,由于很多后端數(shù)據(jù)源不具備高并發(fā)性,甚至可能造成后端數(shù)據(jù)源宕掉。 AutoLoadCache 框架一方面使用“拿來主義”機制,減少回源請求并發(fā)數(shù)、降低數(shù)據(jù)源的負載,另一方面默認將 null 值使用 CacheWrapper“包裝”后進行緩存。但為了避免數(shù)據(jù)不一致及不必要的內(nèi)存占用,建議縮短緩存過期時間,并增加相關(guān)的主動刪除緩存功能,如下面代碼所示 (代碼一): public interface UserMapper { /** * 根據(jù)用戶 id 獲取用戶信息 **/ @Cache(expire = 1200, expireExpression='null == #retVal ? 120: 1200', key = ''user-byid-' + #args[0]') UserDO getUserById(Long userId); /** * 更新用戶信息 **/ @CacheDelete({ @CacheDeleteKey(value = ''user-byid-' + #args[0].id') }) void updateUser(UserDO user);} 通過 expireExpression 動態(tài)設(shè)置緩存過期時間,上面例子中,getUserById 方法如果沒有返回值,緩存時間為 120 秒,有數(shù)據(jù)時緩存時間為 1200 秒。調(diào)用 updateUser 方法時,刪除'user-byid-{userId}'的緩存。 還要記住一點,數(shù)據(jù)層出現(xiàn)異常時,不能捕獲異常后直接返回 null 值,而是盡量把異常往外拋,讓調(diào)用者知道到底發(fā)生了什么事情,以便于做相應(yīng)的處理。 一些初學者使用 AutoloadCache 框架進行管理緩存時,以為在原有的代碼中直接加上 @Cache、@CacheDelete 注解后,就完事了。其實并沒這么簡單,不管你有沒有使用 AutoloadCache 框架,都需要考慮同一份數(shù)據(jù)是否會在多次緩存后,造成緩存無法更新的問題。盡量做到 允許修改的數(shù)據(jù)只被緩存一次,而不被多次緩存,保證數(shù)據(jù)更新時,緩存數(shù)據(jù)也能被同步更新,或者方便做主動清除,換句話說就是盡量緩存不可變數(shù)據(jù)。而如果數(shù)據(jù)更新頻率足夠低,那么在業(yè)務(wù)允許的情況下,則可以直接使用最終一致性方案。下面舉個例子說明這個問題: 業(yè)務(wù)背景:用戶表中有 id, name, password, status 字段,name 字段是登錄名。并且注冊成功后,用戶名不允許被修改。 假設(shè)用戶表中的數(shù)據(jù),如下: 下面是 Mybatis 操作用戶表的 Mapper 類 (代碼二): public interface UserMapper { /** * 根據(jù)用戶 id 獲取用戶信息 **/ @Cache(expire = 1200, key = ''user-byid-' + #args[0]') UserDO getUserById(Long userId); /** * 根據(jù)用戶名獲取用戶信息 **/ @Cache(expire = 1200, key = ''user-byname-' + #args[0]') UserDO getUserByName(String name); /** * 根據(jù)動態(tài)組合查詢條件,獲取用戶列表 **/ @Cache(expire = 1200, key = ''user-list-' + #hash(#args[0])') List 假設(shè) alice 登錄后馬上進行修改密碼,并重新登錄驗證新密碼是否生效:
問題已經(jīng)清楚了,那該如何解決呢? 我們都知道 ID 是數(shù)據(jù)的唯一標識,而且它是不允許修改的數(shù)據(jù),不用擔心被修改,所以可以對它重復緩存,那么就可以使用 id 作為中間數(shù)據(jù)。為了讓大家更好地理解,將上面的代碼進行重構(gòu) (代碼三): public interface UserMapper { /** * 根據(jù)用戶 id 獲取用戶信息 * @param id * @return */ @Cache(expire=3600, expireExpression='null == #retVal ? 600: 3600', key=''user-byid-' + #args[0]') UserDO getUserById(Long id); /** * 根據(jù)用戶名獲取用戶 id * @param name * @return */ @Cache(expire = 1200, expireExpression='null == #retVal ? 120: 1200', key = ''userid-byname-' + #args[0]') Long getUserIdByName(String name); /** * 根據(jù)動態(tài)組合查詢條件,獲取用戶 id 列表 * @param condition * @return **/ @Cache(expire = 600, key = ''userid-list-' + #hash(#args[0])') List 通過上面代碼可看出:
細心的讀者也許會問,如果系統(tǒng)中有一個查詢 status = 1 的用戶列表 (調(diào)用上面的 listIdsByCondition 方法),而這時把這個列表中的用戶 status = 0,緩存中的并沒有把相應(yīng)的 id 排除,那么不就會造成業(yè)務(wù)不正確了嗎?這個主要是要考慮系統(tǒng)可接受這種不正確情況存在多久。這時就需要前端加上相應(yīng)的邏輯來處理這種情況。比如,電商系統(tǒng)中,某商口被下線了,可有些列表頁因緩存沒及時更新,仍然顯示在列表中,但在進入商品詳情頁或者點擊購買時,一定會有商品已下線的提示。 通過上面例子我們發(fā)現(xiàn),需要根據(jù)業(yè)務(wù)特點,思考不同場景下數(shù)據(jù)之間的關(guān)系,這樣才能設(shè)計出好的緩存方案。 有興趣的讀者可以思考一下,上面例子中,如果用戶名允許修改的情況下,相應(yīng)的代碼要做哪些調(diào)整? 在數(shù)據(jù)更新時,如果出現(xiàn)緩存服務(wù)不可用的情況,造成無法刪除緩存數(shù)據(jù),當緩存服務(wù)恢復可用時,就可能出現(xiàn)緩存數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)不一致的情況。為了解決此問題筆者提供以下幾種方案: 方案一,基于 MQ 的解決方案。如下圖所示: 流程如下:
方案二,基于 Canal 的解決方案。如下圖所示: 流程如下:
像電商詳情頁這種高并發(fā)的場景,要盡量避免用戶請求回源到數(shù)據(jù)庫,所以會把數(shù)據(jù)都持久化到 Redis 中,那么相應(yīng)的緩存架構(gòu)也要做些調(diào)整。 流程如下:
此方案中,把數(shù)據(jù)更新的消息發(fā)送到 MQ 中,主要避免數(shù)據(jù)更新洪峰時,造成從數(shù)據(jù)庫獲取數(shù)據(jù)壓力過大,起到削峰的作用。通過 Canal 就可以把最新數(shù)據(jù)發(fā)到 MQ 以及應(yīng)用,為什么還要從數(shù)據(jù)庫中獲取最新數(shù)據(jù)?因為當消息過多時,MQ 消息可能出現(xiàn)積壓,應(yīng)用收到時可能已經(jīng)是“舊”消息,通過去數(shù)據(jù)庫取一次,以保證緩存數(shù)據(jù)是最新的。 總的來說以上幾種方案都借助 MQ 重復消費功能,以實現(xiàn)緩存數(shù)據(jù)最終得以更新。為了避免 MQ 消息積壓,前兩種方案都是先嘗試直接刪除緩存,當出現(xiàn)異常情況時,才使用 MQ 進行補償處理。方案一實現(xiàn)比較簡單,但如果 MQ 出現(xiàn)故障時,還是會造成一些數(shù)據(jù)不一致的情況,而方案二因為增加了刪除緩存流程,延長了緩存數(shù)據(jù)的更新時間,但是可以彌補方案一中因 MQ 故障造成數(shù)據(jù)不一致的情況:Canal 可以重新訂閱和消費 MQ 故障后的 binlog,從而增加了一重保障。 而第三種方案中 Redis 不僅僅是做緩存用了,還有持久化的功能在里面,所以采用更新緩存而不是刪除緩存保證 Redis 的數(shù)據(jù)是最新的。 本文首發(fā)于作者公眾號:京西(ID:tech_top)。 邱家榆,隨行付基礎(chǔ)平臺架構(gòu)師,專注于分布式計算及微服務(wù)。 隨著互聯(lián)網(wǎng)業(yè)務(wù)的飛速發(fā)展,系統(tǒng)動輒要支持億級流量壓力,架構(gòu)設(shè)計不斷面臨新的挑戰(zhàn)。海量系統(tǒng)設(shè)計、容災、健壯性,架構(gòu)師要考慮多方面的需求做出權(quán)衡。不如來聽聽國內(nèi)外知名互聯(lián)網(wǎng)公司的架構(gòu)師分享架構(gòu)設(shè)計背后的挑戰(zhàn)與問題解決之道。 QCon 北京 2018 目前 8 折報名中,立減 1360 元,有任何問題歡迎咨詢購票經(jīng)理 Hanna,電話:15110019061,微信:qcon-0410。 |
|