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

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

最終方案
由于 WAF 規(guī)則讀多寫少,絕大多數時候從 Redis 讀到的數據不會有變化。有經驗的老同事建議用 Redis 維護版本號,規(guī)則數據仍然存在內存緩存中。經過反復推敲,最終的設計的架構如下。

讀寫邏輯:
- 寫操作比較簡單,使用當前微秒時間戳作為新的版本號,做如下四件事:寫 DB,更新 redis 版本號,更新本地內存緩存中的數據和版本號,四件事的順序可交換
- 讀操作稍微復雜一點:先讀 redis 中的版本號,如果本地版本號沒有過期(絕大多數情況)就直接從本地內存緩存中讀數據。對于 redis 與內存中版本號不一致和 redis 沒讀到(expired)的情況要單獨處理,處理邏輯如偽代碼所示
- 如果一微秒內有多個寫請求,仍然可能出現不一致。不過 Shopee WAF 的實際使用場景不太會有如此頻繁的更新,所以我就沒做處理了。不過時間戳在這里只用來判等,不會比較大小,因此可以用任何一種分布式唯一 ID 解決方案替換時間戳
- 版本號不對用戶暴露,事實上同一版本號可能會讀到不同的規(guī)則數據,但這并不會破壞最終一致性
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+ 形式化驗證
恰好當時自學了 TLA+,順手寫了下這個設計對應的 TLA+ 公式,果然成功通過了最終一致性的驗證。寫這篇總結的時候感覺應該是線性一致的,但沒有驗證。
最開始的持續(xù) 5min 的接口返回數據不一致問題成功得到了解決。
// ================ 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到此這篇關于Redis 解決緩存一致性問題的文章就介紹到這了,更多相關Redis 緩存一致性內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
微服務Spring Boot 整合 Redis 實現好友關注功能
這篇文章主要介紹了微服務Spring Boot 整合 Redis 實現 好友關注,本文結合示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12
redis?sentinel監(jiān)控高可用集群實現的配置步驟
這篇文章主要介紹了redis?sentinel監(jiān)控高可用集群實現的配置步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04

