目錄
數據一致性
先更新緩存,后更新數據庫【一般不考慮】
先更新數據庫,再更新緩存【一般不考慮】
先刪除緩存,后更新數據庫
先更新數據庫,后刪除緩存【推薦】
怎么選擇這些方案?采用哪種合適?
緩存穿透、擊穿、雪崩
緩存穿透
解決方案——緩存穿透問題
緩存擊穿
緩存雪崩
熱點Key、BigKey【數據傾斜】
熱點key產生原因&危害
怎么發現熱點Key
預估發現
客戶端發現
怎么解決熱點Key?
使用二級緩存
key分散
什么是BigKey
BigKey危害
發現bigkey
解決BigKey【核心思路:拆分】
Redis腦裂【數據丟失】
哨兵主從集群腦裂
集群腦裂
多級緩存案例
攜程金融在Redis的實踐
整體方案
數據準確性
并發控制?
基于updateTime的更新順序控制
數據完整性設計
數據一致性
只要使用到緩存,無論是本地內存做緩存還是使用 redis 做緩存,那么就會存在數據同步的問題。
以 Tomcat 向 MySQL 中寫入和刪改數據為例,來給你解釋一下,數據的增刪改操作具體是如何進行的。
數據一致性方案
-
先更新緩存,再更新數據庫
-
先更新數據庫,再更新緩存
-
先刪除緩存,后更新數據庫
-
先更新數據庫,后刪除緩存
新增數據時,數據直接寫到數據庫中,不需要對緩存做任何的操作,此時,緩存中本身就沒有新增數據,而數據庫中的值就是最新值,此時緩存&數據庫中的數據是一致的。
當我們涉及到更新緩存的時候呢?
先更新緩存,后更新數據庫【一般不考慮】
更新緩存成功,更新數據庫異常,會導致緩存與數據庫數據完全不一致,且很難察覺,因為緩存中的數據一直都存在。
先更新數據庫,再更新緩存【一般不考慮】
原因與上一種情況一致。數據庫更新成功,緩存更新失敗,也會有數據不一致問題。除此以外,還可能存在這樣的問題:
-
并發:當請求A與請求B同時進行更新操作。可能出現A請求更新數據庫,B請求更新數據庫,B請求更新緩存,A此時更新緩存。這就出現請求A更新緩存比請求B更新緩存應該早才對,因網絡等不可抗拒的因素,B卻比A先一步更新緩存,從而導致出現臟數據。
-
業務場景:寫場景較多時,讀場景少。采用這種方案導致數據壓根沒有被讀取到,但緩存卻一直頻繁的更新而浪費性能。
到底是選擇更新緩存還是淘汰緩存呢?
主要取決于“更新緩存的復雜度”,更新緩存的代價很小,此時我們應該更傾向于更新緩存,以保證更高的緩存命中率,更新緩存的代價很大,此時我們應該更傾向于淘汰緩存。
先刪除緩存,后更新數據庫
也存在問題,但是可以解決
出現問題的情況:請求A【更新】和請求B【查詢】,A先刪除Redis的數據,在數據庫中進行更新操作。此時請求B查詢時,Redis數據為空,就會去數據庫中查詢該值,補回到Redis中。但是此時請求A還沒有更新成功或者事務還沒有提交。請求B從數據庫中查詢到老數據!這就會產生數據庫和緩存中數據不一致的問題。
解決方案:延遲雙刪。即先淘汰緩存,再寫數據庫,休眠1秒,再淘汰緩存。
以下是“延遲雙刪”的偽代碼
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X)
這么做,可以將【1秒】內所造成的緩存臟數據,再次刪除。
針對休眠時間,需要評估自身項目。
項目中寫數據的休眠時間在讀數據業務邏輯的耗時基礎上,加幾百ms即可。這么做的目的,就是確保讀請求結束,寫請求可以刪除讀請求造成的緩存臟數據。
上面保證事務提交完以后再進行刪除緩存還有一個問題,如果MySQL采取的讀寫分離架構,主從同步之間會有時間差。
請求A【更新】和請求B【查詢】,A更新刪除緩存數據,請求主庫進行更新操作,主庫與從庫進行同步數據的操作,請求B發現緩存沒有數據,會去庫中查詢,此時同步數據未完成時,拿到的依舊是舊數據。
解決方案:
-
延遲雙刪策略,休眠時間在同步的延遲時間基礎加上幾百毫秒。
-
方案2:如果是對緩存進行填充數據的行為,查詢數據庫的操作強制從主庫進行查詢。
采用這種同步淘汰策略,吞吐量降低怎么破?
方案:將第二次刪除作為異步。自己起一個線程,異步刪除。這樣寫的請求就不需要休眠幾秒后再返回數據,這么做以加大吞吐量。
如果第二次刪除刪除失敗咋辦?
啊,震驚。要看下面這種策略了
先更新數據庫,后刪除緩存【推薦】
這種方式【Cache Aside Pattern】。讀數據的時候先讀緩存,緩存沒有就查數據庫。然后取出的數據庫放入緩存,同時返回響應。更新的時候,先更新數據庫,再刪除緩存。
怎么選擇這些方案?采用哪種合適?
在線上,更多的偏向與使用刪除緩存類操作,因為這種方式的話,會更容易避免一些問題。
因為刪除緩存更新緩存的速度比在數據庫中要快一些,所以一般情況下我們可能會先用先更新數據庫,后刪除緩存的操作。
因為這種情況下緩存不一致性的情況只有可能是查詢比刪除慢的情況,而這種情況相對來說會少很多。同時結合延時雙刪的處理,可以有效的避免緩存不一致的情況。
緩存穿透、擊穿、雪崩
緩存穿透
指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,于是這個請求就可以隨意訪問數據庫,這個就是緩存穿透,緩存穿透將導致不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護后端存儲的意義。
緩存穿透問題可能會使后端存儲負載加大,由于很多后端存儲不具備高并發性,甚至可能造成后端存儲宕掉。通常可以在程序中分別統計總調用數、緩存層命中數、存儲層命中數,如果發現大量存儲層空命中,可能就是出現了緩存穿透問題。
造成緩存穿透的基本原因有兩個。
-
自身業務代碼或者數據出現問題,比如,我們數據庫的 id 都是1開始自增上去的,如發起為id值為 -1 的數據或 id 為特別大不存在的數據。如果不對參數做校驗,數據庫id都是大于0的,我一直用小于0的參數去請求你,每次都能繞開Redis直接打到數據庫,數據庫也查不到,每次都這樣,并發高點就容易崩掉了。
-
一些惡意攻擊、爬蟲等造成大量空命中。
解決方案——緩存穿透問題
-
緩存空對象
當存儲層不命中,到數據庫查發現也沒有命中,那么仍然將空對象保留到緩存層中,之后再訪問這個數據將會從緩存中獲取,這樣就保護了后端數據源。
緩存空對象會有兩個問題:
-
空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的內存空間(如果是攻擊,問題更嚴重),比較有效的方法是針對這類數據設置一個較短的過期時間,讓其自動剔除。
-
緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有一定影響。例如過期時間設置為5分鐘,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以利用消前面所說的數據一致性方案處理。
-
布隆過濾器攔截
【這種方法適用于數據命中不高、數據相對固定、實時性低(通常是數據集較大)的應用場景,代碼維護較為復雜,但是緩存空間占用少。】
在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截。例如:一個推薦系統有4億個用戶id,每個小時算法工程師會根據每個用戶之前歷史行為計算出推薦數據放到存儲層中,但是最新的用戶由于沒有歷史行為,就會發生緩存穿透的行為,為此可以將所有推薦數據的用戶做成布隆過濾器。如果布隆過濾器認為該用戶id不存在,那么就不會訪問存儲層,在一定程度保護了存儲層。
緩存擊穿
緩存擊穿是指一個Key非常熱點,在不停的扛著大并發,大并發集中對這一個點進行訪問,當這個Key在失效的瞬間,持續的大并發就穿破緩存,直接請求數據庫,就像在一個完好無損的桶上鑿開了一個洞。
解決方案:設置熱點數據永遠不過期。或者加上互斥鎖。
-
永不過期
-
從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
-
從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value里,如果發現要過期了,通過一個后臺的異步線程進行緩存的構建,也就是“邏輯”過期
-
從實戰看,這種方法對于性能非常友好,唯一不足的就是構建緩存時候,其余線程(非構建緩存的線程)可能訪問的是老數據,但是對于一般的互聯網功能來說這個還是可以忍受。
-
使用互斥鎖(mutnex key)
-
簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load 數據庫,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load 數據庫的操作并回設緩存;否則,就重試整個get緩存的方法。
-
緩存雪崩
由于緩存層承載著大量請求,有效地保護了存儲層,但是如果緩存層由于某些原因不能提供服務,比如同一時間緩存數據大面積失效,那一瞬間Redis跟沒有一樣,于是所有的請求都會達到存儲層,存儲層的調用量會暴增,造成存儲層也會級聯宕機的情況。
緩存雪崩的英文原意是stampeding herd(奔逃的野牛),指的是緩存層宕掉后,流量會像奔逃的野牛一樣,打向后端存儲。
預防和解決緩存雪崩問題,可以從以下三個方面進行著手。
1)保證緩存層服務高可用性。和飛機都有多個引擎一樣,如果緩存層設計成高可用的,即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務,例如Redis中Sentinel和 Redis Cluster都實現了高可用。
2)依賴隔離組件為后端限流并降級。無論是緩存層還是存儲層都會有出錯的概率,可以將它們視同為資源。作為并發量較大的系統,假如有一個資源不可用,可能會造成線程全部阻塞(hang)在這個資源上,造成整個系統不可用。
3)提前演練。在項目上線前,演練緩存層宕掉后,應用以及后端的負載情況以及可能出現的問題,在此基礎上做一些預案設定。
4)將緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復率就會降低,就很難引發集體失效的事件。
熱點Key、BigKey【數據傾斜】
數據傾斜其實就是訪問量傾斜或者數據量傾斜
-
熱點key出現造成集群訪問量傾斜
-
bigKey造成集群數據量傾斜。
熱點key產生原因&危害
原因:hotkey的原因大致分為兩種。
-
用戶消費的數據遠大于生產的數據。比如熱點評論、洛克王國直播(嘿嘿)......
-
日常生活中突發的事件。比如特朗普在前幾天增加關稅......。
雙十一期間某些熱門商品的降價促銷,當這其中的某一件商品被數萬次點擊瀏覽或者購買時,會形成一個較大的需求量,這種情況下就會造成熱點問題。同理,被大量刊發、瀏覽的熱點新聞、熱點評論、明星直播等,這些典型的讀多寫少的場景也會產生熱點問題。
請求分片集中,超過單Server的性能極限。在服務端讀數據進行訪問時,往往會對數據進行分片切分,此過程中會在某一主機Server上對相應的Key進行訪問,當訪問超過Server極限時,就會導致熱點Key問題的產生。
危害:
-
流量集中,達到物理網卡上線。
-
請求過多,緩存分片服務被打垮
-
數據庫擊穿,引起業務雪崩
怎么發現熱點Key
預估發現
針對業務提前預估出訪問頻繁的熱點key,例如秒殺商品業務中,秒殺的商品都是熱點key。
當然并非所有的業務都容易預估出熱點key,可能出現漏掉或者預估錯誤的情況。
客戶端發現
客戶端其實是距離key"最近"的地方,因為Redis命令就是從客戶端發出的,以Jedis為例,可以在核心命令入口,使用這個Google Guava中的AtomicLongMap進行記錄,如下所示。
使用客戶端進行熱點key的統計非常容易實現,但是同時問題也非常多:
(1) 無法預知key的個數,存在內存泄露的危險。
(2) 對于客戶端代碼有侵入,各個語言的客戶端都需要維護此邏輯,維護成本較高。
(3) 規模化匯總實現比較復雜。
-
redis的monitor指令
-
monitor命令在高并發條件下,內存暴增同時會影響Redis的性能,所以此種方法適合在短時間內使用。只能統計一個Redis節點的熱點key,對于Redis集群需要進行匯總統計。
-
-
redis在4.0.3中給redis-cli 提供--hotkeys,用于找到熱點key
-
如果鍵值較多的情況下,執行慢。和熱點的概念的有點背道而馳,同時熱度定義的不夠準確。
-
-
TCP抓包發現
-
Redis客戶端使用TCP協議與服務端進行交互,通信協議采用的是RESP。如果站在機器的角度,可以通過對機器上所有Redis端口的TCP數據包進行抓取完成熱點key的統計。
-
此種方法對于Redis客戶端和服務端來說毫無侵入,是比較完美的方案,但是依然存在3個問題:
-
需要一定的開發成本
-
對于高流量的機器抓包,對機器網絡可能會有干擾,同時抓包時候會有丟包的可能性。
-
維護成本過高。
-
-
對于成本問題,有一些開源方案實現了該功能,例如ELK(ElasticSearch Logstash Kibana)體系下的packetbeat[2] 插件,可以實現對Redis、MySQL等眾多主流服務的數據包抓取、分析、報表展示
-
怎么解決熱點Key?
使用二級緩存
可以使用 guava-cache或hcache,發現熱點key之后,將這些熱點key加載到JVM中作為本地緩存。訪問這些key時直接從本地緩存獲取即可,不會直接訪問到redis層了,有效的保護了緩存服務器。
key分散
將熱點key分散為多個子key,然后存儲到緩存集群的不同機器上,這些子key對應的value都和熱點key是一樣的。當通過熱點key去查詢數據時,通過某種hash算法隨機選擇一個子key,然后再去訪問緩存機器,將熱點分散到了多個子key上。
什么是BigKey
bigkey是指key對應的value所占的內存空間比較大,例如一個字符串類型的value可以最大存到512MB,一個列表類型的value最多可以存儲23-1個元素。
如果按照數據結構來細分的話,一般分為字符串類型bigkey和非字符串類型bigkey。
字符串類型:體現在單個value值很大,一般認為超過10KB就是bigkey,但這個值和具體的OPS(Operations Per Second:每秒操作數)相關。
非字符串類型:哈希、列表、集合、有序集合,體現在元素個數過多。
bigkey無論是空間復雜度和時間復雜度都不太友好。
BigKey危害
bigkey的危害體現在三個方面:
-
內存空間不均勻(平衡):例如在Redis Cluster中,bigkey 會造成節點的內存空間使用不均勻。
-
超時阻塞:由于Redis單線程的特性,操作bigkey比較耗時,也就意味著阻塞Redis可能性增大。
-
網絡擁塞:每次獲取bigkey產生的網絡流量較大
假設一個bigkey為1MB,每秒訪問量為1000,那么每秒產生1000MB 的流量,對于普通的千兆網卡(按照字節算是128MB/s)的服務器來說簡直是滅頂之災,而且一般服務器會采用單機多實例的方式來部署,也就是說一個bigkey可能會對其他實例造成影響,其后果不堪設想。
bigkey的存在并不是完全致命的:
如果這個bigkey存在但是幾乎不被訪問,那么只有內存空間不均勻的問題存在,相對于另外兩個問題沒有那么重要緊急,但是如果bigkey是一個熱點key(頻繁訪問),那么其帶來的危害不可想象,所以在實際開發和運維時一定要密切關注bigkey的存在。
發現bigkey
-
redis-cli --bigkeys可以命令統計bigkey的分布。但是在生產環境中,開發和運維人員更希望自己可以定義bigkey的大小,而且更希望找到真正的bigkey都有哪些,這樣才可以去定位、解決、優化問題。
-
debug object key查看serializedlength屬性。判斷一個key是否為bigkey,可以執行那個命令,它表示 key對應的value序列化之后的字節數。
如果是要遍歷多個,則盡量不要使用keys的命令,可以使用scan的命令來減少壓力。它能有效的解決keys命令存在的問題。和keys命令執行時會遍歷所有鍵不同,scan采用漸進式遍歷的方式來解決 keys命令可能帶來的阻塞問題,但是要真正實現keys的功能,需要執行多次scan。可以想象成只掃描一個字典中的一部分鍵,直到將字典中的所有鍵遍歷完畢。
-
scan cursor [match pattern] [count number]
-
cursor :是必需參數,實際上cursor是一個游標,第一次遍歷從0開始,每次scan遍歷完都會返回當前游標的值,直到游標值為0,表示遍歷結束。
-
Match pattern :是可選參數,它的作用的是做模式的匹配,這點和keys的模式匹配很像。
-
Count number :是可選參數,它的作用是表明每次要遍歷的鍵個數,默認值是10,此參數可以適當增大。
-
-
除了scan 以外,Redis提供了面向哈希類型、集合類型、有序集合的掃描遍歷命令,解決諸如hgetall、smembers、zrange可能產生的阻塞問題,對應的命令分別是hscan、sscan、zscan,它們的用法和scan基本類似。
漸進式遍歷可以有效的解決keys命令可能產生的阻塞問題,但是scan并非完美無瑕,如果在scan 的過程中如果有鍵的變化(增加、刪除、修改),那么遍歷效果可能會碰到如下問題:新增的鍵可能沒有遍歷到,遍歷出了重復的鍵等情況,也就是說scan并不能保證完整的遍歷出來所有的鍵,這些是我們在開發時需要考慮的。
如果鍵值個數比較多,scan + debug object會比較慢,可以利用Pipeline機制完成。對于元素個數較多的數據結構,debug object執行速度比較慢,存在阻塞Redis的可能,所以如果有從節點,可以考慮在從節點上執行。
解決BigKey【核心思路:拆分】
對 big key 存儲的數據 (big value)進行拆分,變成value1,value2… valueN等等。
例如big value 是個大json 通過 mset 的方式,將這個 key 的內容打散到各個實例中,或者一個hash,每個field代表一個具體屬性,通過hget、hmget獲取部分value,hset、hmset來更新部分屬性。
例如big value 是個大list,可以拆成將list拆成。= list_1, list_2, list3, ...listN
其他數據類型同理。
Redis腦裂【數據丟失】
所謂的腦裂,就是指在有主從集群中,同時有兩個主節點,它們都能接收寫請求。而腦裂最直接的影響,就是客戶端不知道應該往哪個主節點寫入數據,結果就是不同的客戶端會往不同的主節點上寫入數據。而且,嚴重的話,腦裂會進一步導致數據丟失。
哨兵主從集群腦裂
現在假設:有三臺服務器一臺主服務器,兩臺從服務器,還有一個哨兵。
基于上邊的環境,這時候網絡環境發生了波動導致了sentinel沒有能夠心跳感知到master,但是哨兵與slave之間通訊正常。所以通過選舉的方式提升了一個salve為新master。如果恰好此時server1仍然連接的是舊的master,而server2連接到了新的master上。數據就不一致了,哨兵恢復對老master節點的感知后,會將其降級為slave節點,然后從新maste同步數據(full resynchronization),導致腦裂期間老master寫入的數據丟失。
而且基于setNX指令的分布式鎖,可能會拿到相同的鎖;基于incr生成的全局唯一id,也可能出現重復。通過配置參數
-
min-replicas-to-write 2
-
min-replicas-max-lag 10
第一個參數表示最少的salve節點為2個
第二個參數表示數據復制和同步的延遲不能超過10秒
配置了這兩個參數:如果發生腦裂:原master會在客戶端寫入操作的時候拒絕請求。這樣可以避免大量數據丟失。
集群腦裂
Redis集群的腦裂一般是不存在的,因為Redis集群中存在著過半選舉機制,而且當集群16384個槽任何一個沒有指派到節點時整個集群不可用。所以我們在構建Redis集群時,應該讓集群 Master 節點個數最少為 3 個,且集群可用節點個數為奇數。
不過腦裂問題不是是可以完全避免,只要是分布式系統,必然就會一定的幾率出現這個問題,CAP的理論就決定了。
多級緩存案例
首先,用戶的請求被負載均衡服務分發到Nginx上,此處常用的負載均衡算法是輪詢或者一致性哈希,輪詢可以使服務器的請求更加均衡,而一致性哈希可以提升Nginx應用的緩存命中率。
接著,Nginx應用服務器讀取本地緩存,實現本地緩存的方式可以是Lua Shared Dict,或者面向磁盤或內存的Nginx Proxy Cache,以及本地的Redis實現等,如果本地緩存命中則直接返回。Nginx應用服務器使用本地緩存可以提升整體的吞吐量,降低后端的壓力,尤其應對熱點數據的反復讀取問題非常有效。
如果Nginx應用服務器的本地緩存沒有命中,就會進一步讀取相應的分布式緩存——Redis分布式緩存的集群,可以考慮使用主從架構來提升性能和吞吐量,如果分布式緩存命中則直接返回相應數據,并回寫到Nginx應用服務器的本地緩存中。
如果Redis分布式緩存也沒有命中,則會回源到Tomcat集群,在回源到Tomcat集群時也可以使用輪詢和一致性哈希作為負載均衡算法。當然,如果Redis分布式緩存沒有命中的話,Nginx應用服務器還可以再嘗試一次讀主Redis集群操作,目的是防止當從 Redis集群有問題時可能發生的流量沖擊。
在Tomcat集群應用中,首先讀取本地平臺級緩存,如果平臺級緩存命中則直接返回數據,并會同步寫到主Redis集群,然后再同步到從Redis集群。此處可能存在多個Tomcat實例同時寫主Redis集群的情況,可能會造成數據錯亂,需要注意緩存的更新機制和原子化操作。
如果所有緩存都沒有命中,系統就只能查詢數據庫或其他相關服務獲取相關數據并返回,當然,我們已經知道數據庫也是有緩存的。
整體來看,這是一個使用了多級緩存的系統。Nginx應用服務器的本地緩存解決了熱點數據的緩存問題,Redis分布式緩存集群減少了訪問回源率,Tomcat應用集群使用的平臺級緩存防止了相關緩存失效崩潰之后的沖擊,數據庫緩存提升數據庫查詢時的效率。正是多級緩存的使用,才能保障系統具備優良的性能。
攜程金融在Redis的實踐
攜程金融形成了自頂向下的多層次系統架構,如業務層、平臺層、基礎服務層等,其中用戶信息、產品信息、訂單信息等基礎數據由基礎平臺等底層系統產生,服務于所有的金融系統,對這部分基礎數據我們引入了統一的緩存服務(系統名utag)。
緩存數據有三大特點:全量、準實時、永久有效,在數據實時性要求不高的場景下,業務系統可直接調用統一的緩存查詢接口。
在構建此統一緩存服務時候,有三個關鍵目標:
-
數據準確性:數據庫中單條數據的更新一定要準確同步到緩存服務。
-
數據完整性:將對應數據庫表的全量數據進行緩存且永久有效,從而可以替代對應的數據庫查詢。
-
系統可用性:我們多個產品線的多個核心服務都已經接入,utag的高可用性顯得尤為關鍵。
整體方案
系統在多地都有部署,故緩存服務也做了相應的異地多機房部署,一來可以讓不同地區的服務調用本地區服務,無需跨越網絡專線,二來也可以作為一種災備方案,增加可用性。
對于緩存的寫入,由于緩存服務是獨立部署的,因此需要感知業務數據庫數據變更然后觸發緩存的更新,本著“可以多次更新,但不能漏更新”的原則,設計了多種數據更新觸發源:定時任務掃描,業務系統MQ、binlog變更MQ,相互之間作為互補來保證數據不會漏更新。
對于MQ使用攜程開源消息中間件QMQ 和 Kafka,在公司內部QMQ和Kafka也做了異地機房的互通。
使用MQ來驅動多地多機房的緩存更新,在不同的觸發源觸發后,會查詢最新的數據庫數據,然后發出一個緩存更新的MQ消息,不同地區機房的緩存系統同時監聽該主題并各自進行緩存的更新。
對于緩存的讀取,utag系統提供dubbo協議的緩存查詢接口,業務系統可調用本地區的接口,省去了網絡專線的耗時(50ms延遲)。在utag內部查詢redis數據,并反序列化為對應的業務model,再通過接口返回給業務方。
數據準確性
不同的觸發源,對緩存更新過程是一樣的,整個更新步驟可抽象為4步:
step1:觸發更新,查詢DB中的新數據,并發送統一的MQ
step2:接收MQ,查詢緩存中的老數據
step3:新老數據對比,判斷是否需要更新
step4:若需要,則更新緩存
并發控制?
若一條數據庫數據出現了多次更新,且剛好被不同的觸發源觸發,更新緩存時候若未加控制,可能出現數據更新錯亂,如下圖所示:
需要將第2、3、4步加鎖,使得緩存刷新操作全部串行化。由于utag本身就依賴了redis,此處我們的分布式鎖就基于redis實現。
基于updateTime的更新順序控制
即使加了鎖,也需要進一步判斷當前數據庫數據與緩存數據的新老,因為到達緩存更新流程的順序并不代表數據的真正更新順序。我們通過對比新老數據的更新時間來實現數據更新順序的控制。若新數據的更新時間大于老數據的更新時間,則認為當前數據可以直接寫入緩存。
我們系統從建立之初就有自己的MySQL規范,每張表都必須有update_time字段,且設置為ON
UPDATE CURRENT_TIMESTAMP,但是并沒有約束時間字段的精度,大部分都是秒級別的,因此在同一秒內的多次更新操作就無法識別出數據的新老。
針對同一秒數據的更新策略我們采用的方案是:先進行數據對比,若當前數據與緩存數據不相等,則直接更新,并且發送一條延遲消息,延遲1秒后再次觸發更新流程。
舉個例子:假設同一秒內同一條數據出現了兩次更新,value=1和value=2,期望最終緩存中的數據是value=2。若這兩次更新后的數據被先后觸發,分兩種情況:
case1:若value=1先更新,value=2后更新,(兩者都可更新到緩存中,因為雖然是同一秒,但是值不相等)則緩存中最終數據為value=2。
case2:若value=2先更新,value=1后更新,則第一輪更新后緩存數據為value=1,不是期望數據,之后對比發現是同一秒數據后會通過消息觸發二次更新,重新查詢數據庫數據為value=2,可以更新到緩存中。如下圖所示:
數據完整性設計
數據準確性是從單條數據更新角度的設計,而我們構建緩存服務的目的是替代對應數據庫表的查詢,因此需要緩存對應數據庫表的全量數據,而數據的完整性從以下三個方面得到保證:
(1)“把雞蛋放到多個籃子里”,使用多種觸發源(定時任務,業務MQ,binlog MQ)來最大限度降低單條數據更新缺失的可能性。
單一觸發源有可能出現問題,比如消息類的觸發依賴業務系統、中間件canel、中間件QMQ和Kafka,掃表任務依賴分布式調度平臺、MySQL等。中間任何一環都可能出現問題,而這些中間服務同時出概率的可能相對來說就極小了,相互之間可以作為互補。
(2)全量數據刷新任務:全表掃描定時任務,每周執行一次來進行兜底,確保緩存數據的全量準確同步。
(3)數據校驗任務:監控Redis和數據庫數據是否同步并進行補償。