Redis解決緩存一致性問題
背景
這是我校招剛?cè)肼?Shopee 時遇到的一個問題。Shopee 私有云上 WAF 給內(nèi)部用戶提供了設(shè)置 IP 黑白名單規(guī)則的能力,所有規(guī)則存儲在 MySQL 中。我校招剛?cè)肼殨r從已離職前輩的手中接過了這套系統(tǒng)。但很快發(fā)現(xiàn)每次修改規(guī)則后的 5min 內(nèi)讀到的數(shù)據(jù)不穩(wěn)定——新規(guī)則時而查得到,時而查不到,也經(jīng)常有用戶反饋這個問題。排查發(fā)現(xiàn)原因是服務(wù)代碼中使用了內(nèi)存緩存,而這個服務(wù)部署了兩個實例,實例之間沒有同步寫請求。如果寫后讀的讀寫請求被路由到不同的實例上,就無法讀到最新數(shù)據(jù)。而內(nèi)存緩存的過期時間被設(shè)置為 5min。
查了下這個服務(wù)的運維記錄,在我入職之前做過一次擴容,從單實例擴容到雙實例。之前的研發(fā)同事維護 WAF 時一直是單實例運行,所以沒出過問題。后來他離職了,別的同事擴容時可能也沒意識到會造成不一致的問題。于是問題就到了我這兒。
引入 Redis
我首先想到的解決辦法是把內(nèi)存緩存換成了 Redis,但上線灰度階段 Redis 帶寬被打滿,排查發(fā)現(xiàn)是因為有些規(guī)則的封禁 IP 列表很長,導(dǎo)致傳輸數(shù)據(jù)量非常大。
最終方案
由于 WAF 規(guī)則讀多寫少,絕大多數(shù)時候從 Redis 讀到的數(shù)據(jù)不會有變化。有經(jīng)驗的老同事建議用 Redis 維護版本號,規(guī)則數(shù)據(jù)仍然存在內(nèi)存緩存中。經(jīng)過反復(fù)推敲,最終的設(shè)計的架構(gòu)如下。
讀寫邏輯:
- 寫操作比較簡單,使用當前微秒時間戳作為新的版本號,做如下四件事:寫 DB,更新 redis 版本號,更新本地內(nèi)存緩存中的數(shù)據(jù)和版本號,四件事的順序可交換
- 讀操作稍微復(fù)雜一點:先讀 redis 中的版本號,如果本地版本號沒有過期(絕大多數(shù)情況)就直接從本地內(nèi)存緩存中讀數(shù)據(jù)。對于 redis 與內(nèi)存中版本號不一致和 redis 沒讀到(expired)的情況要單獨處理,處理邏輯如偽代碼所示
- 如果一微秒內(nèi)有多個寫請求,仍然可能出現(xiàn)不一致。不過 Shopee WAF 的實際使用場景不太會有如此頻繁的更新,所以我就沒做處理了。不過時間戳在這里只用來判等,不會比較大小,因此可以用任何一種分布式唯一 ID 解決方案替換時間戳
- 版本號不對用戶暴露,事實上同一版本號可能會讀到不同的規(guī)則數(shù)據(jù),但這并不會破壞最終一致性
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+,順手寫了下這個設(shè)計對應(yīng)的 TLA+ 公式,果然成功通過了最終一致性的驗證。寫這篇總結(jié)的時候感覺應(yīng)該是線性一致的,但沒有驗證。
最開始的持續(xù) 5min 的接口返回數(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 詳解redis緩存與數(shù)據(jù)庫一致性問題解決
- 面試常問:如何保證Redis緩存和數(shù)據(jù)庫的數(shù)據(jù)一致性
- redis緩存一致性延時雙刪代碼實現(xiàn)方式
- 淺談一下如何保證Redis緩存與數(shù)據(jù)庫的一致性
- MySQL數(shù)據(jù)庫和Redis緩存一致性的更新策略
- redis分布式鎖解決緩存雙寫一致性
- redis緩存與數(shù)據(jù)庫一致性的問題及解決
- Spring?Boot與Redis的緩存一致性問題解決
- Redis緩存和數(shù)據(jù)庫的數(shù)據(jù)一致性的問題解決
- Redis+Caffeine多級緩存數(shù)據(jù)一致性解決方案
- Redis 緩存雙寫一致性的解決方案
相關(guān)文章
解析高可用Redis服務(wù)架構(gòu)分析與搭建方案
我們按照由簡至繁的步驟,搭建一個最小型的高可用的Redis服務(wù)。 本文通過四種方案給大家介紹包含每種方案的優(yōu)缺點及詳細解說,具體內(nèi)容詳情跟隨小編一起看看吧2021-06-06Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法
在現(xiàn)代的互聯(lián)網(wǎng)應(yīng)用中,Redis作為一種高性能的內(nèi)存數(shù)據(jù)庫,被廣泛應(yīng)用于緩存、會話管理和消息隊列等場景,然而,Redis的內(nèi)存資源是有限的,過多的內(nèi)存占用可能會導(dǎo)致數(shù)據(jù)丟失所以本文將給大家介紹一下Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法2023-08-08高并發(fā)下Redis如何保持數(shù)據(jù)一致性(避免讀后寫)
本文主要介紹了高并發(fā)下Redis如何保持數(shù)據(jù)一致性(避免讀后寫),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03redis執(zhí)行l(wèi)ua腳本的實現(xiàn)方法
redis在2.6推出了腳本功能,允許開發(fā)者使用Lua語言編寫腳本傳到redis中執(zhí)行。本文就介紹了redis執(zhí)行l(wèi)ua腳本的實現(xiàn)方法,感興趣的可以了解一下2021-11-11