在數據讀多寫少的情況下作為緩存來使用,恐怕是Redis使用最普遍的場景了。當使用Redis作為緩存的時候,一般流程是這樣的。
- 如果緩存在Redis中存在,即緩存命中,則直接返回數據
- 如果Redis中沒有對應緩存,則需要直接查詢數據庫,然后存入Redis,最后把數據返回
通常情況下,我們會為某個緩存設置一個key值,并針對key值設置一個過期時間,如果被查詢的數據對應的key過期了,則直接查詢數據庫,并將查詢得到的數據存入Redis,然后重置過期時間,最后將數據返回,偽代碼如下:
/**
* 根據用戶名獲取用戶詳細信息
* @author 公眾號【電子發燒友】
*/
public User getUserInfo(String userName) {
User user = redisCache.getName("user:" + userName);
if (user != null) {
return user;
}
// 從數據庫中直接搜索
user = selectUserByUserName(userName);
// 將數據寫入Redis,并設置過期時間
redisCache.set("user:" + userName, user, 30000);
// 返回數據
return user;
}
一致性問題
但是,在Redis的key值未過期的情況下,用戶修改了個人信息,我們此時既要操作數據庫數據,也要操作Redis數據。現在我們面臨了兩種選擇:
- 先操作Redis的數據,再操作數據庫的數據
- 先操作數據庫的數據,再操作Redis的數據
如論選擇哪種方法,最理想的情況下,兩個操作要么同時成功,要么同時失敗,否則就會出現Redis和數據庫數據不一致的情況。
遺憾的是,目前沒有什么框架能夠保證Redis的數據和數據庫的數據的完全一致性。我們只能根據場景和所需要付出的代碼來采取一定的措施降低數據不一致出現的概率,在一致性和性能之間取得一個折中。
下面我們來討論一下關于Redis和數據庫之間數據一致性的一些方案。
方案選擇
是刪除緩存還是更新緩存?
當數據庫數據發生變化的時候,Redis的數據也需要進行相應的操作,那么這個「操作」到底是用「更新」還是用「刪除」呢?
「更新」的話調用Redis的set方法,新值替換舊值;「刪除」直接刪除原來的緩存,下次查詢的時候重新讀取數據庫,然后再更新Redis。
結論:推薦直接使用「刪除」操作 。
因為使用「更新」操作的話,你會面臨兩種選擇
- 先更新緩存,再更新數據庫
- 先更新數據庫,再更新緩存
第1種不用考慮了,下面討論一下「先更新數據庫,再更新緩存」這種方案。
如果線程1和線程2同時進行更新操作,但是每個線程的執行順序如上圖所示,此時就會導致數據不一致,因此從這個角度上我們推薦直接使用刪除緩存的方式。
此外,推薦使用「刪除緩存」還有兩點原因。
- 如果寫數據庫的場景比讀數據場景多,采用這種方案就會導致緩存就被頻繁寫入,浪費性能;
- 如果緩存要經過一系列復雜的計算才能得到,那么每次寫入數據庫后,都再次計算寫入的緩存無疑也是浪費性能的。
明確這個問題之后,擺在我們面前的就只有兩個選擇了:
- 先更新數據庫,再刪除緩存
- 先刪除緩存,再更新數據庫
先更新數據庫,再刪除緩存
這種方式可能存在以下兩種異常情況
- 更新數據庫失敗,這時可以通過程序捕獲異常,直接返回結果,不再繼續刪除緩存,所以不會出現數據不一致的問題
- 更新數據庫成功,刪除緩存失敗。導致數據庫是最新數據,緩存中的是舊數據,數據不一致
第2種情況應該怎么辦呢?我們有兩種方式:失敗重試和 異步更新 。
失敗重試
如果刪除緩存失敗,我們可以捕獲這個異常,把需要刪除的 key 發送到消息隊列。自己創建一個消費者消費,嘗試再次刪除這個 key,直到刪除成功為止。
這種方式有個缺點,首先會對業務代碼造成入侵,其次引入了消息隊列,增加了系統的不確定性。
異步更新緩存
因為更新數據庫時會往 binlog 中寫入日志,所以我們可以啟動一個監聽 binlog變化的服務(比如使用阿里的 canal開源組件),然后在客戶端完成刪除 key 的操作。如果刪除失敗的話,再發送到消息隊列。
總結
總之,對于刪除緩存失敗的情況,我們的做法是不斷地重試刪除操作,直到成功。無論是重試還是異步刪除,都是最終一致性的思想。
先刪除緩存,再更新數據庫
這種方式可能存在以下兩種異常情況
- 刪除緩存失敗,這時可以通過程序捕獲異常,直接返回結果,不再繼續更新數據庫,所以不會出現數據不一致的問題
- 刪除緩存成功,更新數據庫失敗。在多線程下可能會出現數據不一致的問題
這時,Redis中存儲的舊數據,數據庫的值是新數據,導致數據不一致。這時我們可以采用延時雙刪的策略,即更新數據庫數據之后,再刪除一次緩存。
用偽代碼表示就是:
/**
* 延時雙刪
* @author 公眾號【蟬沐風】
*/
public void update(String key, Object data) {
// 首先刪除緩存
redisCache.delKey(key);
// 更新數據庫
db.updateData(data);
// 休眠一段時間,時間依據數據的讀取耗費的時間而定
Thread.sleep(500);
// 再次刪除緩存
redisCache.delKey(key);
}
-
數據
+關注
關注
8文章
6893瀏覽量
88830 -
緩存
+關注
關注
1文章
233瀏覽量
26649 -
數據庫
+關注
關注
7文章
3765瀏覽量
64276 -
Redis
+關注
關注
0文章
371瀏覽量
10846
發布評論請先 登錄
相關推薦
評論