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