1. 程式人生 > >高併發下快取和資料庫一致性問題(更新淘汰快取不得不注意的細節)

高併發下快取和資料庫一致性問題(更新淘汰快取不得不注意的細節)

 快取和資料庫一致性問題

本文討論的背景是,cache如memcache,redia等快取來快取資料庫讀取出來的資料,以提高讀效能,如何處理快取裡的資料和資料庫資料的一致性是本文討論的內容:

正常的快取步驟是:

1查詢快取資料是否存在,2不存在即查詢資料庫,3將資料新增到快取同時返回結果,4下一次訪問發現快取存在即直接返回快取資料。那麼當更新資料庫資料的時候,該如果更新快取呢,至少要考慮儘量短時間的一致性,這個看業務需求,比如使用者資訊快取時間越短越好,比如排行榜可能是一天更新一次,本文純技術討論,就是儘量縮短非一致性的時間以此來學習思路。

1、當更新資料庫時候,快取應該如何更新     
    1.1、更新快取VS淘汰快取

     答:更新快取很直接,但是涉及到本次更新的資料結果需要一堆資料運算(例如更新使用者餘額,可能需要先看看有沒有優惠券等),複雜度就增加了。而淘汰快取僅僅會增加一次cache miss,代價可以忽略,所以建議淘汰快取

   1.2、先淘汰後寫資料庫vs先寫資料庫後淘汰

    答: 先寫後淘汰,如果淘汰失敗,cache裡一直是髒資料

           先淘汰後寫,下次請求的時候快取就會miss hit一次,這個代價是可以忽略的,(如果淘汰失敗return false)

           綜合比較統,推薦先淘汰快取再寫資料庫

          下次請求直接從資料庫取然後再寫在快取裡。(當然這裡可能會大併發一起擊穿(通過下面1.3的方式可以解決),還有在淘汰快取再寫資料庫的這一瞬間,再來一個讀取請求,這個讀取比上一個請求的寫先完成,那麼就會出現髒資料。網上有人說 修改資料庫的連線池方法,就是對於同一個ID的資料請求,比如query(id),edit(id)都使用同一個連線物件,這樣來保證先來的先完成,貌似還是挺複雜的,關於後來的讀取請求先與先來的寫請求完成,只能通過這樣的序列方式執行)

           關於髒資料,如果需要強一致性

           1.2.1、可以通過資料庫無論是讀或寫操作都是通過一個請求db connection連線完成(目的是序列),這樣就需要修改連線池

           1.2.2、可以採用更新快取而不是淘汰快取,前提是更新的代價比較低

           1.2.3、可以先更新資料庫再淘汰快取(更新的原則是誰影響小先更新誰,此處倒著來了,一般推薦先更新資料庫再淘汰緩   存),不過一般情況,淘汰快取失敗的可能性很小,可以以快取處理100%不失敗為前期。

           1.2.4、雙淘汰發,即:淘汰快取-更新資料庫-淘汰快取,可以儘量減少髒資料的留存時間。

           1.2.5、以上實現起來,要麼極短時間的不一致要麼一致性代價比較高,實際專案我會這樣處理,更新資料庫的地方和讀取的地方上同樣key的分散式鎖,這樣就能保證,先操作(或讀或寫)資料的先獲得結果,實際中這樣的強一致需求比較少,參考思路即可。

           當然資料既然都快取起來了,絕大部分都不要求強一致性,為了儘可能的縮短一致性的時間,可以如下處理:

           1.2.6、非同步訊息匯流排esb更新法,即:修改資料庫往訊息匯流排裡傳送一個訊息,在接收端去處理這個訊息更新快取,缺點是有程式碼入侵

           1.2.7,非同步binlog掃描更新法,增量的去掃描binlog中的修改記錄,符合條件的更新快取,相比訊息匯流排法沒有程式碼入侵

    1.3、在1.2快取miss hit的時候,此時大併發請求這個過程,會出現什麼異常

     答:快取擊穿(關於快取丟失導致雪崩擊穿參考:https://blog.csdn.net/zeb_perfect/article/details/54135506)

           主要是熱點key的請求或者一直沒寫快取成功會出現這種情況,解決方案網上很多,這裡我寫下的我解決方案:

            虛擬碼:

            //主要是資料庫查詢的時候序列,會帶來毫秒級的卡頓,綜合複雜度效能等,推薦此方法  

String json="";
cache=redis.get(key);
if(cache is not null)
    { 
        return cache        
    }
}
else
{
    lock();//如果分散式部署,這裡有用分散式鎖哦,分散式鎖來鎖住資料庫查詢請求,應儘量避免鎖,這樣程式就是單執行緒達不到併發要求,這裡使用鎖主要是因 極少概率會穿透到資料庫,鎖一點點時間不影響效能
//為什麼再來一次判斷,自行想象下高併發場景下
    cache=redis.get(key);
    if(cache is not null)
    {
       return cache;
    }
    data=json=server.Query(Sql);
    redis.set(data,key);
    unlock();
    return data;
}

