Redis 架構演變與 Redis-cluster 群集讀寫方案

發表於2018-08-14

導言

Redis-cluster 是近年來 Redis 架構不斷改進中的相對較好的 Redis 高可用方案。本文涉及到近年來 Redis 多例項架構的演變過程,包括普通主從架構(Master、slave 可進行寫讀分離)、哨兵模式下的主從架構、Redis-cluster 高可用架構(Redis 官方預設 cluster 下不進行讀寫分離)的簡介。同時還介紹使用Java的兩大redis客戶端:Jedis與Lettuce用於讀寫redis-cluster的資料的一般方法。再通過官方文件以及網際網路的相關技術文件,給出redis-cluster架構下的讀寫能力的優化方案,包括官方的推薦的擴充套件redis-cluster下的Master數量以及非官方預設的redis-cluster的讀寫分離方案,案例中使用Lettuce的特定方法進行redis-cluster架構下的資料讀寫分離。

 

近年來redis多例項用架構的演變過程

redis是基於記憶體的高效能key-value資料庫,若要讓redis的資料更穩定安全,需要引入多例項以及相關的高可用架構。而近年來redis的高可用架構亦不斷改進,先後出現了本地持久化、主從備份、哨兵模式、redis-cluster群集高可用架構等等方案。

 

1、redis普通主從模式

通過持久化功能,Redis保證了即使在伺服器重啟的情況下也不會損失(或少量損失)資料,因為持久化會把記憶體中資料儲存到硬碟上,重啟會從硬碟上載入資料。 。但是由於資料是儲存在一臺伺服器上的,如果這臺伺服器出現硬碟故障等問題,也會導致資料丟失。為了避免單點故障,通常的做法是將資料庫複製多個副本以部署在不同的伺服器上,這樣即使有一臺伺服器出現故障,其他伺服器依然可以繼續提供服務。為此, Redis 提供了複製(replication)功能,可以實現當一臺資料庫中的資料更新後,自動將更新的資料同步到其他資料庫上。

在複製的概念中,資料庫分為兩類,一類是主資料庫(master),另一類是從資料庫(slave)。主資料庫可以進行讀寫操作,當寫操作導致資料變化時會自動將資料同步給從資料庫。而從資料庫一般是隻讀的,並接受主資料庫同步過來的資料。一個主資料庫可以擁有多個從資料庫,而一個從資料庫只能擁有一個主資料庫。

主從模式的配置,一般只需要再作為slave的redis節點的conf檔案上加入“slaveof masterip masterport”, 或者作為slave的redis節點啟動時使用如下參考命令:

redis的普通主從模式,能較好地避免單獨故障問題,以及提出了讀寫分離,降低了Master節點的壓力。網際網路上大多數的對redis讀寫分離的教程,都是基於這一模式或架構下進行的。但實際上這一架構並非是目前最好的redis高可用架構。

 

2、redis哨兵模式高可用架構

當主資料庫遇到異常中斷服務後,開發者可以通過手動的方式選擇一個從資料庫來升格為主資料庫,以使得系統能夠繼續提供服務。然而整個過程相對麻煩且需要人工介入,難以實現自動化。 為此,Redis 2.8開始提供了哨兵工具來實現自動化的系統監控和故障恢復功能。 哨兵的作用就是監控redis主、從資料庫是否正常執行,主出現故障自動將從資料庫轉換為主資料庫。

顧名思義,哨兵的作用就是監控Redis系統的執行狀況。它的功能包括以下兩個。

(1)監控主資料庫和從資料庫是否正常執行。
(2)主資料庫出現故障時自動將從資料庫轉換為主資料庫。

可以用info replication檢視主從情況 例子: 1主2從 1哨兵,可以用命令起也可以用配置檔案裡 可以使用雙哨兵,更安全,參考命令如下:

其中,哨兵配置檔案sentinel.conf參考如下:

