緩存更新的策略
1、先淘汰緩存,再更新數據庫
public void updateData1(DataObject dataObject) {
//第一步,淘汰緩存
deleteFromCache(dataObject.getId());
//第二步,操作數據庫
updateFromDB(dataObject);
}
問題分析:
兩個併發操作,操作時序如下:
-
1、更新請求刪除了緩存
-
2、查詢請求沒有命中緩存
-
3、查詢請求從數據庫中讀出數據放入緩存
-
4、更新請求更新了數據庫中的數據。
於是,在緩存中的數據還是老的數據,導致緩存中的數據是髒的
2、先更新數據庫,再修改緩存
public void updateData3(DataObject dataObject) {
//第一步,更新數據庫
updateFromDB(dataObject);
//第二步,更新緩存
setDataToCache(dataObject.getId(), dataObject);
}
問題分析:
兩個併發更新操作,操作時序
-
1、請求1更新數據庫
-
2、請求2更新數據庫
-
3、請求2set緩存
-
4、請求1set緩存
數據庫中的數據是請求2設置的,而緩存中的數據是請求1設置的,數據庫與緩存的數據不一致
3、先更新數據庫,再淘汰緩存
public void updateData2(DataObject dataObject) {
//第一步,操作數據庫
updateFromDB(dataObject);
//第二步,淘汰緩存
deleteFromCache(dataObject.getId());
}
這是經典的Cache Aside Pattern
問題分析:
第一步數據庫更新成功,第二步緩存操作失敗,會導致緩存中的是髒數據,原子性無法保證。
兩個併發操作,操作時序如下:
1、查詢請求沒有命中緩存
2、查詢請求從數據庫中讀出數據
3、更新請求更新了數據庫
4、更新請求刪除緩存
5、查詢請求把讀取到的老數據放入緩存
於是,在緩存中的數據還是老的數據,導致緩存中的數據是髒的
但這個case理論上會出現,不過實際上出現的概率可能非常低,
因爲這個條件需要發生在讀緩存時緩存失效,而且併發着有一個寫操作。
而實際上數據庫的寫操作會比讀操作慢得多,而且還要鎖表,
而讀操作必需在寫操作前進入數據庫操作,而又要晚於寫操作更新緩存,所有的這些條件都具備的概率基本並不大。
4、先更新數據庫,再淘汰緩存,並且保證原子性
@Transactional
public void updateData(DataObject dataObject) {
//第一步,操作數據庫
updateFromDB(dataObject);
//第二步,淘汰緩存
try {
deleteFromCache(dataObject.getId());
} catch (Exception e) {
throw new RuntimeException();
}
}
問題分析:
這是Cache Aside Pattern的改進版,查詢和更新請求併發的問題同樣存在
將方法置於事務中執行,緩存操作失敗拋出RuntimeException會回滾事務,保證了原子性
缺點是redis遠程操作會導致事務執行時間變長,降低併發
任何技術方案的設計,都是折衷。只有適合的方案,未必有最優的方案。