分散式鎖:

//分散式鎖  
String get(String key) {    
   String value = redis.get(key);    
   if (value  == null) {    
    if (redis.setnx(key_mutex, "1")) {    
        // 3 min timeout to avoid mutex holder crash    
        redis.expire(key_mutex, 3 * 60)    
        value = db.get(key);    
        redis.set(key, value);    
        redis.delete(key_mutex);    
    } else {    
        //其他執行緒休息50毫秒後重試    
        Thread.sleep(50);    
        get(key);    
    }    
  }    
}  

備註:設計快取的時候,尤其是熱點key過期的時候 需要考慮擊穿,以及雪崩,穿透等情形對下游DB的併發請求帶來的影響

https://blog.csdn.net/zeb_perfect/article/details/54135506

https://blog.csdn.net/wang0112233/article/details/79558612

2、1中的方案都是在單主庫的環境下討論的,如果涉及到主從資料庫如何處理呢?

      一般主從都是 寫主讀從,寫主後,立馬讀從,而從還沒有更新,有一定的延遲,這個延遲時間我們經驗總結暫定500ms,超過500ms超時返回。如果主從的話涉及到強一致性更復雜,這裡暫且按照弱一致性的需求,只是要儘量的縮短非一致性的時間

      2.1、淘汰快取,修改完資料,thread.wait(500s),再次淘汰快取,下次讀從庫就是最新資料,在期間有可能500ms的舊資料

      2.2、1.2.6和1.2.7一樣,只是在處理更新快取的時候加上500ms的延遲時間,以此來保證從庫更新完成,再更新快取

3、主從一致性,即修改完立馬就要讀取到最新的資料(本方案不涉及到快取的同步,如果涉及可以結合全篇思路去設計) 方案如下:

      3.1、半同步複製,理應資料庫原生的功能,等從庫同步完才返回結果,缺點吞吐量下降

      3.2、強制讀主庫,部分有一致性要求的,程式碼中強制讀取主庫,這個時候一定要結合好快取,提高讀效能

      3.3、資料庫中介軟體,一般情況資料庫中介軟體把寫路由到主,把讀路由到從,此處是記錄所以寫的key,在500ms內讀主庫,超過500ms後讀從庫,能保證絕對的一致性,缺點是成本比較高

      3.4、快取記錄寫key法,發生寫操作,把此key記錄在快取裡過期時間500ms,key存在表示剛更新過,還沒完成同步,強制路由到主庫,沒有則路由到從庫

     關於強一致的需求,現實是不多的,本身就使用cache了還要求強一致,貌似本末倒置,但是不排除特殊情況的存在,主要是思路和大家分享。

     手碼確實挺累的,如有更好方案或者疑問可以加QQ:1609170062 討論