其中mymaster表示要監控的主資料庫的名字。配置哨兵監控一個系統時,只需要配置其監控主資料庫即可,哨兵會自動發現所有複製該主資料庫的從資料庫。
Master與slave的切換過程:
(1)slave leader升級為master
(2)其他slave修改為新master的slave
(3)客戶端修改連線
(4)老的master如果重啟成功,變為新master的slave

 

3、redis-cluster群集高可用架構

即使使用哨兵,redis每個例項也是全量儲存,每個redis儲存的內容都是完整的資料,浪費記憶體且有木桶效應。為了最大化利用記憶體,可以採用cluster群集,就是分散式儲存。即每臺redis儲存不同的內容。
採用redis-cluster架構正是滿足這種分散式儲存要求的叢集的一種體現。redis-cluster架構中,被設計成共有16384個hash slot。每個master分得一部分slot,其演算法為:hash_slot = crc16(key) mod 16384 ,這就找到對應slot。採用hash slot的演算法,實際上是解決了redis-cluster架構下,有多個master節點的時候,資料如何分佈到這些節點上去。key是可用key,如果有{}則取{}內的作為可用key,否則整個可以是可用key。群集至少需要3主3從,且每個例項使用不同的配置檔案。

在redis-cluster架構中,redis-master節點一般用於接收讀寫,而redis-slave節點則一般只用於備份,其與對應的master擁有相同的slot集合,若某個redis-master意外失效,則再將其對應的slave進行升級為臨時redis-master。
在redis的官方文件中,對redis-cluster架構上,有這樣的說明:在cluster架構下,預設的,一般redis-master用於接收讀寫,而redis-slave則用於備份,當有請求是在向slave發起時,會直接重定向到對應key所在的master來處理。但如果不介意讀取的是redis-cluster中有可能過期的資料並且對寫請求不感興趣時,則亦可通過readonly命令,將slave設定成可讀,然後通過slave獲取相關的key,達到讀寫分離。具體可以參閱redis官方文件(https://redis.io/commands/readonly)等相關內容:

例如,我們假設已經建立了一個三主三從的redis-cluster架構,其中A、B、C節點都是redis-master節點,A1、B1、C1節點都是對應的redis-slave節點。在我們只有master節點A,B,C的情況下,對應redis-cluster如果節點B失敗,則群集無法繼續,因為我們沒有辦法再在節點B的所具有的約三分之一的hash slot集合範圍內提供相對應的slot。然而,如果我們為每個主伺服器節點新增一個從伺服器節點,以便最終叢集由作為主伺服器節點的A,B,C以及作為從伺服器節點的A1,B1,C1組成,那麼如果節點B發生故障,系統能夠繼續執行。節點B1複製B,並且B失效時,則redis-cluster將促使B的從節點B1作為新的主伺服器節點並且將繼續正確地操作。但請注意,如果節點B和B1在同一時間發生故障,則Redis群集無法繼續執行。

Redis群集配置引數:在繼續之前,讓我們介紹一下Redis Cluster在redis.conf檔案中引入的配置引數。有些命令的意思是顯而易見的,有些命令在你閱讀下面的解釋後才會更加清晰。

(1)cluster-enabled :如果想在特定的Redis例項中啟用Redis群集支援就設定為yes。 否則,例項通常作為獨立例項啟動。
(2)cluster-config-file :請注意,儘管有此選項的名稱,但這不是使用者可編輯的配置檔案,而是Redis群集節點每次發生更改時自動保留群集配置(基本上為狀態)的檔案。
(3)cluster-node-timeout :Redis群集節點可以不可用的最長時間,而不會將其視為失敗。 如果主節點超過指定的時間不可達,它將由其從屬裝置進行故障切換。
(4)cluster-slave-validity-factor :如果設定為0,無論主裝置和從裝置之間的鏈路保持斷開連線的時間長短,從裝置都將嘗試故障切換主裝置。 如果該值為正值,則計算最大斷開時間作為節點超時值乘以此選項提供的係數,如果該節點是從節點,則在主鏈路斷開連線的時間超過指定的超時值時,它不會嘗試啟動故障切換。
(5)cluster-migration-barrier :主裝置將保持連線的最小從裝置數量,以便另一個從裝置遷移到不受任何從裝置覆蓋的主裝置。有關更多資訊,請參閱本教程中有關副本遷移的相應部分。
(6)cluster-require-full-coverage :如果將其設定為yes,則預設情況下,如果key的空間的某個百分比未被任何節點覆蓋,則叢集停止接受寫入。 如果該選項設定為no,則即使只處理關於keys子集的請求,群集仍將提供查詢。

以下是最小的Redis叢集配置檔案:

注意:
(1)redis-cluster最小配置為三主三從,當1個主故障,大家會給對應的從投票,把從立為主,若沒有從資料庫可以恢復則redis群集就down了。
(2)在這個redis cluster中,如果你要在slave讀取資料,那麼需要帶上readonly指令。redis cluster的核心的理念,主要是用slave做高可用的,每個master掛一兩個slave,主要是做資料的熱備,當master故障時的作為主備切換,實現高可用的。redis cluster預設是不支援slave節點讀或者寫的,跟我們手動基於replication搭建的主從架構不一樣的。slave node要設定readonly,然後再get,這個時候才能在slave node進行讀取。對於redis -cluster主從架構,若要進行讀寫分離,官方其實是不建議的,但也能做,只是會複雜一些。具體見下面的章節。
(3)redis-cluster的架構下,實際上本身master就是可以任意擴充套件的,你如果要支撐更大的讀吞吐量,或者寫吞吐量,或者資料量,都可以直接對master進行橫向擴充套件就可以了。也擴容master,跟之前擴容slave進行讀寫分離,效果是一樣的或者說更好。
(4)可以使用自帶客戶端連線:使用redis-cli -c -p cluster中任意一個埠,進行資料獲取測試。

 

Java中對redis-cluster資料的一般讀取方法簡介

 

使用Jedis讀寫redis-cluster的資料

由於Jedis類一般只能對一臺redis-master進行資料操作,所以面對redis-cluster多臺master與slave的群集,Jedis類就不能滿足了。這個時候我們需要引用另外一個操作類:JedisCluster類。
例如我們有6臺機器組成的redis-cluster:
172.20.52.85:7000、 172.20.52.85:7001、172.20.52.85:7002、172.20.52.85:7003、172.20.52.85:7004、172.20.52.85:7005
其中master機器對應埠:7000、7004、7005
slave對應埠:7001、7002、7003

使用JedisCluster對redis-cluster進行資料操作的參考程式碼如下:

執行結果截圖如下圖所示:

第一節中我們已經介紹了redis-cluster架構下master提供讀寫功能,而slave一般只作為對應master機器的資料備份不提供讀寫。如果我們只在hostAndPortsSet中只配置slave,而不配置master,實際上還是可以讀到資料,但其內部操作實際是通過slave重定向到相關的master主機上,然後再將結果獲取和輸出。

上面是普通專案使用JedisCluster的簡單過程,若在spring boot專案中,可以定義JedisConfig類,使用@Configuration、@Value、@Bean等一些列註解完成JedisCluster的配置,然後再注入該JedisCluster到相關service邏輯中引用,這裡介紹略。

 

使用Lettuce讀寫redis-cluster資料

Lettuce 和 Jedis 的定位都是Redis的client。Jedis在實現上是直接連線的redis server,如果在多執行緒環境下是非執行緒安全的,這個時候只有使用連線池,為每個Jedis例項增加物理連線,每個執行緒都去拿自己的 Jedis 例項,當連線數量增多時,物理連線成本就較高了。
Lettuce的連線是基於Netty的,連線例項(StatefulRedisConnection)可以在多個執行緒間併發訪問,應為StatefulRedisConnection是執行緒安全的,所以一個連線例項(StatefulRedisConnection)就可以滿足多執行緒環境下的併發訪問,當然這個也是可伸縮的設計,一個連線例項不夠的情況也可以按需增加連線例項。
其中spring boot 2.X版本中,依賴的spring-session-data-redis已經預設替換成Lettuce了。
同樣,例如我們有6臺機器組成的redis-cluster:
172.20.52.85:7000、 172.20.52.85:7001、172.20.52.85:7002、172.20.52.85:7003、172.20.52.85:7004、172.20.52.85:7005
其中master機器對應埠:7000、7004、7005
slave對應埠:7001、7002、7003
在spring boot 2.X版本中使用Lettuce操作redis-cluster資料的方法參考如下:
(1)pom檔案參考如下:
parent中指出spring boot的版本,要求2.X以上:

依賴中需要加入spring-boot-starter-data-redis,參考如下:

(2)springboot的配置檔案要包含如下內容:

(3)新建RedisConfiguration類,參考程式碼如下:

(4)新建RedisFactoryConfig類,參考程式碼如下:

(5)在業務類service中注入Lettuce相關的RedisTemplate,進行相關操作。以下是我化簡到了springbootstarter中進行,參考程式碼如下:

執行結果的截圖如下:

以上的介紹,是採用Jedis以及Lettuce對redis-cluster資料的簡單讀取。Jedis也好,Lettuce也好,其對於redis-cluster架構下的資料的讀取,都是預設是按照redis官方對redis-cluster的設計,自動進行重定向到master節點中進行的,哪怕是我們在配置中列出了所有的master節點和slave節點。查閱了Jedis以及Lettuce的github上的原始碼,預設不支援redis-cluster下的讀寫分離,可以看出Jedis若要支援redis-cluster架構下的讀寫分離,需要自己改寫和構建多一些包裝類,定義好Master和slave節點的邏輯;而Lettuce的原始碼中,實際上預留了方法(setReadForm(ReadFrom.SLAVE))進行redis-cluster架構下的讀寫分離,相對來說修改會簡單一些,具體可以參考後面的章節。

 

redis-cluster架構下的讀寫能力的優化方案

在上面的一些章節中,已經有講到redis近年來的高可用架構的演變,以及在redis-cluster架構下,官方對redis-master、redis-slave的其實有使用上的建議,即redis-master節點一般用於接收讀寫,而redis-slave節點則一般只用於備份,其與對應的master擁有相同的slot集合,若某個redis-master意外失效,則再將其對應的slave進行升級為臨時redis-master。但如果不介意讀取的是redis-cluster中有可能過期的資料並且對寫請求不感興趣時,則亦可通過readonly命令,將slave設定成可讀,然後通過slave獲取相關的key,達到讀寫分離。
具體可以參閱redis官方文件(https://redis.io/commands/readonly),以下是reids線上文件中,對slave的readonly說明內容:

實際上本身master就是可以任意擴充套件的,所以如果要支撐更大的讀吞吐量,或者寫吞吐量,或者資料量,都可以直接對master進行橫向水平擴充套件就可以了。也就是說,擴容master,跟之前擴容slave並進行讀寫分離,效果是一樣的或者說更好。
所以下面我們將按照redis-cluster架構下分別進行水平擴充套件Master,以及在redis-cluster架構下對master、slave進行讀寫分離兩套方案進行講解。

 

(一)水平擴充套件Master例項來進行redis-cluster效能的提升

redis官方線上文件以及一些網際網路的參考資料都表明,在redis-cluster架構下,實際上不建議做物理的讀寫分離。那麼如果我們真的不做讀寫分離的話,能否通過簡單的方法進行redis-cluster下的效能的提升?我們可以通過master的水平擴充套件,來橫向擴充套件讀寫吞吐量,並且能支撐更多的海量資料。
對master進行水平擴充套件有兩種方法,一種是單機上面進行master例項的增加(建議每新增一個master,也新增一個對應的slave),另一種是新增機器部署新的master例項(同樣建議每新增一個master,也新增一個對應的slave)。當然,我們也可以進行這兩種方法的有效結合。

(1)單機上通過多執行緒建立新redis-master例項,即邏輯上的水平擴充套件:
一般的,對於redis單機,單執行緒的讀吞吐是4w/s~5W/s,寫吞吐為2w/s。
單機合理開啟redis多執行緒情況下(一般執行緒數為CPU核數的倍數),總吞吐量會有所上升,但每個執行緒的平均處理能力會有所下降。例如一個2核CPU,開啟2執行緒的時候,總讀吞吐能上升是6W/s~7W/s,即每個執行緒平均約3W/s再多一些。但過多的redis執行緒反而會限制了總吞吐量。

(2)擴充套件更多的機器,部署新redis-master例項,即物理上的水平擴充套件:
例如,我們可以再原來只有3臺master的基礎上,連入新機器繼續新例項的部署,最終水平擴充套件為6臺master(建議每新增一個master,也新增一個對應的slave)。例如之前每臺master的處理能力假設是讀吞吐5W/s,寫吞吐2W/s,擴充套件前一共的處理能力是:15W/s讀,6W/s寫。如果我們水平擴充套件到6臺master,讀吞吐可以達到總量30W/s,寫可以達到12w/s,效能能夠成倍增加。

(3)若原本每臺部署redis-master例項的機器都效能良好,則可以通過上述兩者的結合,進行一個更優的組合。

使用該方案進行redis-cluster效能的提升的優點有:
(1)符合redis官方要求和資料的準確性。
(2)真正達到更大吞吐量的效能擴充套件。
(3)無需程式碼的大量更改,只需在配置檔案中重新配置新的節點資訊。

當然缺點也是有的:
(1)需要新增機器,提升效能,即成本會增加。
(2)若不新增機器,則需要原來的例項所執行的機器效能較好,能進行以多執行緒的方式部署新例項。但隨著執行緒的增多,而機器的能力不足以支撐的時候,實際上總體能力會提升不太明顯。
(3)redis-cluster進行新的水平擴容後,需要對master進行新的hash slot重新分配,這相當於需要重新載入所有的key,並按演算法平均分配到各個Master的slot當中。

 

(二)引入Lettuce以及修改相關方法,達到對redis-cluster的讀寫分離

通過上面的一些章節,我們已經可以瞭解到Lettuce客戶端讀取redis的一些操作,使用Lettuce能體現出了簡單,安全,高效。實際上,查閱了Lettuce對redis的讀寫,許多地方都進行了redis的讀寫分離。但這些都是基於上述redis架構中最普通的主從分離架構下的讀寫分離,而對於redis-cluster架構下,Lettuce可能是遵循了redis官方的意見,在該架構下,Lettuce在原始碼中直接設定了只由master上進行讀寫(具體參見gitHub的Lettuce專案):

那麼如果真的需要讓Lettuce改為能夠讀取redis-cluster的slave,進行讀寫分離,是否可行?實際上還是可以的。這就需要我們自己在專案中進行二次加工,即不使用spring-boot中的預設Lettuce初始化方法,而是自己去寫一個屬於自己的Lettuce的新RedisClusterClient的連線,並且對該RedisClusterClient的連線進行一個比較重要的設定,那就是由connection.setReadFrom(ReadFrom.MASTER)改為connection.setReadFrom(ReadFrom.SLAVE)。

下面我們開始對之前章節中的Lettuce讀取redis-cluster資料的例子,進行改寫,讓Lettuce能夠支援該架構下的讀寫分離:

spring boot 2.X版本中,依賴的spring-session-data-redis已經預設替換成Lettuce了。
同樣,例如我們有6臺機器組成的redis-cluster:
172.20.52.85:7000、 172.20.52.85:7001、172.20.52.85:7002、172.20.52.85:7003、172.20.52.85:7004、172.20.52.85:7005
其中master機器對應埠:7000、7004、7005
slave對應埠:7001、7002、7003
在spring boot 2.X版本中使用Lettuce操作redis-cluster資料的方法參考如下:
(1)pom檔案參考如下:
parent中指出spring boot的版本,要求2.X以上:

依賴中需要加入spring-boot-starter-data-redis,參考如下:

(2)springboot的配置檔案要包含如下內容:

(3)我們回到RedisConfiguration類中,刪除或遮蔽之前的RedisTemplate方法,新增自定義的redisClusterConnection方法,並且設定好讀寫分離,參考程式碼如下:

其中,這三行程式碼是能進行redis-cluster架構下讀寫分離的核心:

在業務類service中注入Lettuce相關的redisClusterConnection,進行相關讀寫操作。以下是我直接化簡到了springbootstarter中進行,參考程式碼如下:

執行的結果如下圖所示:

可以看到,經過改寫的redisClusterConnection的確能讀取到redis-cluster的資料。但這一個資料我們還需要驗證一下到底是不是通過slave讀取到的,又或者還是通過slave重定向給master才獲取到的?
帶著疑問,我們可以開通debug模式,在redisClusterConnection.sync().get(“event:10”)等類似的獲取資料的程式碼行上面打上斷點。通過程式碼的走查,我們可以看到,在ReadFromImpl類中,最終會select到key所在的slave節點,進行返回,並在該slave中進行資料的讀取:

ReadFromImpl顯示:

另外我們通過connectFuture中的顯示也驗證了對於slave的readonly生效了:

這樣,就達到了通過Lettuce客戶端對redis-cluster的讀寫分離了。

使用該方案進行redis-cluster效能的提升的優點有:
(1)直接通過程式碼級更改,而不需要配置新的redis-cluster環境。
(2)無需增加機器或升級硬體裝置。

但同時,該方案也有缺點:
(1)非官方對redis-cluster的推薦方案,因為在redis-cluster架構下,進行讀寫分離,有可能會讀到過期的資料。
(2)需對專案進行全面的替換,將Jedis客戶端變為Lettuce客戶端,對程式碼的改動較大,而且使用Lettuce時,使用的並非spring boot的自帶整合Lettuce的redisTemplate配置方法,而是自己配置讀寫分離的 redisClusterConnetcion,日後遇到問題的時候,可能官方文件的支援率或支撐能力會比較低。
(3)需修改redis-cluster的master、slave配置,在各個節點中都需要加入slave-read-only yes。
(4)效能的提升沒有水平擴充套件master主機和例項來得直接乾脆。

 

總結

總體上來說,redis-cluster高可用架構方案是目前最好的redis架構方案,redis的官方對redis-cluster架構是建議redis-master用於接收讀寫,而redis-slave則用於備份(備用),預設不建議讀寫分離。但如果不介意讀取的是redis-cluster中有可能過期的資料並且對寫請求不感興趣時,則亦可通過readonly命令,將slave設定成可讀,然後通過slave獲取相關的key,達到讀寫分離。Jedis、Lettuce都可以進行redis-cluster的讀寫操作,而且預設只針對Master進行讀寫,若要對redis-cluster架構下進行讀寫分離,則Jedis需要進行原始碼的較大改動,而Lettuce開放了setReadFrom()方法,可以進行二次封裝成讀寫分離的客戶端,相對簡單,而且Lettuce比Jedis更安全。redis-cluster架構下可以直接通過水平擴充套件master來達到效能的提升。

 

參考文件

1,網文《關於redis主從、哨兵、叢集的介紹》:https://blog.csdn.net/c295477887/article/details/52487621
2,知乎《lettuce與jedis對比介紹》:https://www.zhihu.com/question/53124685
3,網文《Redis 高可用架構最佳實踐問答集錦》:http://www.talkwithtrend.com/Article/178165
4,網文《Redis進階實踐之十一 Redis的Cluster叢集搭建》:https://www.cnblogs.com/PatrickLiu/p/8458788.html
5,redis官方線上文件:https://redis.io/
6,網文《redis cluster的介紹及搭建(6)》:https://blog.csdn.net/qq1137623160/article/details/79184686
7,網文《Springboot2.X整合redis叢集(Lettuce)連線》:http://www.cnblogs.com/xymBlog/p/9303032.html
8,Jedis的gitHub地址:https://github.com/xetorthio/jedis
9,Lettuce的gitHub地址:https://github.com/lettuce-io/lettuce-core/

相關文章