在資料讀多寫少的情況下作為快取來使用,恐怕是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);
}
最后給讀者留下兩個思考題:
- 為什么
先更新快取,再更新資料庫行不通? - 延時雙刪的方法為什么要休眠一段時間呢?
歡迎大家評論區留言,
推薦閱讀
- 就這?Redis持久化策略——RDB
- 就這?Redis持久化策略——AOF
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/423851.html
標籤:其他
