前面我們談了Redis Sharding多服務器集群技術,Redis Sharding是客戶端Sharding技術,對於服務端來說,各個Redis服務器彼此是相互獨立的,這對於服務端根據需要靈活部署Redis非常輕便,Redis Sharding具有很好的靈活性、可伸縮性,是一種輕量級集群技術。
本篇,介紹另外一種多Redis服務器集群技術,即Redis Cluster。Redis Cluster是一種服務器Sharding技術,3.0版本開始正式提供。
Redis Cluster中,Sharding采用slot(槽)的概念,一共分成16384個槽,這有點兒類似前面講的pre sharding思路。對於每個進入Redis的鍵值對,根據key進行散列,分配到這16384個slot中的某一個中。使用的hash算法也比較簡單,就是CRC16后16384取模。
Redis集群中的每個node(節點)負責分攤這16384個slot中的一部分,也就是說,每個slot都對應一個node負責處理。當動態添加或減少node節點時,需要將16384個槽做個再分配,槽中的鍵值也要遷移。當然,這一過程,在目前實現中,還處於半自動狀態,需要人工介入。
Redis集群,要保證16384個槽對應的node都正常工作,如果某個node發生故障,那它負責的slots也就失效,整個集群將不能工作。
為了增加集群的可訪問性,官方推薦的方案是將node配置成主從結構,即一個master主節點,掛n個slave從節點。這時,如果主節點失效,Redis Cluster會根據選舉算法從slave節點中選擇一個上升為主節點,整個集群繼續對外提供服務。這非常類似前篇文章提到的Redis Sharding場景下服務器節點通過Sentinel監控架構成主從結構,只是Redis Cluster本身提供了故障轉移容錯的能力。
Redis Cluster的新節點識別能力、故障判斷及故障轉移能力是通過集群中的每個node都在和其它nodes進行通信,這被稱為集群總線(cluster bus)。它們使用特殊的端口號,即對外服務端口號加10000。例如如果某個node的端口號是6379,那么它與其它nodes通信的端口號是16379。nodes之間的通信采用特殊的二進制協議。
對客戶端來說,整個cluster被看做是一個整體,客戶端可以連接任意一個node進行操作,就像操作單一Redis實例一樣,當客戶端操作的key沒有分配到該node上時,Redis會返回轉向指令,指向正確的node,這有點兒像瀏覽器頁面的302 redirect跳轉。
Redis Cluster是Redis 3.0以后才正式推出,時間較晚,目前能證明在大規模生產環境下成功的案例還不是很多,需要時間檢驗。
集群搭建R
下面,我們就實際操作建立一個Redis Cluster。我們將建立3個node,每個node架構成一主一從,故總共有6個redis實例,端口號從7000-7005。
Cluster下的Redis實例,和普通Redis實例一樣,只是處於cluster模式方式下運行。
實操步驟如下:
1. 以3.0.5為例,建立cluster目錄,其下再建6個子目錄代表6實例環境
mkdir cluster
mkdir 7000 7001 7002 7003 7004 7005
2. 將redis.conf模板分別copy到上面6子目錄中,並做如下修改,以7000為例:
修改如下信息
daemonize yes
pidfile /var/run/redis-7000.pid
port 7000
logfile "/var/log/redis-7000.log"
注釋掉如下信息, 不需要RDB持久化
#save 900 1
#save 300 10
#save 60 10000
修改如下信息
appendonly yes
appendfilename "appendonly-7000.aof"
取消如下注釋,讓Redis在集群模式下運行
cluster-enabled yes 啟動cluster模式
cluster-config-file nodes-7000.conf 集群信息文件名,由redis自己維護
cluster-node-timeout 15000 15秒中聯系不到對方node,即認為對方有故障可能
3. 在各個目錄下執行 redis-server redis.conf 啟動redis實例
這時,這幾個實例都是各自是一個集群狀態在運行,並沒有形成一個整體集群態,我們需要Redis提供的基於ruby開發的工具進行人工設置。
4.安裝ruby環境
yum install ruby ruby-devel rubygems
5.安裝Redis的ruby依賴接口
gem install redis
6.利用腳本工具建立集群
./redis-trib.rb create --replicas 1 192.168.1.142:7000 192.168.1.142:7001 192.168.1.142:7002 192.168.1.142:7003 192.168.1.142:7004 192.168.1.142:7005
該腳本自動執行節點分配方案,將前3個redis實例作為主節點,后三個作為從節點,如圖:
按提示敲入"yes",執行方案,將6個節點組成集群,3主3從。
執行redis-cli -p 7000 info Replication 命令,觀察7000這個節點,發現其復制配置信息已配置成主節點,並有一個從節點7003
再執行redis-cli -p 7003 info Replication發現,該節點已設置成從節點。這些主從設置,都是在創建集群時自動完成的。
至此,3主3從的Redis集群建立起來了。接下來我們做個故障轉移試驗,將主節點7001 shutdown掉,看看發生什么?
我們看到,從節點7004會上升為主節點繼續提供集群服務。那又重新啟動7001節點呢?
我們發現7001節點已經成為從節點,不會成為取代7004成為主節點。那如果將主節點7001和從節點7004都shutdown掉呢?
這時,整個cluster是拒絕提供服務的。因為原來7001分配的slot現在無節點接管,需要人工介入重新分配slots。
增刪集群節點R
下面我們操作下,如果修改集群節點架構:
刪除一個從節點。注意,如果刪除主節點,其負責的slots必須為空。
./redis-trib.rb del-node 192.168.1.142:7000 ee2fc0ea6e630f54e3b811caedf8896b26a99cba
將7001節點的slot都轉移到7000
./redis-trib.rb reshard 192.168.1.142:7000
按提示操作即可。
加一個節點。注意新添加的節點還沒有分配slot,用reshard給它分配一定比例的slots
./redis-trib.rb add-node 192.168.1.142:7001 192.168.1.142:7000
給指定一個主節點添加一個從節點。
./redis-trib.rb add-node --slave --master-id f4d17d56a9dda1a102da7cd799192beff7cba69e 192.168.1.142:7004 192.168.1.142:7000
注意如果此redis實例參與過集群,需先cluster reset 清除重置一下。
Jedis客戶端訪問R
上面介紹了服務端Redis Cluster搭建過程。下面來看看客戶端如何使用?
Java語言的客戶端驅動Jedis是支持Redis Cluster的。我們具體實操下:
1. pom.xml中配置jedis jar包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.0</version> <!-- 最近升級了 -->
</dependency>
2. spring配置文件中配置JedisCluster
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="4096"/>
<property name="maxIdle" value="200"/>
<property name="maxWaitMillis" value="3000"/>
<property name="testOnBorrow" value="true" />
<property name="testOnReturn" value="true" />
</bean>
<bean id = "jedisCluster" class = "redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.1.142" />
<constructor-arg index="1" value="7002" />
</bean>
</set>
</constructor-arg>
<constructor-arg index="1" value="2000" type="int"/>
<constructor-arg index="2" value="2" type="int"/>
<constructor-arg index="3" ref="poolConfig"/>
</bean>
3.編寫測試代碼
@Test
public void basicOpTestForCluster(){
long begin = System.currentTimeMillis();
for(int i=0;i<10000; i++){
jedis.set("person." + i + ".name", "frank");
jedis.set("person." + i + ".city", "beijing");
String name = jedis.get("person." + i + ".name");
String city = jedis.get("person." + i + ".city");
assertEquals("frank",name);
assertEquals("beijing",city);
jedis.del("person." + i + ".name");
Boolean result = jedis.exists("person." + i + ".name");
assertEquals(false,result);
result = jedis.exists("person." + i + ".city");
assertEquals(true,result);
}
long end = System.currentTimeMillis();
System.out.println("total time: " + (end-begin)/1000);
}