2020国产成人精品视频,性做久久久久久久久,亚洲国产成人久久综合一区,亚洲影院天堂中文av色

分享

再談緩存的穿透、數(shù)據(jù)一致性和最終一致性問(wèn)題

 xujin3 2018-06-17

首先回顧一下之前講了什么:

  • 借鑒 Spring Cache 的思想,使用 AOP + Annotation 等技術(shù)將緩存管理與業(yè)務(wù)邏輯之間進(jìn)行解耦;

  • 使用 CacheWrapper 對(duì)緩存數(shù)據(jù)進(jìn)行“包裝”,不僅能方便獲取緩存的 TTL 值,并且能解決緩存穿透問(wèn)題;

  • 可以 Spring EL、Ognl、JavaScript 等表達(dá)式,進(jìn)行緩存動(dòng)態(tài)管理,比如:生成緩存 Key、緩存時(shí)間以及判斷是否進(jìn)行緩存等;

  • 分布式緩存服務(wù)器 (如 Redis、Memcached) 沒(méi)有命名空間,而且對(duì)鍵名沒(méi)有強(qiáng)制要求,可以使用“命名空間”(namespace)防止鍵沖突,增強(qiáng)項(xiàng)目的可維護(hù)性;

  • 使用“拿來(lái)主義機(jī)制”、“自動(dòng)加載機(jī)制 (確切的說(shuō)是自動(dòng)刷新)”以及異步刷新等功能減少并發(fā)回源、并發(fā)寫緩存;

  • 顯示“實(shí)時(shí)性”要求比較高,但又不易于反向生成緩存 Key 的數(shù)據(jù),可以使用 Redis 的 hash 表進(jìn)行緩存。當(dāng)數(shù)據(jù)發(fā)生變更時(shí),可以直接刪除整個(gè) hash 表,來(lái)達(dá)到實(shí)時(shí)性的要求;

  • 在事務(wù)環(huán)境下,使用 @CacheDeleteTransactional 注解,實(shí)現(xiàn)事務(wù)提交后,主動(dòng)刪除相關(guān)的緩存數(shù)據(jù),以緩解數(shù)據(jù)不一致問(wèn)題。

具體可以閱讀之前的文章,下面補(bǔ)充三個(gè)方面。

緩存穿透問(wèn)題

緩存穿透是指查詢一個(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)的處理。

數(shù)據(jù)一致性問(wèn)題進(jìn)行補(bǔ)充

一些初學(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 listByCondition(UserCondition condition);    /**    * 添加用戶信息    **/    @CacheDelete({ @CacheDeleteKey(value = ''user-byname-' + #args[0].name') })    void addUser(UserDO user);    /**    * 更新用戶信息    **/    @CacheDelete({ @CacheDeleteKey(value = ''user-byid-' + #args[0].id') })    void updateUser(UserDO user);    /**    * 根據(jù)用戶 ID 刪除用戶記錄    **/    @CacheDelete({ @CacheDeleteKey(value = ''user-byid-' + #args[0]') })    void deleteUserById(Long id);}

假設(shè) alice 登錄后馬上進(jìn)行修改密碼,并重新登錄驗(yàn)證新密碼是否生效:

  • 1、alice 登錄時(shí),調(diào)用 getUserByName 方法,獲取 User 數(shù)據(jù),進(jìn)行登錄驗(yàn)證。這時(shí)會(huì)緩存數(shù)據(jù):key 為:user-byname-alice;value 為:{'id':1, 'name':'alice', 'password':'123456', 'status': 1}。

  • 2、此時(shí)又有人調(diào) getUserById(1) 方法,會(huì)在緩存中增加數(shù)據(jù),key 為:user-byid-1,value 為:{'id':1, 'name':'alice', 'password':'123456', 'status': 1}。此時(shí)緩存中 user-byname-alice 和 user-byid-1 這兩個(gè)緩存 key 對(duì)應(yīng)的數(shù)據(jù)完全一樣,即是同一數(shù)據(jù),被緩存了多次。

  • 3、alice 修改登錄密碼 (調(diào)用 updateUser 方法),修改數(shù)據(jù)庫(kù)中數(shù)據(jù)的同時(shí)刪除 user-byid-1 的緩存數(shù)據(jù),但是沒(méi)有刪除 user-byname-alice 的數(shù)據(jù)。

  • 4、alice 重新登錄,想驗(yàn)證新密碼是否生效時(shí),驗(yàn)證不通過(guò)。

