Redis解決緩存一致性問題
背景
這是我校招剛?cè)肼?Shopee 時(shí)遇到的一個(gè)問題。Shopee 私有云上 WAF 給內(nèi)部用戶提供了設(shè)置 IP 黑白名單規(guī)則的能力,所有規(guī)則存儲(chǔ)在 MySQL 中。我校招剛?cè)肼殨r(shí)從已離職前輩的手中接過了這套系統(tǒng)。但很快發(fā)現(xiàn)每次修改規(guī)則后的 5min 內(nèi)讀到的數(shù)據(jù)不穩(wěn)定——新規(guī)則時(shí)而查得到,時(shí)而查不到,也經(jīng)常有用戶反饋這個(gè)問題。排查發(fā)現(xiàn)原因是服務(wù)代碼中使用了內(nèi)存緩存,而這個(gè)服務(wù)部署了兩個(gè)實(shí)例,實(shí)例之間沒有同步寫請(qǐng)求。如果寫后讀的讀寫請(qǐng)求被路由到不同的實(shí)例上,就無法讀到最新數(shù)據(jù)。而內(nèi)存緩存的過期時(shí)間被設(shè)置為 5min。

查了下這個(gè)服務(wù)的運(yùn)維記錄,在我入職之前做過一次擴(kuò)容,從單實(shí)例擴(kuò)容到雙實(shí)例。之前的研發(fā)同事維護(hù) WAF 時(shí)一直是單實(shí)例運(yùn)行,所以沒出過問題。后來他離職了,別的同事擴(kuò)容時(shí)可能也沒意識(shí)到會(huì)造成不一致的問題。于是問題就到了我這兒。
引入 Redis
我首先想到的解決辦法是把內(nèi)存緩存換成了 Redis,但上線灰度階段 Redis 帶寬被打滿,排查發(fā)現(xiàn)是因?yàn)橛行┮?guī)則的封禁 IP 列表很長,導(dǎo)致傳輸數(shù)據(jù)量非常大。

最終方案
由于 WAF 規(guī)則讀多寫少,絕大多數(shù)時(shí)候從 Redis 讀到的數(shù)據(jù)不會(huì)有變化。有經(jīng)驗(yàn)的老同事建議用 Redis 維護(hù)版本號(hào),規(guī)則數(shù)據(jù)仍然存在內(nèi)存緩存中。經(jīng)過反復(fù)推敲,最終的設(shè)計(jì)的架構(gòu)如下。

