分布式緩存數據一致性
Ⅰ 大數據環境下分布式文件系統有哪些特點,相應的優化思路是什麼
分布式元數據管理:分布式元數據管理主要通過元數據服務分布式部署的方式,實現了元數據分布式管理,解決一般分布式文件系統的單元數據服務節點導致的響應用戶請求效率不高、存儲文件數目受限和單點故障等問題,具有降低用戶請求處理延遲,提高分布式文件系統的可擴展性和可用性的特性。一般包括完全分布式架構、元數據訪問負載均衡、元數據伺服器高效索引、元數據伺服器彈性伸縮等技術點。
多層級存儲管理:多層級存儲管理用於實現內存 / SSD/HDD 等異構存儲設備的池化管理,以及各類存儲設備的動態接入管理,通過設備抽象和提供統一命名空間,面向分布式文件系統提供統一的存儲資源池,支持熱點數據自動感知和智能化存儲調度,最大程度提升數據存儲與訪問的效能。一般包括異構存儲設備管理、多存儲系統適配、統一命名空間、基於熱度的存儲資源調度等技術點廳冊帶。
數據一致性保障:數據一致性保障主要解決分布式文件系統中多副本和緩存等在數據存儲與訪問過程中的一致性問題,通過構建數據一致性模型、進行數據一致性校驗等方式,保障數據在存儲和訪問過程中的一致性,在提升數據訪問性能的同時確保數據存儲和訪問的正確性。一般包括一致性協議優化、一致性檢驗等技術點。
高並行讀寫優化:高並行讀寫優化用於提高分布式文件讀寫的並行化水平,最大化提升分布式文件系統下的數據訪問效率。一般包括分布式數據訪問緩存管理和調度演算法優化、IO 演算法優化和合並 IO 等技術點。
分布式散列與動態均衡:分布式散列與動態均衡實現分布式文件系統下高性能的數據塊定位,提高數據訪問性能,以及數據塊的遷移和再平衡,提升分布式文件系統的穩定性和可持續服務能力。一般包扮蘆括基於一致性哈希的數據塊索引管理、動態數據再平衡等技術點。
存儲高可用:存儲高可用通過數據多副本技術、狀態自檢測和自修復、核心服務分布式部署等技術手段,實現自動檢測分布式文件系統中的各種錯誤和失效,並且在文件系統出現錯誤和失效時可自行進行多副本間的數據修復,最終持續向用戶提供正常的數據訪問服務。一般包括可配置數據多副本、數據自恢復及自維護等技術點。
海量小文件高性能存儲訪問:海量小文件高性能存儲訪問主要採用小文件匯集成大文件進姿睜行存儲、細粒度二級索引管理等技術,實現在現有分布式文件系統的基礎上,擴展對海量小文件的存儲與訪問的能力,同時解決小文件的隨機讀寫問題,大大提高分布式文件系統對海量小文件的存儲訪問效率。
Ⅱ Redis-分布式緩存一致性解決方案
一致性Hash演算法也使用取模的方法,剛才描述的慎行備取模法是對伺服器的數量進行寬毀取模,而一致性Hash演算法是對2^32取模
首先,我們把2^32 想像成一帶滾個圓,圓上一共有2^32 個點,編號0-2^32-1,這個圓稱為hash環
Ⅲ 華為技術架構師分享:高並發場景下緩存處理的一些思路
在實際的開發當中,我們經常需要進行磁碟數據的讀取和搜索,因此經常會有出現從資料庫讀取數據的場景出現。但是當數據訪問量次數增大的時候,過多的磁碟讀取可能會最終成為整個系統的性能瓶頸,甚至是壓垮整個資料庫,導致系統卡死等嚴重問題。
常規的應用系統中,我們通常會在需要的時候對資料庫進行查找,因此系統的大致結構如下所示:
1.緩存和資料庫之間數據一致性問題
常用於緩存處理的機制我總結為了以下幾種:
首先來簡單說說Cache aside的這種方式:
Cache Aside模式
這種模式處理緩存通常都是先從資料庫緩存查詢,如果緩存沒有命中則從資料庫中進行查找。
這裡面會發生的三種情況如下:
緩存命中:
當查詢的時候發現緩存存在,那麼直接從緩存中提取。
緩存失效:
當緩存沒有數據的時候,則從database裡面讀取源數據,再加入到cache裡面去。
緩存更新:
當有新的寫操作去修改database裡面的數據時,需要在寫操作完成之後,讓cache裡面對應的數據失效。
關於這種模式下依然會存在缺陷。比如,一個是讀操作,但是沒有命中緩存,然後就到資料庫中取數據,此時來了一個寫操作,寫完資料庫後,讓緩存失效,然後,之前的那個讀操作再把老的數據放進去,所以,會造成臟數據。
Facebook的大牛們也曾經就緩存處理這個問題發表過相關的論文,鏈接如下:
分布式環境中要想完全的保證數據一致性是一件極為困難的事情,我們只能夠盡可能的減低這種數據不一致性問題產生的情況。
Read Through模式
Read Through模式是指應用程序始終從緩存中請求數據。 如果緩存沒有數據,則它負責使用底層提供程序插件從資料庫中檢索數據。 檢索數據後,緩存會自行更新並將數據返回給調用應用程序。使用Read Through 有一個好處。
我們總是使用key從緩存中檢索數據, 調用的應用程序不知道資料庫, 由存儲方來負責自己的緩存處理,這使代碼更具可讀性, 代碼更清晰。但是這也有相應的缺陷,開發人員需要給編寫相關的程序插件,增加了開發的難度性。
Write Through模式
Write Through模式和Read Through模式類似,當數據發生更新的時候,先去Cache裡面進行更新,如果命中了,則先更新緩存再由Cache方來更新database。如果沒有命中的話,就直接更新Cache裡面的數據。
2.緩存穿透問題
在高並發的場景中,緩存穿透是一個經常都會遇到的問題。
什麼是緩存穿透?
大量的請求在緩存中沒有查詢到指定的數據,因此需要從資料庫中進行查詢,造成緩存穿透。
會造成什麼後果?
大量的請求短時間內湧入到database中進行查詢會增加database的壓力,最終導致database無法承載客戶單請求的壓力,出現宕機卡死等現象。
常用的解決方案通常有以下幾類:
1.空值緩存
在某些特定的業務場景中,對於數據的查詢可能會是空的,沒有實際的存在,並且這類數據信息在短時間進行多次的反復查詢也不會有變化,那麼整個過程中,多次的請求資料庫操作會顯得有些多餘。
不妨可以將這些空值(沒有查詢結果的數據)對應的key存儲在緩存中,那麼第二次查找的時候就不需要再次請求到database那麼麻煩,只需要通過內存查詢即可。這樣的做法能夠大大減少對於database的訪問壓力。
2.布隆過濾器
通常對於database裡面的數據的key值可以預先存儲在布隆過濾器裡面去,然後先在布隆過濾器裡面進行過濾,如果發現布隆過濾器中沒有的話,就再去redis裡面進行查詢,如果redis中也沒有數據的話,再去database查詢。這樣可以避免不存在的數據信息也去往存儲庫中進行查詢情況。
什麼是緩存雪崩?
當緩存伺服器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給後端系統(比如DB)帶來很大壓力。
如何避免緩存雪崩問題?
1.使用加鎖隊列來應付這種問題。當有多個請求湧入的時候,當緩存失效的時候加入一把分布式鎖,只允許搶鎖成功的請求去庫裡面讀取數據然後將其存入緩存中,再釋放鎖,讓後續的讀請求從緩存中取數據。但是這種做法有一定的弊端,過多的讀請求線程堵塞,將機器內存占滿,依然沒有能夠從根本上解決問題。
2.在並發場景發生前,先手動觸發請求,將緩存都存儲起來,以減少後期請求對database的第一次查詢的壓力。數據過期時間設置盡量分散開來,不要讓數據出現同一時間段出現緩存過期的情況。
3.從緩存可用性的角度來思考,避免緩存出現單點故障的問題,可以結合使用 主從+哨兵的模式來搭建緩存架構,但是這種模式搭建的緩存架構有個弊端,就是無法進行緩存分片,存儲緩存的數據量有限制,因此可以升級為Redis Cluster架構來進行優化處理。(需要結合企業實際的經濟實力,畢竟Redis Cluster的搭建需要更多的機器)
4.Ehcache本地緩存 + Hystrix限流&降級,避免MySQL被打死。
使用 Ehcache本地緩存的目的也是考慮在 Redis Cluster 完全不可用的時候,Ehcache本地緩存還能夠支撐一陣。
使用 Hystrix進行限流 & 降級 ,比如一秒來了5000個請求,我們可以設置假設只能有一秒 2000個請求能通過這個組件,那麼其他剩餘的 3000 請求就會走限流邏輯。
然後去調用我們自己開發的降級組件(降級),比如設置的一些默認值呀之類的。以此來保護最後的 MySQL 不會被大量的請求給打死。
Ⅳ 分布式緩存中,哈希取余分區和一致性哈希分區有什麼區別
環割法(一致性 hash)環割法的原理如下:
1. 初始化的時候生成分片數量 X × 環割數量 N 的固定方式編號的字元串,例如 SHARD-1-NODE-1,並計算所有 X×N 個字元串的所有 hash 值。
2. 將所有計算出來的 hash 值放到一個排序的 Map 中,並將其中的所有元素進行排序。
3. 輸入字元串的時候計算輸入字元串的 hash 值,查看 hash 值介於哪兩個元素之間,取小於 hash 值的那個元素對應的分片為數據的分片。
數據比較
下面將通過測試對環割法和跳躍法的性能及均衡性進行對比,說明 DBLE 為何使用跳躍法代替了環割法。
數據源:現場數據 350595 條
測試經過:
1. 通過各自的測試方法執行對於測試數據的分片任務。
2. 測試方法侍嫌豎:記錄老大分片結果的方差;記錄從開始分片至分片結束的時間;記錄分片結果與平均數的最大差值。
3. 由於在求模法 PartitionByString 的方法中要求分片的數量是 1024 的因數,所以測試過程只能使用 2 的指數形式進行測試,並在 PartitionByString 方法進行測試的時候不對於 MAC 地址進行截斷,取全量長度進行測試。
Ⅳ chcahe 如何保證分布式緩存數據一致性
VPLEX的技術核心是「分布式緩存一致性」,下圖則是「分布式緩存一致性」技術的工作機制示意:正是因為這項核心技術優勢,使得VPLEX方案和目前所有廠商的虛擬化方案截然不同,並能夠實現異地的數據中心整合。對跨數據中心的所有負載實現跨引擎的平攤或者實時遷移,來自任何一個主機的I/O請求可以通過任何一個引擎得到響應。
緩存一致性的記錄目錄使用少量的元數據,記錄下哪個數據塊屬於哪個引擎更新的,以及在何時更新過,並通過4K大小的數據塊告訴在集群中的所有其他的引擎。在整個過程中實際發生的溝通過程,遠遠比實際上正在更新數據塊少很多。
分布式緩存一致性數據流示意圖:上方是一個目錄,記錄下左側的主機讀取緩存A的操作,並分發給所有引擎,右側主機需要讀取該數據塊時,會先通過目錄查詢,確定該數據塊所屬的引擎位置,讀取請求會直接發送給引擎,並直接從數據塊所在的緩存上讀取。
當一個讀請求進入時,VPLEX會自動檢查目錄,查找該數據塊所屬的引擎,一旦確定該數據塊所屬的引擎位置,讀的請求會直接發送給該引擎。一旦一個寫入動作完成,並且目錄表被修改,這時另一個讀請求從另一個引擎過來,VPLEX會檢查目錄,並且直接從該引擎的緩存上讀取。如果該數據仍然在緩存上,則完全沒必要去磁碟上讀取。
如上圖,來自圖中左側主機的操作,由Cache A服務,會記錄一個更新狀態,並分發給所有所有引擎知道。如果讀取的需求來自最右側的伺服器,首先通過目錄查詢。通過這種技術可以實現所有引擎一致性工作,而且這個技術不僅可以跨引擎還可以跨VPLEX集群,而VPLEX集群可以跨區域,因此緩存一致性也可以跨區域部署。
分布式緩存一致性技術使VPLEX相比傳統的虛擬化方案擁有更高的性能和可靠性,並實現異地數據中心的虛擬化整合
對傳統的虛擬化架構來說,如果虛擬化的I/O集群中有一個節點壞了,那麼性能就會降低一半,而且實際情況降低不止一半。因為壞了一個節點,這個節點緩存一般會被寫進去。因為沒有緩存,操作會直接寫到硬碟里。如果圖中中心這個節點壞掉,那主機所有的可用性都沒有了。而VPLEX如果有一個引擎或者一個控制器壞掉了,那這個引擎的負載會均攤到其他活動引擎上。這樣總體來講用戶可以維持可預知性能,性能降低也不那麼明顯。
Ⅵ Redis分布式緩存搭建
花了兩天時間整理了之前記錄的Redis單體與哨兵模式的搭建與使用,又補齊了集群模式的使用和搭建經驗,並對集群的一些個原理做了理解。
筆者安裝中遇到的一些問題:
如果make報錯,可能是沒裝gcc或者gcc++編輯器,安裝之 yum -y install gcc gcc-c++ kernel-devel ,有可能還是提示一些個c文件編譯不過,gcc -v查看下版本,如果不到5.3那麼升級一下gcc:
在 /etc/profile 追加一行 source /opt/rh/devtoolset-9/enable
scl enable devtoolset-9 bash
重新make clean, make
這回編譯通過了,提示讓你最好make test一下/
執行make test ,如果提示 You need tcl 8.5 or newer in order to run the Redis test
那就升級tcl, yum install tcl
重新make test,如果還有error就刪了目錄,重新tar包解壓重新make , make test
o/ All tests passed without errors! ,表示編譯成功。
然後make install即可。
直接運行命令: ./redis-server /usr/redis-6.0.3/redis.conf &
redis.conf 配置文件里 bind 0.0.0.0 設置外部訪問, requirepass xxxx 設置密碼。
redis高可用方案有兩種:
常用搭建方案為1主1從或1主2從+3哨兵監控主節點, 以及3主3從6節點集群。
(1)sentinel哨兵
/usr/redis-6.0.3/src/redis-sentinel /usr/redis-6.0.3/sentinel2.conf &
sentinel2.conf配置:
坑1:master節點也會在故障轉移後成為從節點,也需要配置masterauth
當kill master進程之後,經過sentinel選舉,slave成為了新的master,再次啟動原master,提示如下錯誤:
原因是此時的master再次啟動已經是slave了,需要向現在的新master輸入密碼,所以需要在master.conf
中配置:
坑2:哨兵配置文件要暴露客戶端可以訪問到的master地址
在 sentinel.conf 配置文件的 sentinel monitor mymaster 122.xx.xxx.xxx 6379 2 中,配置該哨兵對應的master名字、master地址和埠,以及達到多少個哨兵選舉通過認為master掛掉。其中master地址要站在redis訪問者(也就是客戶端)的角度、配置訪問者能訪問的地址,例如sentinel與master在一台伺服器(122.xx.xxx.xxx)上,那麼相對sentinel其master在本機也就是127.0.0.1上,這樣 sentinel monitor mymaster 127.0.0.1 6379 2 邏輯上沒有問題,但是如果另外伺服器上的springboot通過lettuce訪問這個redis哨兵,則得到的master地址為127.0.0.1,也就是springboot所在伺服器本機,這顯然就有問題了。
附springboot2.1 redis哨兵配置:
坑3:要注意配置文件.conf會被哨兵修改
redis-cli -h localhost -p 26379 ,可以登到sentinel上用info命令查看一下哨兵的信息。
曾經遇到過這樣一個問題,大致的信息如下
slaves莫名其妙多了一個,master的地址也明明改了真實對外的地址,這里又變成127.0.0.1 !
最後,把5個redis進程都停掉,逐個檢查配置文件,發現redis的配置文件在主從哨兵模式會被修改,master的配置文件最後邊莫名其妙多了一行replicaof 127.0.0.1 7001, 懷疑應該是之前配置錯誤的時候(見坑2)被哨兵動態加上去的! 總之,實踐中一定要多注意配置文件的變化。
(2)集群
當數據量大到一定程度,比如幾十上百G,哨兵模式不夠用了需要做水平拆分,早些年是使用codis,twemproxy這些第三方中間件來做分片的,即 客戶端 -> 中間件 -> Redis server 這樣的模式,中間件使用一致性Hash演算法來確定key在哪個分片上。後來Redis官方提供了方案,大家就都採用官方的Redis Cluster方案了。
Redis Cluster從邏輯上分16384個hash slot,分片演算法是 CRC16(key) mod 16384 得到key應該對應哪個slot,據此判斷這個slot屬於哪個節點。
每個節點可以設置1或多個從節點,常用的是3主節點3從節點的方案。
reshard,重新分片,可以指定從哪幾個節點移動一些hash槽到另一個節點去。重新分片的過程對客戶端透明,不影響線上業務。
搭建Redis cluster
redis.conf文件關鍵的幾個配置:
啟動6個集群節點
[root@VM_0_11_centos redis-6.0.3]# ps -ef|grep redis
root 5508 1 0 21:25 ? 00:00:00 /usr/redis-6.0.3/src/redis-server 0.0.0.0:7001 [cluster]
root 6903 1 0 21:32 ? 00:00:00 /usr/redis-6.0.3/src/redis-server 0.0.0.0:7002 [cluster]
root 6939 1 0 21:33 ? 00:00:00 /usr/redis-6.0.3/src/redis-server 0.0.0.0:7003 [cluster]
root 6966 1 0 21:33 ? 00:00:00 /usr/redis-6.0.3/src/redis-server 0.0.0.0:7004 [cluster]
root 6993 1 0 21:33 ? 00:00:00 /usr/redis-6.0.3/src/redis-server 0.0.0.0:7005 [cluster]
root 7015 1 0 21:33 ? 00:00:00 /usr/redis-6.0.3/src/redis-server 0.0.0.0:7006 [cluster]
這時候這6個節點還是獨立的,要把他們配置成集群:
說明: -a xxxx 是因為筆者在redis.conf中配置了requirepass xxxx密碼,然後 --cluster-replicas 1 中的1表示每個master節點有1個從節點。
上述命令執行完以後會有一個詢問: Can I set the above configuration? yes同意自動做好的分片即可。
最後 All 16384 slots covered. 表示集群中16384個slot中的每一個都有至少有1個master節點在處理,集群啟動成功。
查看集群狀態:
坑1:暴露給客戶端的節點地址不對
使用lettuce連接發現連不上,查看日誌 Connection refused: no further information: /127.0.0.1:7002 ,跟之前哨兵配置文件sentinel.conf里邊配置master地址犯的錯誤一樣,集群啟動的時候帶的地址應該是提供給客戶端訪問的地址。
我們要重建集群:先把6個redis進程停掉,然後刪除 nodes-7001.conf 這些節點配置文件,刪除持久化文件 mp.rdb 、 appendonly.aof ,重新啟動6個進程,在重新建立集群:
然後,還是連不上,這次報錯 connection timed out: /172.xx.0.xx:7004 ,發現連到企鵝雲伺服器的內網地址上了!
解決辦法,修改每個節點的redis.conf配置文件,找到如下說明:
所以增加配置:
然後再重新構建集群,停進程、改配置、刪除節點文件和持久化文件、啟動進程、配置集群。。。再來一套(累死了)
重新使用Lettuce測試,這次終於連上了!
坑2:Lettuce客戶端在master節點故障時沒有自動切換到從節點
name這個key在7002上,kill這個進程模擬master下線,然後Lettuce一直重連。我們期望的是應該能自動切換到其slave 7006上去,如下圖:
重新啟動7002進程,
7006已成為新master,7002成為它的slave,然後Lettuce也能連接上了。
解決辦法,修改Lettuce的配置:
筆者用的是springboot 2.1 spring-boot-starter-data-redis 默認的Lettuce客戶端,當使用Redis cluster集群模式時,需要配置一下 RedisConnectionFactory 開啟自適應刷新來做故障轉移時的自動切換從節點進行連接。
重新測試:停掉master 7006,這次Lettuce可以正常切換連到7002slave上去了。(仍然會不斷的在日誌里報連接錯誤,因為需要一直嘗試重連7006,但因為有7002從節點頂上了、所以應用是可以正常使用的)
Redis不保證數據的強一致性
Redis並不保證數據的強一致性,也就是取CAP定理中的AP
關於一致性Hash演算法,可以參考 一致性Hash演算法 - (jianshu.com)
Redis cluster使用的是hash slot演算法,跟一致性Hash演算法不太一樣,固定16384個hash槽,然後計算key落在哪個slot里邊(計算key的CRC16值再對16384取模),key找的是slot而不是節點,而slot與節點的對應關系可以通過reshard改變並通過gossip協議擴散到集群中的每一個節點、進而可以為客戶端獲知,這樣key的節點定址就跟具體的節點個數沒關系了。也同樣解決了普通hash取模演算法當節點個數發生變化時,大量key對應的定址都發生改動導致緩存失效的問題。
比如集群增加了1個節點,這時候如果不做任何操作,那麼新增加的這個節點上是沒有slot的,所有slot都在原來的節點上且對應關系不變、所以沒有因為節點個數變動而緩存失效,當reshard一部分slot到新節點後,客戶端獲取到新遷移的這部分slot與新節點的對應關系、定址到新節點,而沒遷移的slot仍然定址到原來的節點。
關於熱遷移,猜想,內部應該是先做復制遷移,等遷移完了,再切換slot與節點的對應關系,復制沒有完成之前仍按照原來的slot與節點對應關系去原節點訪問。復制結束之後,再刪除原節點上已經遷移的slot所對應的key。
與哨兵模式比較類似,當1個節點發現某個master節點故障了、會對這個故障節點進行pfail主觀宕機,然後會通過gossip協議通知到集群中的其他節點、其他節點也執行判斷pfail並gossip擴散廣播這一過程,當超過半數節點pfail時那麼故障節點就是fail客觀宕機。接下來所有的master節點會在故障節點的從節點中選出一個新的主節點,此時所有的master節點中超過半數的都投票選舉了故障節點的某個從節點,那麼這個從節點當選新的master節點。
所有節點都持有元數據,節點之間通過gossip這種二進制協議進行通信、發送自己的元數據信息給其他節點、故障檢測、集群配置更新、故障轉移授權等等。
這種去中心化的分布式節點之間內部協調,包括故障識別、故障轉移、選主等等,核心在於gossip擴散協議,能夠支撐這樣的廣播協議在於所有的節點都持有一份完整的集群元數據,即所有的節點都知悉當前集群全局的情況。
Redis高可用方案 - (jianshu.com)
面試題:Redis 集群模式的工作原理能說一下么 - 雲+社區 - 騰訊雲 (tencent.com)
深度圖解Redis Cluster原理 - detectiveHLH - 博客園 (cnblogs.com)
Redis學習筆記之集群重啟和遇到的坑-阿里雲開發者社區 (aliyun.com)
雲伺服器Redis集群部署及客戶端通過公網IP連接問題
Ⅶ 分布式系統常用的一致性演算法有哪些
在做伺服器負載均衡時候可供選擇的負載均衡的演算法有很多,包括: 輪循演算法(Round Robin)、哈希演算法(HASH)、最少連接演算法(Least Connection)、響應速度演算法(Response Time)、加權法(Weighted )等。其中哈希演算法是最為常用的演算法. 典型的應用場景是: 有N台伺服器提供緩存服務,需要對伺服器進行負載均衡,將請求平均分發到每台伺服器上,每台機器負責1/N的服務。 常用的演算法是對hash結果取余數 (hash() mod N):對機器編號從0到N-1,按照自定義的hash()演算法,對每個請求的hash()值按N取模,得到余數i,然後將請求分發到編號為i的機器。但這樣的演算法方法存在致命問題,如果某一台機器宕機,那麼應該落在該機器的請求就無法得到正確的處理,這時需要將當掉的伺服器從演算法從去除,此時候會有(N-1)/N的伺服器的緩存數據需要重新進行計算;如果新增一台機器,會有N /(N+1)的伺服器的緩存數據需要進行重新計算。對於系統而言,這通常是不可接受的顛簸(因為這意味著大量緩存的失效或者數據需要轉移)。那麼,如何設計一個負載均衡策略,使得受到影響的請求盡可能的少呢? 在Memcached、Key-Value Store、Bittorrent DHT、LVS中都採用了Consistent Hashing演算法,可以說Consistent Hashing 是分布式系統負載均衡的首選演算法。 1、Consistent Hashing演算法描述 下面以Memcached中的Consisten Hashing演算法為例說明。 由於hash演算法結果一般為unsigned int型,因此對於hash函數的結果應該均勻分布在[0,232-1]間,如果我們把一個圓環用232 個點來進行均勻切割,首先按照hash(key)函數算出伺服器(節點)的哈希值, 並將其分布到0~232的圓上。 用同樣的hash(key)函數求出需要存儲數據的鍵的哈希值,並映射到圓上。然後從數據映射到的位置開始順時針查找,將數據保存到找到的第一個伺服器(節點)上。 Consistent Hashing原理示意圖 新增一個節點的時候,只有在圓環上新增節點逆時針方向的第一個節點的數據會受到影響。刪除一個節點的時候,只有在圓環上原來刪除節點順時針方向的第一個節點的數據會受到影響,因此通過Consistent Hashing很好地解決了負載均衡中由於新增節點、刪除節點引起的hash值顛簸問題。 Consistent Hashing添加伺服器示意圖 虛擬節點(virtual nodes):之所以要引進虛擬節點是因為在伺服器(節點)數較少的情況下(例如只有3台伺服器),通過hash(key)算出節點的哈希值在圓環上並不是均勻分布的(稀疏的),仍然會出現各節點負載不均衡的問題。虛擬節點可以認為是實際節點的復製品(replicas),本質上與實際節點實際上是一樣的(key並不相同)。引入虛擬節點後,通過將每個實際的伺服器(節點)數按照一定的比例(例如200倍)擴大後並計算其hash(key)值以均勻分布到圓環上。在進行負載均衡時候,落到虛擬節點的哈希值實際就落到了實際的節點上。由於所有的實際節點是按照相同的比例復製成虛擬節點的,因此解決了節點數較少的情況下哈希值在圓環上均勻分布的問題。 虛擬節點對Consistent Hashing結果的影響 從上圖可以看出,在節點數為10個的情況下,每個實際節點的虛擬節點數為實際節點的100-200倍的時候,結果還是很均衡的。 第3段中有這些文字:「但這樣的演算法方法存在致命問題,如果某一台機器宕機,那麼應該落在該機器的請求就無法得到正確的處理,這時需要將當掉的伺服器從演算法從去除,此時候會有(N-1)/N的伺服器的緩存數據需要重新進行計算;」 為何是 (N-1)/N 呢?解釋如下: 比如有 3 台機器,hash值 1-6 在這3台上的分布就是: host 1: 1 4 host 2: 2 5 host 3: 3 6 如果掛掉一台,只剩兩台,模數取 2 ,那麼分布情況就變成: host 1: 1 3 5 host 2: 2 4 6 可以看到,還在數據位置不變的只有2個: 1,2,位置發生改變的有4個,占共6個數據的比率是 4/6 = 2/3這樣的話,受影響的數據太多了,勢必太多的數據需要重新從 DB 載入到 cache 中,嚴重影響性能 【consistent hashing 的辦法】 上面提到的 hash 取模,模數取的比較小,一般是負載的數量,而 consistent hashing 的本質是將模數取的比較大,為 2的32次方減1,即一個最大的 32 位整數。然後,就可以從容的安排數據導向了,那個圖還是挺直觀的。 以下部分為一致性哈希演算法的一種PHP實現。點擊下載
Ⅷ 如何保證資料庫緩存的最終一致性
對於互聯網業務來說,傳統的直接訪問資料庫方式,主要通過數據分片、一主多從等方式來扛住讀寫流量,但隨著數據量的積累和流量的激增,僅依賴資料庫來承接所有流量,不僅成本高、效率低、而且還伴隨著穩定性降低的風險。
鑒於大部分業務通常是讀多寫少(讀取頻率遠遠高於更新頻率),甚至存在讀操作數量高出寫操作多個數量級的情況。因此, 在架構設計中,常採用增加緩存層來提高系統的響應能力 ,提升數據讀寫性能、減少資料庫訪問壓力,從而提升業務的穩定性和訪問體驗。
根據 CAP 原理,分布式系統在可用性、一致性和分區容錯性上無法兼得,通常由於分區容錯無法避免,所以一致性和可用性難以同時成立。對於緩存系統來說, 如何保證其數據一致性是一個在應用緩存的同時不得不解決的問題 。
需要明確的是,緩存系統的數據一致性通常包括持久化層和緩存層的一致性、以及多級緩存之間的一致性,這里我們僅討論前者。持久化層和緩存層的一致性問題也通常被稱為雙寫一致性問題,「雙寫」意為數據既在資料庫中保存一份,也在緩存中保存一份。
對於一致性來說,包含強一致性和弱一致性 ,強一致性保證寫入後立即可以讀取,弱一致性則不保證立即可以讀取寫入後的值,而是盡可能的保證在經過一定時間後可以讀取到,在弱一致性中應用最為廣泛的模型則是最終一致性模型,即保證在一定時間之後寫入和讀取達到一致的狀態。對於應用緩存的大部分場景來說,追求的則是最終一致性,少部分對數據一致性要求極高的場景則會追求強一致性。
為了達到最終一致性,針對不同的場景,業界逐步形成了下面這幾種應用緩存的策略。
— 1 —
Cache-Aside
Cache-Aside 意為旁路緩存模式,是應用最為廣泛的一種緩存策略。下面的圖示展示了它的讀寫流程,來看看它是如何保證最終一致性的。在讀請求中,首先請求緩存,若緩存命中(cache hit),則直接返回緩存中的數據;若緩存未命中(cache miss),則查詢資料庫並將查詢結果更新至緩存,然後返回查詢出的數據(demand-filled look-aside )。在寫請求中,先更新資料庫,再刪除緩存(write-invalidate)。
1、為什麼刪除緩存,而不是更新緩存?
在 Cache-Aside 中,對於讀請求的處理比較容易理解,但在寫請求中,可能會有讀者提出疑問,為什麼要刪除緩存,而不是更新緩存?站在符合直覺的角度來看,更新緩存是一個容易被理解的方案,但站在性能和安全的角度,更新緩存則可能會導致一些不好的後果。
首先是性能 ,當該緩存對應的結果需要消耗大量的計算過程才能得到時,比如需要訪問多張資料庫表並聯合計算,那麼在寫操作中更新緩存的動作將會是一筆不小的開銷。同時,當寫操作較多時,可能也會存在剛更新的緩存還沒有被讀取到,又再次被更新的情況(這常被稱為緩存擾動),顯然,這樣的更新是白白消耗機器性能的,會導致緩存利用率不高。
而等到讀請求未命中緩存時再去更新,也符合懶載入的思路,需要時再進行計算。刪除緩存的操作不僅是冪等的,可以在發生異常時重試,而且寫-刪除和讀-更新在語義上更加對稱。
其次是安全 ,在並發場景下,在寫請求中更新緩存可能會引發數據的不一致問題。參考下面的圖示,若存在兩個來自不同線程的寫請求,首先來自線程 1 的寫請求更新了資料庫(step 1),接著來自線程 2 的寫請求再次更新了資料庫(step 3),但由於網路延遲等原因,線程 1 可能會晚於線程 2 更新緩存(step 4 晚於 step 3),那麼這樣便會導致最終寫入資料庫的結果是來自線程 2 的新值,寫入緩存的結果是來自線程 1 的舊值,即緩存落後於資料庫,此時再有讀請求命中緩存(step 5),讀取到的便是舊值。
2、為什麼先更新資料庫,而不是先刪除緩存?
另外,有讀者也會對更新資料庫和刪除緩存的時序產生疑問,那麼為什麼不先刪除緩存,再更新資料庫呢?在單線程下,這種方案看似具有一定合理性,這種合理性體現在刪除緩存成功。
但更新資料庫失敗的場景下,盡管緩存被刪除了,下次讀操作時,仍能將正確的數據寫回緩存,相對於 Cache-Aside 中更新資料庫成功,刪除緩存失敗的場景來說,先刪除緩存的方案似乎更合理一些。那麼,先刪除緩存有什麼問題呢?
問題仍然出現在並發場景下,首先來自線程 1 的寫請求刪除了緩存(step 1),接著來自線程 2 的讀請求由於緩存的刪除導致緩存未命中,根據 Cache-Aside 模式,線程 2 繼而查詢資料庫(step 2),但由於寫請求通常慢於讀請求,線程 1 更新資料庫的操作可能會晚於線程 2 查詢資料庫後更新緩存的操作(step 4 晚於 step 3),那麼這樣便會導致最終寫入緩存的結果是來自線程 2 中查詢到的舊值,而寫入資料庫的結果是來自線程 1 的新值,即緩存落後於資料庫,此時再有讀請求命中緩存( step 5 ),讀取到的便是舊值。
另外,先刪除緩存,由於緩存中數據缺失,加劇資料庫的請求壓力,可能會增大緩存穿透出現的概率。
3、如果選擇先刪除緩存,再更新資料庫,那如何解決一致性問題呢?
為了避免「先刪除緩存,再更新資料庫」這一方案在讀寫並發時可能帶來的緩存臟數據,業界又提出了延時雙刪的策略,即在更新資料庫之後,延遲一段時間再次刪除緩存,為了保證第二次刪除緩存的時間點在讀請求更新緩存之後,這個延遲時間的經驗值通常應稍大於業務中讀請求的耗時。
延遲的實現可以在代碼中 sleep 或採用延遲隊列。顯而易見的是,無論這個值如何預估,都很難和讀請求的完成時間點准確銜接,這也是延時雙刪被詬病的主要原因。
4、那麼 Cache-Aside 存在數據不一致的可能嗎?
在 Cache-Aside 中,也存在數據不一致的可能性。在下面的讀寫並發場景下,首先來自線程 1 的讀請求在未命中緩存的情況下查詢資料庫(step 1),接著來自線程 2 的寫請求更新資料庫(step 2),但由於一些極端原因,線程 1 中讀請求的更新緩存操作晚於線程 2 中寫請求的刪除緩存的操作(step 4 晚於 step 3),那麼這樣便會導致最終寫入緩存中的是來自線程 1 的舊值,而寫入資料庫中的是來自線程 2 的新值,即緩存落後於資料庫,此時再有讀請求命中緩存(step 5),讀取到的便是舊值。
這種場景的出現,不僅需要緩存失效且讀寫並發執行,而且還需要讀請求查詢資料庫的執行早於寫請求更新資料庫,同時讀請求的執行完成晚於寫請求。足以見得,這種 不一致場景產生的條件非常嚴格,在實際的生產中出現的可能性較小 。
除此之外,在並發環境下,Cache-Aside 中也存在讀請求命中緩存的時間點在寫請求更新資料庫之後,刪除緩存之前,這樣也會導致讀請求查詢到的緩存落後於資料庫的情況。
雖然在下一次讀請求中,緩存會被更新,但如果業務層面對這種情況的容忍度較低,那麼可以採用加鎖在寫請求中保證「更新資料庫&刪除緩存」的串列執行為原子性操作(同理也可對讀請求中緩存的更新加鎖)。 加鎖勢必會導致吞吐量的下降,故採取加鎖的方案應該對性能的損耗有所預期。
— 2 —
補償機制
我們在上面提到了,在 Cache-Aside 中可能存在更新資料庫成功,但刪除緩存失敗的場景,如果發生這種情況,那麼便會導致緩存中的數據落後於資料庫,產生數據的不一致的問題。
其實,不僅 Cache-Aside 存在這樣的問題,在延時雙刪等策略中也存在這樣的問題。針對可能出現的刪除失敗問題,目前業界主要有以下幾種補償機制。
1、刪除重試機制
由於同步重試刪除在性能上會影響吞吐量,所以常通過引入消息隊列,將刪除失敗的緩存對應的 key 放入消息隊列中,在對應的消費者中獲取刪除失敗的 key ,非同步重試刪除。這種方法在實現上相對簡單,但由於刪除失敗後的邏輯需要基於業務代碼的 trigger 來觸發 ,對業務代碼具有一定入侵性。
鑒於上述方案對業務代碼具有一定入侵性,所以需要一種更加優雅的解決方案,讓緩存刪除失敗的補償機制運行在背後,盡量少的耦合於業務代碼。一個簡單的思路是通過後台任務使用更新時間戳或者版本作為對比獲取資料庫的增量數據更新至緩存中,這種方式在小規模數據的場景可以起到一定作用,但其擴展性、穩定性都有所欠缺。
一個相對成熟的方案是基於 MySQL 資料庫增量日誌進行解析和消費,這里較為流行的是阿里巴巴開源的作為 MySQL binlog 增量獲取和解析的組件 canal(類似的開源組件還有 Maxwell、Databus 等)。
canal sever 模擬 MySQL slave 的交互協議,偽裝為 MySQL slave,向 MySQL master 發送 mp 協議,MySQL master 收到 mp 請求,開始推送 binary log 給 slave (即 canal sever ),canal sever 解析 binary log 對象(原始為 byte 流),可由 canal client 拉取進行消費,同時 canal server 也默認支持將變更記錄投遞到 MQ 系統中,主動推送給其他系統進行消費。
在 ack 機制的加持下,不管是推送還是拉取,都可以有效的保證數據按照預期被消費。當前版本的 canal 支持的 MQ 有 Kafka 或者 RocketMQ。另外, canal 依賴 ZooKeeper 作為分布式協調組件來實現 HA ,canal 的 HA 分為兩個部分:
那麼,針對緩存的刪除操作便可以在 canal client 或 consumer 中編寫相關業務代碼來完成。這樣,結合資料庫日誌增量解析消費的方案以及 Cache-Aside 模型,在讀請求中未命中緩存時更新緩存(通常這里會涉及到復雜的業務邏輯),在寫請求更新資料庫後刪除緩存,並基於日誌增量解析來補償資料庫更新時可能的緩存刪除失敗問題,在絕大多數場景下,可以有效的保證緩存的最終一致性。
另外需要注意的是,還應該隔離事務與緩存,確保資料庫入庫後再進行緩存的刪除操作。 比如考慮到資料庫的主從架構,主從同步及讀從寫主的場景下,可能會造成讀取到從庫的舊數據後便更新了緩存,導致緩存落後於資料庫的問題,這就要求對緩存的刪除應該確保在資料庫操作完成之後。所以,基於 binlog 增量日誌進行數據同步的方案,可以通過選擇解析從節點的 binlog,來避免主從同步下刪除緩存過早的問題。
3、數據傳輸服務 DTS
— 3 —
Read-Through
Read-Through 意為讀穿透模式,它的流程和 Cache-Aside 類似,不同點在於 Read-Through 中多了一個訪問控制層,讀請求只和該訪問控制層進行交互,而背後緩存命中與否的邏輯則由訪問控制層與數據源進行交互,業務層的實現會更加簡潔,並且對於緩存層及持久化層交互的封裝程度更高,更易於移植。
— 4 —
Write-Through
Write-Through 意為直寫模式,對於 Write-Through 直寫模式來說,它也增加了訪問控制層來提供更高程度的封裝。不同於 Cache-Aside 的是,Write-Through 直寫模式在寫請求更新資料庫之後,並不會刪除緩存,而是更新緩存。
這種方式的 優勢在於讀請求過程簡單 ,不需要查詢資料庫更新緩存等操作。但其劣勢也非常明顯,除了上面我們提到的更新資料庫再更新緩存的弊端之外,這種方案還會造成更新效率低,並且兩個寫操作任何一次寫失敗都會造成數據不一致。
如果要使用這種方案, 最好可以將這兩個操作作為事務處理,可以同時失敗或者同時成功,支持回滾,並且防止並發環境下的不一致 。另外,為了防止緩存擾動的頻發,也可以給緩存增加 TTL 來緩解。
站在可行性的角度,不管是 Write-Through 模式還是 Cache-Aside 模式,理想狀況下都可以通過分布式事務保證緩存層數據與持久化層數據的一致性,但在實際項目中,大多都對一致性的要求存在一些寬容度,所以在方案上往往有所折衷。
Write-Through 直寫模式適合寫操作較多,並且對一致性要求較高的場景,在應用 Write-Through 模式時,也需要通過一定的補償機制來解決它的問題。首先,在並發環境下,我們前面提到了先更新資料庫,再更新緩存會導致緩存和資料庫的不一致,那麼先更新緩存,再更新資料庫呢?
這樣的操作時序仍然會導致下面這樣線程 1 先更新緩存,最後更新資料庫的情況,即由於線程 1 和 線程 2 的執行不確定性導致資料庫和緩存的不一致。這種由於線程競爭導致的緩存不一致,可以通過分布式鎖解決,保證對緩存和資料庫的操作僅能由同一個線程完成。對於沒有拿到鎖的線程,一是通過鎖的 timeout 時間進行控制,二是將請求暫存在消息隊列中順序消費。
在下面這種並發執行場景下,來自線程 1 的寫請求更新了資料庫,接著來自線程 2 的讀請求命中緩存,接著線程 1 才更新緩存,這樣便會導致線程 2 讀取到的緩存落後於資料庫。同理,先更新緩存後更新資料庫在寫請求和讀請求並發時,也會出現類似的問題。面對這種場景,我們也可以加鎖解決。
另在,在 Write-Through 模式下,不管是先更新緩存還是先更新資料庫,都存在更新緩存或者更新資料庫失敗的情況,上面提到的重試機制和補償機制在這里也是奏效的。
— 5 —
Write-Behind
Write behind 意為非同步回寫模式,它也具有類似 Read-Through/Write-Through 的訪問控制層,不同的是,Write behind 在處理寫請求時,只更新緩存而不更新資料庫,對於資料庫的更新,則是通過批量非同步更新的方式進行的,批量寫入的時間點可以選在資料庫負載較低的時間進行。
在 Write-Behind 模式下,寫請求延遲較低,減輕了資料庫的壓力,具有較好的吞吐性。但資料庫和緩存的一致性較弱,比如當更新的數據還未被寫入資料庫時,直接從資料庫中查詢數據是落後於緩存的。同時,緩存的負載較大,如果緩存宕機會導致數據丟失,所以需要做好緩存的高可用。顯然,Write behind 模式下適合大量寫操作的場景,常用於電商秒殺場景中庫存的扣減。
— 6 —
Write-Around
如果一些非核心業務,對一致性的要求較弱,可以選擇在 cache aside 讀模式下增加一個緩存過期時間,在寫請求中僅僅更新資料庫,不做任何刪除或更新緩存的操作,這樣,緩存僅能通過過期時間失效。這種方案實現簡單,但緩存中的數據和資料庫數據一致性較差,往往會造成用戶的體驗較差,應慎重選擇。
— 7 —
總結
在解決緩存一致性的過程中,有多種途徑可以保證緩存的最終一致性,應該根據場景來設計合適的方案,讀多寫少的場景下,可以選擇採用「Cache-Aside 結合消費資料庫日誌做補償」的方案,寫多的場景下,可以選擇採用「Write-Through 結合分布式鎖」的方案 ,寫多的極端場景下,可以選擇採用「Write-Behind」的方案。