問(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 listIdsByCondition(UserCondition condition);    /**     * 添加用戶信息     * @param user     */    @CacheDelete({        @CacheDeleteKey(value = ''userid-byname-' + #args[0].name')    })    int addUser(UserDO user);    /**     * 更新用戶信息     * @param user     * @return     */    @CacheDelete({        @CacheDeleteKey(value=''user-byid-' + #args[0].id', condition='#retVal > 0')    })    int updateUser(UserDO user);    /**    * 根據(jù)用戶 id 刪除用戶記錄    **/    @CacheDelete({        @CacheDeleteKey(value = ''user-byid-' + #args[0]', condition='#retVal > 0')    })    int deleteUserById(Long id);}@Service@Transactional(readOnly=true)public class UserServiceImpl implements UserService {    @Autowired    private UserMapper userMapper;    @Override    public UserDO getUserById(Long id) {        return userMapper.getUserById(id);    }    @Override    public List listByCondition(UserCondition condition) {        List list = new ArrayList<>();        List ids = userMapper.listIdsByCondition(condition);        if(null != ids && ids.size() > 0) {            for(Long id : ids) {                list.add(userMapper.getUserById(id));            }        }        return list;    }    @Override    @CacheDeleteTransactional    @Transactional(rollbackFor=Throwable.class)    public void register(UserDO user) {        Long userId = userMapper.getUserIdByName(user.getName());        if(null != userId) {           throw new RuntimeException('用戶名已被占用');        }        userMapper.addUser(user);    }    @Override    public UserDO doLogin(String name, String password) {        Long userId = userMapper.getUserIdByName(name);        if(null == userId) {            throw new RuntimeException('用戶不存在!');        }        UserDO user = userMapper.getUserById(userId);        if(null == user) {            throw new RuntimeException('用戶不存在!');        }        if(!user.getPassword().equals(password)) {            throw new RuntimeException('密碼不正確!');        }        return user;    }    @Override    @CacheDeleteTransactional    @Transactional(rollbackFor=Throwable.class)    public void updateUser(UserDO user) {        userMapper.updateUser(user);    }    @Override    @CacheDeleteTransactional    @Transactional(rollbackFor=Throwable.class)    public void deleteUserById(Long userId) {        userMapper.deleteUserById(userId);    }}

通過(guò)上面代碼可看出:

  • 1、緩存操作與業(yè)務(wù)邏輯解耦后,代碼的維護(hù)也變得更加方便;

  • 2、只有 getUserById 方法的緩存是直接緩存用戶數(shù)據(jù),其它地方只緩存用戶 ID。數(shù)據(jù)更新時(shí),就不需要再關(guān)心其它數(shù)據(jù)也要同步更新的問(wèn)題了,更好地保證了數(shù)據(jù)的一致性。

細(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ù)據(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 的解決方案。如下圖所示:

流程如下:

  • 1、更新數(shù)據(jù)庫(kù)數(shù)據(jù);

  • 2、刪除緩存中的數(shù)據(jù),可此時(shí)緩存服務(wù)出現(xiàn)不可用情況,造成無(wú)法刪除緩存數(shù)據(jù);

  • 3、當(dāng)刪除緩存數(shù)據(jù)失敗時(shí),將需要?jiǎng)h除緩存的 Key 發(fā)送到消息隊(duì)列 (MQ) 中;

  • 4、應(yīng)用自己消費(fèi)需要?jiǎng)h除緩存 Key 的消息;

  • 5、應(yīng)用接收到消息后,刪除緩存,如果刪除緩存確認(rèn) MQ 消息被消費(fèi),如果刪除緩存失敗,則讓消息重新入隊(duì)列,進(jìn)行多次嘗試刪除緩存操作。

方案二,基于 Canal 的解決方案。如下圖所示:

流程如下:

  • 1、更新數(shù)據(jù)庫(kù)數(shù)據(jù);

  • 2、MySQL 將數(shù)據(jù)更新日志寫入 binlog 中;

  • 3、Canal 訂閱 & 消費(fèi) MySQL binlog,并提取出被更新數(shù)據(jù)的表名及 ID;

  • 4、調(diào)用應(yīng)用刪除緩存接口;

  • 5、刪除緩存數(shù)據(jù);

  • 6、Redis 不可用時(shí),將更新數(shù)據(jù)的表名及 ID 發(fā)送到 MQ 中;

  • 7、應(yīng)用接收到消息后,刪除緩存,如果刪除緩存確認(rèn) MQ 消息被消費(fèi),如果刪除緩存失敗,則讓消息重新入隊(duì)列,進(jìn)行多次嘗試刪除緩存操作,直到緩存刪除成功為止。

像電商詳情頁(yè)這種高并發(fā)的場(chǎng)景,要盡量避免用戶請(qǐng)求回源到數(shù)據(jù)庫(kù),所以會(huì)把數(shù)據(jù)都持久化到 Redis 中,那么相應(yīng)的緩存架構(gòu)也要做些調(diào)整。

流程如下:

  • 1、更新數(shù)據(jù)庫(kù)數(shù)據(jù);

  • 2、MySQL 將數(shù)據(jù)更新日志寫入 binlog 中;

  • 3、Canal 訂閱 & 消費(fèi) MySQL binlog,并提取出被更新數(shù)據(jù)的表名及 ID;

  • 4、將更新數(shù)據(jù)的表名及 ID 發(fā)送到 MQ 中;

  • 5、應(yīng)用訂閱 & 消費(fèi)數(shù)據(jù)更新消息;

  • 6、從數(shù)據(jù)庫(kù)中拉取最新的數(shù)據(jù);

  • 7、更新緩存數(shù)據(jù),如果更新緩存失敗,則讓消息重新入隊(duì)列,進(jìn)行多次嘗試更新緩存操作,直到緩存更新成功為止。

此方案中,把數(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ù)。

More

重大革新!Dubbo 3.0來(lái)了

面對(duì)緩存,有哪些問(wèn)題需要思考?

其它

隨著互聯(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。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多