Redis和MySQL保證雙寫一致性的問題解析
Redis和MySQL的雙寫一致性指的是在同時使用緩存和數(shù)據(jù)庫存儲數(shù)據(jù)的時候,保證Redis和MySQL中數(shù)據(jù)的一致性。
用戶發(fā)起請求,先從Redis中查取數(shù)據(jù),有數(shù)據(jù)就直接返回,沒有數(shù)據(jù)就從MySQL中查詢數(shù)據(jù),并且存儲到Redis中,然后返回。從MySQL中查詢到數(shù)據(jù)再存入Redis中這個步驟稱為回寫。
上述這種有回寫的緩存稱為讀寫緩存,僅僅用于查詢的緩存稱為只讀緩存,只讀緩存中的數(shù)據(jù)是通過命令或者批量腳本從MySQL中寫到Redis的。
對于讀寫緩存,如果需要盡可能保證數(shù)據(jù)庫和緩存數(shù)據(jù)一致,使用同步直寫策略,寫數(shù)據(jù)庫后也同步寫Redis緩存;如果數(shù)據(jù)庫和緩存的數(shù)據(jù)同步容許有一定的時間間隔,比如倉庫系統(tǒng),就可以使用異步緩寫策略,寫數(shù)據(jù)庫的一段時間后再同步緩存,當(dāng)出現(xiàn)異常情況需要對數(shù)據(jù)進(jìn)行修補的時候,也可能需要使用異步換寫策略,比如用Kafka或RabbitMQ之類的消息中間件重寫數(shù)據(jù)。
源碼地址,文中只展示關(guān)鍵代碼。
雙檢加鎖策略
從緩存中查詢兩次,并且加上互斥鎖。
func (dao *UserDAO) FindByID(c context.Context, userID int64) (u domain.User, err error) {
db := dao.db
rdb := dao.rdb
key := fmt.Sprintf("user:%v", userID)
// 1. 從緩存中查詢數(shù)據(jù),如果有數(shù)據(jù)就返回
var user domain.User
val, err := rdb.Get(c, key).Result()
if val != "" && err == nil {
err := json.Unmarshal([]byte(val), &user)
if err == nil {
return user, nil
}
}
// 2. 沒有查到數(shù)據(jù)就加鎖再查一次
mu.Lock()
defer mu.Unlock()
val, err = rdb.Get(c, key).Result()
// 2.1 從緩存中查到數(shù)據(jù)就直接返回
if val != "" && err == nil {
err := json.Unmarshal([]byte(val), &user)
if err == nil {
return user, nil
}
}
// 2.2 沒有從緩存中查到數(shù)據(jù)就從數(shù)據(jù)庫中查詢
err = db.Where("id=?", userID).First(&user).Error
if err != nil {
return user, err
}
// 3. 將從數(shù)據(jù)庫中拿到的數(shù)據(jù)寫到緩存中
userStr, err := json.Marshal(user)
if err == nil {
rdb.Set(c, key, userStr, 1000*time.Second)
}
return user, nil
}

數(shù)據(jù)庫和緩存一致性的幾種更新策略
上面說的是查詢策略,接下來說一下數(shù)據(jù)庫和緩存一致性的更新策略。
可以停機(jī)的情況:
? 比如先往MySQL中灌入1萬條數(shù)據(jù),再同步到Redis中,可以在凌晨升級,給出升級提示。
不可以停機(jī)的情況:
1.先更新數(shù)據(jù)庫,再更新緩存(不可行)
異常情況1:
更新Redis出現(xiàn)異常時導(dǎo)致的問題。

異常情況2:
并發(fā)情況下執(zhí)行順序的不確定性導(dǎo)致的問題。

2.先更新緩存,再更新數(shù)據(jù)庫(不可行)
和1一樣,因為并發(fā)可能造成MySQL和Redis中的數(shù)據(jù)不一致。并且一般要把MySQL作為底單數(shù)據(jù),保證最后解釋。
3.先刪除緩存,再更新數(shù)據(jù)庫(不可行)
兩個并發(fā)操作,一個時更新操作,一個是查詢操作,由于執(zhí)行順序的不確定性,可能導(dǎo)致緩存中存儲的是舊數(shù)據(jù),并且一直是舊數(shù)據(jù)。
可以悲觀地認(rèn)為在A更新數(shù)據(jù)期間,一定會有B來讀取數(shù)據(jù),在A寫完數(shù)據(jù)庫之后,延遲一段時間,再次刪除緩存中的數(shù)據(jù)。但是當(dāng)業(yè)務(wù)中讀取數(shù)據(jù)庫和寫緩存的時間不好估算時,這個延遲的時間不好設(shè)置。

4.先更新數(shù)據(jù)庫,再刪除緩存
先更新數(shù)據(jù)庫也不是完全能保證數(shù)據(jù)一致性的,但是造成的影響比較小。只是在緩存刪除失敗或者來不及刪除的時候,導(dǎo)致查詢請求訪問Redis時緩存命中,讀取到的是緩存舊值。

func (dao *UserDAO) UpdateUserData(c context.Context, userID int64, name string) (user User, err error) {
db := dao.db
rdb := dao.rdb
key := fmt.Sprintf("user:%v", userID)
user.ID = userID
// 先更新數(shù)據(jù)庫中的數(shù)據(jù)
u := User{
Name: name,
}
err = db.Model(&user).
Select("Name").
Where("id=?", userID).Updates(u).Error
if err != nil {
return user, err
}
// 再刪除緩存中的數(shù)據(jù)
err = rdb.Del(c, key).Err()
if err != nil {
return user, err
}
return user, nil
}
5.比較穩(wěn)妥的方式
通過非業(yè)務(wù)代碼訂閱MySQL的binlog日志,將對應(yīng)的緩存刪除,如果沒有刪除成功,就將未成功的數(shù)據(jù)發(fā)送到消息隊列中,從消息隊列中讀取數(shù)據(jù)進(jìn)行刪除緩存的重試,刪除緩存成功就把對應(yīng)數(shù)據(jù)從消息隊列中刪掉,重試超過一定次數(shù)后向業(yè)務(wù)層報錯,提醒開發(fā)或者運維人員進(jìn)行處理。
到此這篇關(guān)于Redis和MySQL保證雙寫一致性的問題解析的文章就介紹到這了,更多相關(guān)Redis MySQL雙寫一致性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
分布式Redis?Cluster集群搭建與Redis基本用法
這篇文章介紹了分布式Redis?Cluster集群搭建與Redis基本用法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-02-02
Redis報錯NOAUTH?Authentication?required簡單解決辦法
這篇文章主要給大家介紹了關(guān)于Redis報錯NOAUTH?Authentication?required的簡單解決辦法,Redis無密碼報錯NOAUTH Authentication required的原因是客戶端訪問Redis時需要提供密碼,但是沒有提供或提供的密碼不正確,需要的朋友可以參考下2024-05-05
關(guān)于Redis數(shù)據(jù)庫三種持久化方案介紹
大家好,本篇文章主要講的是關(guān)于Redis數(shù)據(jù)庫三種持久化方案介紹,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01

