代碼評審-如何保證緩存與數據庫的讀寫一致性?

咱們從近期代碼評審過程當中的一段代碼,開始探討緩存和數據庫的一致性問題。html

探討前置

通常來講,使用緩存主要爲了提高應用性能和下降DB的直接負載,從場景上來講能夠接受最終一致性方案, 若是業務場景要求 「緩存+數據庫」 必須保持強一致性的話,那麼須要使用同步方案,好比排它鎖或者隊列機制+數據庫事務處理 這樣的話影響系統可用性,簡單狀況下可使用....仍是另選方案吧java

業務場景

  1. 商品系統的Cache和DB數據(或者RPC操做庫存應用),大體包含商品基本屬性、庫存、價格,
  2. 業務特性:讀多寫少,同一商品id,基本屬性修改併發少,庫存修改併發多(下單量大的時候庫存操做比較頻繁)
  3. 緩存操做模式上,採用Cache Aside Pattern(圖片來源
    Cache Aside Pattern
    Cache Aside Pattern
  4. 具體使用狀況的僞代碼
public Ware getById(long id) {
        Ware ware = Cache.get(id);
        if (ware != null) {
            return ware;
        }
        ware = Db.get(id);
        if(ware != null){
        	//緩存時間12小時,根據具體業務調整
            Cache.put(ware,60*60*12);
        }
        return ware;
}

public void update(Param param) {
        Db.update(param);
        Cache.del(param.getId());
}
//Cache
public void del(long id) {
		//異常 靜默
        CacheClient.expire("key1"+id, 0);
        CacheClient.expire("key2"+ id, 0);
}
複製代碼

問題

先說說這段代碼已經考慮到的問題,也是使用Cache Aside Pattern的好處git

  1. Q: update 先執行了更新DB,而後刪除cache,爲何不能先刪cache,再更新DB? A: 考慮到 getById 和 update併發執行,更新先刪,可是查詢併發放入,致使cache裏爲髒數據(讀多寫少使這種狀況更容易出現)
  2. Q: update 先執行了更新DB,而後刪除cache,爲何不能直接更新cache,這樣也能夠避免熱點數據致使緩存擊穿問題?

A: 1. 考慮到 update 方法自己併發執行,Db.update和Cache.update不是原子操做,會出現先更新DB的後更新cache 時序不一致問題(庫存修改存在併發狀況,並要求時序一致性)github

A: 2. 商品的緩存數據可能包含多維度好比庫存和價格,這兒更新了庫存一個字段,若是更新緩存須要查詢多表數據聚合放置緩存shell

A: 3. 這次更新的商品可能不被查詢使用,好比冷數據,採用查的時候緩存起到了懶加載效果數據庫

A: 4. 緩存擊穿問題,這兒的更新是基於單個商品,通常狀況可忽略,請求量若是特別高,好比秒殺商品須要更改緩存結構和特殊的處理方式(好比版本替換機制,隊列扣減機制等等,後續有機會bob再詳解...)緩存

未考慮到的問題架構

  1. 緩存操做使用異常靜默,這是查詢時候異常降級的思路,可是在更新或者刪除的時候使用,由於Db.update和Cache.update不是原子操做, 若是發生異常靜默,會致使緩存髒數據的出現,若是出現了髒數據,除了等待過時,還能怎麼辦?
  2. 緩存刪除中咱們看到會操做多個key(僞代碼中2個),若是其中1個成功,1個失敗,partial failures,用戶看到的數據一半對,一半錯...怎麼辦?
  3. 在update方法併發執行時,多個請求依然有可能出現時序不一致致使的問題,好比先更新的後放置緩存。
  4. 在update和查詢並行的狀況下,查詢接口從DB中查詢出數據準備放置緩存,可是GC暫停,接着update刪除緩存,查詢恢復放置緩存,極端狀況也可能出現髒數據

解決思路

  1. 根據場景設置合適的緩存過時時間,即便不一致,也只是緩存過時時間內的不一致,過時時間越短,數據一致性越高,可是查數據庫就會越頻繁
  2. 爲了保持時序一致性能夠採用版本化或者加鎖機制(影響吞吐量)
  3. 爲了達到最終一致性咱們能夠引入消息隊列來做補償,在更新後咱們不刪緩存而是發送消息來異步更新(技術複雜性提升)
    消息補償
  4. 採用binlog+消息隊列(項目目前使用方案),按照時序解析binlog,發送到消息隊列中(使用順序隊列,延遲必定時間消費),而後業務系統順序消費刪除緩存,這樣能起到最終一致性,順序一致性 由於binlog順序解析而且發送到順序隊列中,因此業務上能夠保證順序一致性,若是刪除緩存失敗能夠繼續重試, 爲何要延遲必定時間消費呢,這是爲了保證查詢和刪除緩存併發會出現髒數據,由於延遲了必定時間,這段時間內查詢方法應完成,而後再刪除,就提升了一致性的可能
  5. 從業務上將緩存動靜隔離(好比將庫存做爲單獨緩存key和基礎屬性分開處理)、熱點隔離(好比秒殺商品採用特殊處理方式)

後續

看到最後咱們能夠發現與其說是保證了一致性,不如說咱們是在 提升緩存一致性併發

從上面的業務使用場景結合問題分析,咱們也能夠看出在不一樣的場景下爲了達到不一樣的效果(一致性要求、吞吐、併發)咱們有不一樣的方案,這些方案的選擇離不開場景,但同時咱們也要結合技術複雜度和團隊技術水平、開發維護成本綜合考慮來選擇適合團隊的方案。異步

參考

Redis使用總結(1、幾點使用心得)

高併發架構系列:Redis緩存和MySQL數據一致性方案詳解

Facebook use delete to remove the key

Improving cache consistency

相關文章
相關標籤/搜索