讀寫邏輯:
- 寫操作比較簡單,使用當(dāng)前微秒時(shí)間戳作為新的版本號(hào),做如下四件事:寫 DB,更新 redis 版本號(hào),更新本地內(nèi)存緩存中的數(shù)據(jù)和版本號(hào),四件事的順序可交換
- 讀操作稍微復(fù)雜一點(diǎn):先讀 redis 中的版本號(hào),如果本地版本號(hào)沒有過期(絕大多數(shù)情況)就直接從本地內(nèi)存緩存中讀數(shù)據(jù)。對(duì)于 redis 與內(nèi)存中版本號(hào)不一致和 redis 沒讀到(expired)的情況要單獨(dú)處理,處理邏輯如偽代碼所示
- 如果一微秒內(nèi)有多個(gè)寫請(qǐng)求,仍然可能出現(xiàn)不一致。不過 Shopee WAF 的實(shí)際使用場(chǎng)景不太會(huì)有如此頻繁的更新,所以我就沒做處理了。不過時(shí)間戳在這里只用來判等,不會(huì)比較大小,因此可以用任何一種分布式唯一 ID 解決方案替換時(shí)間戳
- 版本號(hào)不對(duì)用戶暴露,事實(shí)上同一版本號(hào)可能會(huì)讀到不同的規(guī)則數(shù)據(jù),但這并不會(huì)破壞最終一致性
func Set(key, data) {
newVer := time()
localCacheVer.Set(newVer)
localCacheData.Set(data)
WriteMySQL(key, data)
redis.Set(key, newVer, exprire=5min)
}
func Read(key) Data {
ver := redis.Get(key)
if ver != nil {
if localCacheVer.Load() == ver {
// Local cache is up-to-date, just use it
return localCacheData.Load()
}
} else { // This version has expired
ver := time()
res := redis.SetNX(key, ver, expire=5min)
if res == false {
// Another instance has proceded, use that version
ver = redis.Get(key)
}
}
data := ReadFromMySQL(key)
localCacheVer.Store(ver)
localCacheData.Store(data)
return data
}TLA+ 形式化驗(yàn)證
恰好當(dāng)時(shí)自學(xué)了 TLA+,順手寫了下這個(gè)設(shè)計(jì)對(duì)應(yīng)的 TLA+ 公式,果然成功通過了最終一致性的驗(yàn)證。寫這篇總結(jié)的時(shí)候感覺應(yīng)該是線性一致的,但沒有驗(yàn)證。
最開始的持續(xù) 5min 的接口返回?cái)?shù)據(jù)不一致問題成功得到了解決。
// ================ tla file ================
---- MODULE waf ----
EXTENDS Integers, TLC
VARIABLE redisVer, localVer, pc, threadVer, DBData, localData, threadData
CONSTANTS DataDomain, ProcSet, r1, r2, r3, t1, t2, t3
vars == << redisVer, localVer, pc, threadVer, localData, threadData, DBData>>
Init == /\ redisVer = -1 /\ localVer = -1 /\ localData = "" /\ DBData = ""
/\ threadVer = [self \in ProcSet |-> -1]
/\ pc = [self \in ProcSet |-> "A"]
/\ threadData = [self \in ProcSet |-> ""]
RedisExpire == /\ threadData = [self \in ProcSet |-> DBData]
/\ redisVer' = -1
/\ DBData' \in DataDomain
/\ UNCHANGED <<localVer, threadVer, localData, threadData, pc>>
ReadRedis(self) == /\ pc[self] = "A"
/\ threadVer' = [threadVer EXCEPT ![self] = redisVer]
/\ / /\ redisVer = -1
/\ pc' = [pc EXCEPT ![self] = "C"]
/ /\ redisVer # -1
/\ pc' = [pc EXCEPT ![self] = "F"]
/\ UNCHANGED <<localVer, redisVer, localData, threadData, DBData>>
SetRedis(self) == /\ pc[self] = "C"
/\ / /\ redisVer # -1 * SetNX failed => use existing redis
/\ redisVer' = redisVer
/\ threadVer' = [threadVer EXCEPT ![self] = redisVer] * Not strictly the same!
/ /\ redisVer = -1 * SetNX ok => change redis
/\ redisVer' \in 1600012345..1600012350
/\ threadVer' = [threadVer EXCEPT ![self] = redisVer']
/\ pc' = [pc EXCEPT ![self] = "I"]
/\ UNCHANGED <<localVer, localData, threadData, DBData>>
CheckLocal(self) == /\ pc[self] = "F"
/\ / /\ localVer = threadVer[self] * Normal case
/\ threadData' = [threadData EXCEPT ![self] = localData]
/\ pc' = [pc EXCEPT ![self] = "H"]
/ /\ localVer # threadVer[self]
/\ pc' = [pc EXCEPT ![self] = "I"]
/\ threadData' = threadData
/\ UNCHANGED <<redisVer, localVer, localData, threadVer, DBData>>
SetLocal(self) == /\ pc[self] = "I"
/\ localVer' = threadVer[self]
/\ localData' = DBData
/\ threadData' = [threadData EXCEPT ![self] = DBData]
/\ pc' = [pc EXCEPT ![self] = "H"]
/\ UNCHANGED <<redisVer, threadVer, DBData>>
ReturnResult(self) == /\ pc[self] = "H"
/\ pc' = [pc EXCEPT ![self] = "Done"]
/\ UNCHANGED <<redisVer, localVer, threadVer, localData, threadData, DBData>>
Again(self) == /\ pc[self] = "Done"
/\ pc' = [pc EXCEPT ![self] = "A"]
/\ UNCHANGED <<redisVer, localVer, threadVer, localData, threadData, DBData>>
Terminating == /\ \A self \in ProcSet: pc[self] = "Done"
/\ UNCHANGED vars
Proceed(t) == ReadRedis(t) / SetRedis(t) / CheckLocal(t) / SetLocal(t) / ReturnResult(t) / Again(t)
Next == / RedisExpire
/ \E t \in ProcSet: Proceed(t)
FairForEveryone == \A t \in ProcSet: SF_vars(Proceed(t))
Spec == /\ Init /\ [][Next]_vars /\ FairForEveryone
symm == Permutations({r1, r2, r3}) \union Permutations({t1, t2, t3})
EventualCons == \A v \in DataDomain: DBData = v ~> threadData = [t \in ProcSet |-> v]
ECSpec == Spec /\ EventualCons
// ======= cfg file ========
SPECIFICATION Spec
CONSTANTS
DataDomain = {r1, r2}
r1 = r1
r2 = r2
r3 = r3
ProcSet = {t1, t2, t3}
t1 = t1
t2 = t2
t3 = t3
SYMMETRY symm
PROPERTIES EventualCons到此這篇關(guān)于Redis 解決緩存一致性問題的文章就介紹到這了,更多相關(guān)Redis 緩存一致性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 詳解redis緩存與數(shù)據(jù)庫一致性問題解決
- 面試常問:如何保證Redis緩存和數(shù)據(jù)庫的數(shù)據(jù)一致性
- redis緩存一致性延時(shí)雙刪代碼實(shí)現(xiàn)方式
- 淺談一下如何保證Redis緩存與數(shù)據(jù)庫的一致性
- MySQL數(shù)據(jù)庫和Redis緩存一致性的更新策略
- redis分布式鎖解決緩存雙寫一致性
- redis緩存與數(shù)據(jù)庫一致性的問題及解決
- Spring?Boot與Redis的緩存一致性問題解決
- Redis緩存和數(shù)據(jù)庫的數(shù)據(jù)一致性的問題解決
- Redis+Caffeine多級(jí)緩存數(shù)據(jù)一致性解決方案
- Redis 緩存雙寫一致性的解決方案
相關(guān)文章
Linux下安裝Redis 6.0.5的實(shí)現(xiàn)
本文詳細(xì)介紹了在Linux系統(tǒng)下安裝Redis 6.0.5的步驟,包括安裝準(zhǔn)備、編譯安裝、啟動(dòng)服務(wù)、設(shè)置密碼和配置文件修改等,具有一定的參考價(jià)值,感興趣的可以了解一下2025-02-02
解析高可用Redis服務(wù)架構(gòu)分析與搭建方案
我們按照由簡至繁的步驟,搭建一個(gè)最小型的高可用的Redis服務(wù)。 本文通過四種方案給大家介紹包含每種方案的優(yōu)缺點(diǎn)及詳細(xì)解說,具體內(nèi)容詳情跟隨小編一起看看吧2021-06-06
Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法
在現(xiàn)代的互聯(lián)網(wǎng)應(yīng)用中,Redis作為一種高性能的內(nèi)存數(shù)據(jù)庫,被廣泛應(yīng)用于緩存、會(huì)話管理和消息隊(duì)列等場(chǎng)景,然而,Redis的內(nèi)存資源是有限的,過多的內(nèi)存占用可能會(huì)導(dǎo)致數(shù)據(jù)丟失所以本文將給大家介紹一下Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法2023-08-08
高并發(fā)下Redis如何保持?jǐn)?shù)據(jù)一致性(避免讀后寫)
本文主要介紹了高并發(fā)下Redis如何保持?jǐn)?shù)據(jù)一致性(避免讀后寫),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
redis執(zhí)行l(wèi)ua腳本的實(shí)現(xiàn)方法
redis在2.6推出了腳本功能,允許開發(fā)者使用Lua語言編寫腳本傳到redis中執(zhí)行。本文就介紹了redis執(zhí)行l(wèi)ua腳本的實(shí)現(xiàn)方法,感興趣的可以了解一下2021-11-11

