Cache-Aside Pattern
一. 背景和問題
緩存已經成爲了幾乎所有應用系統的必備要素。使用緩存可以有效提高系統的讀性能,相比於直接讀取數據庫,吞吐量有了很大的提高。但是,在實際生產環境中,很難保證緩存與數據庫中數據的完全一致。程序應採取某種策略,儘可能地保證緩存中的數據是最新的,並且可以檢測到緩存中數據失效,並提供相應的解決方案。
簡單來說,Cache-Aside Pattern的提出是爲了儘可能地解決緩存與數據庫的數據不一致問題。
二. 解決方案
大多數的商用緩存系統都提供了下面的功能:
- 訪問數據時,首先嚐試從緩存中獲取。如果緩存命中,則直接返回。
- 如果緩存未命中,則查詢數據庫。
- 將從數據庫中查詢到的結果放入緩存中,並返回。
- 緩存中任何數據的更新,都會自動同步到數據庫。
如果所使用的緩存沒有提供這些功能,則需要應用系統自己去實現,實現時就可以基於Cache-Aside Pattern。
三. Cache-Aside Pattern
Cache-Aside Pattern分爲讀操作和寫操作兩種。
-
讀操作
原理如下圖:
流程:
-
首先從緩存中查詢數據,如果緩存命中則直接返回。
-
緩存未命中,則去數據庫中讀取。
-
將從數據庫中讀取的結果的副本放入到緩存中,並返回。
-
寫操作
流程:
- 首先更新數據庫。
- 然後刪除緩存中的數據。
四. 一些思考
-
爲什麼是刪除緩存,而不是更新緩存?主要基於以下兩點考量:
- 數據更新後,可能不會有大量的訪問。如果每次更新數據後都更新緩存,可能會造成大量不必要的計算開銷。因此,這裏採用一種lazy的思想,每次更新數據時僅僅是刪除緩存,只有在真正讀請求到來時才進行緩存的更新。
- 在高併發場景下,併發地更新緩存可能會造成緩存可數據庫中數據不一致的問題。
-
寫操作的流程十分關鍵!一定要先更新數據庫,再刪除緩存。如果先刪除緩存,就會存在一個很小的窗口期,使得客戶端查詢時無法命中緩存,而去讀數據庫,然而此時數據庫中的數據還未更新,就會從數據庫中加載到舊的數據並放入緩存中,最終導致緩存數據被污染。
-
緩存的過期策略
許多緩存系統都會對緩存數據設置一定的過期策略。使用Cache-Aside Pattern時,一定要合理地設置過期策略。如果過期時間太短,可能導致大量請求涌入數據庫。相反,如果過期時間太長,有可能導致緩存中數據的大量失效。使用緩存的一個原則,就是儘量緩存那些相對靜態的、頻繁被讀取的數據。
-
**Cache-Aside Pattern並無法完全保證數據庫和緩存的數據一致性。**當某條數據被修改時,在數據庫中會立即更新,但是緩存中的更新會在下次讀取數據時纔會發生,
五. 應用場景
-
適用場景:
- 應用程序所使用的緩存系統並沒有提供前文所述的緩存系統的那些功能。
- 加載資源的需求是不可預測的。該模式使得系統可以按需加載數據,而不需提前預設哪些數據可能需要被獲取。
-
不適用場景:
所緩存的數據集是靜態的。
六. 使用示例
/**
* @Author: ZhangShenao
* @Date: 2019/5/15 11:07
* @Description:演示Cache-Aside Pattern
*/
@Service
public class CacheAsidePatternService {
@Autowired
private CacheService cacheService;
@Autowired
private DataDao dataDao;
//讀操作
public Data getData(String key) {
//1. 讀緩存,如果命中則直接返回
Data data = cacheService.loadDataFromCache(key);
if (data != null) {
return data;
}
//2. 緩存未命中,讀數據庫
data = dataDao.loadDataFromDB(key);
//3. 將讀取到的數據放入緩存
cacheService.putData(key,data);
return data;
}
//寫操作
public void updateData(String key,Data data){
//1. 更新數據庫
dataDao.updateData(key, data);
//2. 刪除緩存
cacheService.evictData(key);
}
}