問題
雖然主從復制和哨兵模式完美的解決了Redis的單機問題,但是Redis仍然存在著以下兩個問題:
- 所有的寫操作都集中到主服務器上弄屡,主服務器CPU壓力比較大
- 不管是主服務器還是從服務器棺禾,它們都同樣保存了redis的所有數(shù)據(jù),隨著數(shù)據(jù)越來越多,可能會出現(xiàn)內存不夠用的問題
解題思路
在redis集群中践险,key只能保存在按照某種規(guī)律計算得到的節(jié)點上班套,對該key的讀取和更新也只能在該節(jié)點進行肢藐。比如redis集群一共有6個節(jié)點,現(xiàn)在我想執(zhí)行 set name hello
吱韭,這個key為name吆豹,常見的某種規(guī)律有哈希取余"name".hashcode() % 6 + 1
得到節(jié)點的位置為4鱼的,所以就放在第四個的位置上,以后不管我是讀取還是更新還是刪除痘煤,我都到第四個節(jié)點上凑阶。如此一來,便完美解決了上述兩個問題衷快。
Redis 分區(qū)方案(在哪里按照某種規(guī)律計算)
1. 客戶端分區(qū)方案
指在客戶端計算key得到將要保存的節(jié)點宙橱,然后客戶端再連接該節(jié)點端口,進行數(shù)據(jù)操作蘸拔。這種方案比較簡單师郑,但是一旦節(jié)點數(shù)發(fā)生變化,將要更新新的計算算法(比如取余這個6改成10)到所有客戶端上调窍,會比較麻煩宝冕。
2. 代理分區(qū)方案
指在客戶端和服務器之間加了一層代理層,客戶端的命令先到代理層邓萨,代理層進行計算地梨,再分配到它對應的節(jié)點上;這種方法挺好的缔恳,節(jié)點數(shù)發(fā)生變化宝剖,只需要修改代理層的計算算法即可,但是需要多一層轉發(fā)褐耳,需要一定的耗時诈闺。
3. 查詢路由方案
節(jié)點之間早就約定好哪些key是屬于自己,哪些key是屬于其它節(jié)點铃芦;客戶端最開始隨機把命令發(fā)給某個節(jié)點雅镊,節(jié)點計算并查看這個key是否屬于自己的,如果是自己的就進行處理刃滓,并把結果發(fā)回去仁烹;如果是其它節(jié)點的,就會把那個節(jié)點的信息(ip + 地址)轉發(fā)給客戶端咧虎,讓客戶端重定向卓缰,這么一說感覺是有點像http協(xié)議中的3XX狀態(tài)碼。今天的主角Redis Cluster
就是基于查詢路由方案砰诵。
數(shù)據(jù)分區(qū)(某種規(guī)律有哪些)
數(shù)據(jù)分區(qū)一遍有兩種征唬,哈希分區(qū)和順序分區(qū);哈希分區(qū)顧名思義茁彭,就是對key進行哈希計算然后分區(qū)总寒;而順序分區(qū)則是對按順序對key進行分區(qū)。因為Redis cluster采用的是哈希分區(qū)理肺,所以這里只討論哈希分區(qū)摄闸。哈希分區(qū)也有很多規(guī)則善镰,如下:
1. 節(jié)點取余分區(qū)
對key進行hash計算,然后用節(jié)點的個數(shù)去取余得到應該在哪個節(jié)點hash(key) % N
年枕。這種分區(qū)方法比較方便炫欺。就是當節(jié)點數(shù)變化的時候,幾乎所有的key都需要重新分配熏兄。
2. 一致性哈希分區(qū)
3. 虛擬槽分區(qū)
在Redis Cluster
中品洛,約定了有16383個槽,我們對key進行CRC16(key) & 16383
計算后會得到這個key屬于哪個槽霍弹,這16383個槽在集群創(chuàng)建之初毫别,會自動或者手動的分配到不同的節(jié)點中娃弓,即key -> slot -> node
典格。添加或者刪除新的節(jié)點的時候,只需要對對應的槽進行重新分配即可台丛。
redis cluster 的大概流程
集群創(chuàng)建之初耍缴,我們可以自動或者手動給每個節(jié)點分配槽位。每個節(jié)點通過Gossip協(xié)議挽霉,會和其它節(jié)點交換槽信息防嗡,得到并且保存槽與節(jié)點的全局對應關系圖。于是節(jié)點收到客戶端發(fā)來的命令以后侠坎,對key進行CRC16(key) & 16383
的計算得到槽位蚁趁,對比這個槽位是不是屬于自己的,如果是自己的就進行處理实胸,并把結果發(fā)回去他嫡;如果是其它節(jié)點的,就會把那個節(jié)點的信息(ip + 地址)轉發(fā)給客戶端庐完,然后客戶端再重新請求钢属;當然客戶端也不會那么傻,每次都是隨機請求節(jié)點门躯,客戶端在啟動的時候也會和服務器交換信息得到槽和節(jié)點的映射圖淆党,客戶端請求的節(jié)點,也是客戶端自己計算CRC16(key) & 16383
得到槽位讶凉,再對比關系圖而得到的節(jié)點染乌,如果節(jié)點發(fā)生變化了(即收到請求重定向),它也會更新這個關系圖懂讯。
創(chuàng)建集群
- 準備節(jié)點,一個高可用的redis集群至少要有6個節(jié)點
# redis-6379.conf
port 6379
daemonize yes
protected-mode no
logfile "6379.log"
dbfilename "dump-6379.rdb"
cluster-enabled yes # 開啟集群
cluster-node-timeout 15000 #節(jié)點超時時間荷憋,15s
cluster-config-file "nodes-6379.conf" #集群內部配置文件
# redis-6380.conf
port 6380
daemonize yes
protected-mode no
logfile "6380.log"
dbfilename "dump-6380.rdb"
cluster-enabled yes # 開啟集群
cluster-node-timeout 15000 #節(jié)點超時時間,15s
cluster-config-file "nodes-6380.conf" #集群內部配置文件
# redis-6381.conf
port 6381
daemonize yes
protected-mode no
logfile "6381.log"
dbfilename "dump-6381.rdb"
cluster-enabled yes # 開啟集群
cluster-node-timeout 15000 #節(jié)點超時時間域醇,15s
cluster-config-file "nodes-6381.conf" #集群內部配置文件
# redis-6382.conf
port 6382
daemonize yes
protected-mode no
logfile "6382.log"
dbfilename "dump-6382.rdb"
cluster-enabled yes # 開啟集群
cluster-node-timeout 15000 #節(jié)點超時時間台谊,15s
cluster-config-file "nodes-6382.conf" #集群內部配置文件
# redis-6383.conf
port 6383
daemonize yes
protected-mode no
logfile "6383.log"
dbfilename "dump-6383.rdb"
cluster-enabled yes # 開啟集群
cluster-node-timeout 15000 #節(jié)點超時時間蓉媳,15s
cluster-config-file "nodes-6383.conf" #集群內部配置文件
# redis-6384.conf
port 6384
daemonize yes
protected-mode no
logfile "6384.log"
dbfilename "dump-6384.rdb"
cluster-enabled yes # 開啟集群
cluster-node-timeout 15000 #節(jié)點超時時間,15s
cluster-config-file "nodes-6384.conf" #集群內部配置文件
6個節(jié)點啟動成功后锅铅,我們可以在redis目錄下看到生成的cluster-config-file文件酪呻,打開nodes-6379.conf
如下:
8ba45af25feef061507831ca1b3ddf71a7574631 :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
其中8ba45af25feef061507831ca1b3ddf71a7574631是6379redis的節(jié)點ID,這里我們只要知道它很重要就可以了。
- 節(jié)點握手盐须,打開客戶端進入6379玩荠,然后依次運行
cluster meet 139.199.168.61 6380
到cluster meet 139.199.168.61 6384
139.199.168.61:6379> cluster meet 139.199.168.61 6380
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6381
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6382
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6383
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6384
OK
cluster meet
兩個節(jié)點互相感知對方存在,發(fā)起節(jié)點發(fā)送發(fā)送Gossip協(xié)議中的meet消息給接收節(jié)點贼邓,接收節(jié)點收到meet消息后阶冈,保存發(fā)起節(jié)點的信息,然后通過返回pong消息把自己的信息也返回回去塑径,之后兩個節(jié)點會定期ping/pong進行節(jié)點通信女坑。我們可以把它理解為把某個節(jié)點拉到一個集群里面,如果把其它節(jié)點也拉進來以后统舀,集群里面的節(jié)點兩兩之間都會互相握手匆骗。等所有節(jié)點都拉到集群以后,我們可以執(zhí)行cluster nodes
來查看集群中節(jié)點間的關系誉简。
139.199.168.61:6379> cluster nodes
8ba45af25feef061507831ca1b3ddf71a7574631 10.104.90.159:6379 myself,master - 0 0 1 connected
0573105a355722bc6dd5ab29dea072ce1a6956df 139.199.168.61:6381 master - 0 1540711564922 2 connected
a08f700001a5902dd82b51eb74b4ec8028202d75 139.199.168.61:6382 master - 0 1540711562919 3 connected
dc7a392e05e8b9840164bb21ef662168e28d71b4 139.199.168.61:6380 master - 0 1540711563919 0 connected
ba7816cc7ed0f5c0360708048a68c29b6bf66193 139.199.168.61:6383 master - 0 1540711565924 4 connected
d0585a4fda8ab743bd5b3448f26f46f1e09c19c9 139.199.168.61:6384 master - 0 1540711561916 5 connected
- 分配槽
以上只是建立了一個集群碉就,但是其實集群還不能工作,可以用cluster info
來查看集群狀態(tài):
139.199.168.61:6379> cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_sent:288
cluster_stats_messages_received:288
可以看到此時集群的狀態(tài)是fail闷串,失敗的瓮钥,我們需要把這16383個槽分出去,集群才能正常工作烹吵,分配槽的命令如下:
/usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6379 cluster addslots {0..5461}
/usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6380 cluster addslots {5462..10922}
/usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6381 cluster addslots {10923..16383}
PS:注意0和5641之間隔的是兩個點碉熄,因為看書上寫的是三個點,會報(error) ERR Invalid or out of range slot
的錯誤年叮。
這樣子就把所有的槽都分出去了具被,但是只用到了三個節(jié)點,剩下三個節(jié)點我們可以作為從節(jié)點只损,可以使用cluster replicate 主節(jié)點id
來把某個節(jié)點掛為某個節(jié)點的從節(jié)點一姿。
139.199.168.61:6382> cluster replicate 8ba45af25feef061507831ca1b3ddf71a7574631
139.199.168.61:6383> cluster replicate a08f700001a5902dd82b51eb74b4ec8028202d75
139.199.168.61:6384> cluster replicate 0573105a355722bc6dd5ab29dea072ce1a6956df
最后我們來看一下節(jié)點狀態(tài):
139.199.168.61:6379> cluster info
cluster_state:ok #狀態(tài)OK
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_sent:7325
cluster_stats_messages_received:7325
再來查看一下節(jié)點關系:
139.199.168.61:6379> cluster nodes
8ba45af25feef061507831ca1b3ddf71a7574631 10.104.90.159:6379 myself,master - 0 0 1 connected 0-5461
0573105a355722bc6dd5ab29dea072ce1a6956df 139.199.168.61:6381 master - 0 1540714889809 2 connected 10923-16383
a08f700001a5902dd82b51eb74b4ec8028202d75 139.199.168.61:6382 slave 8ba45af25feef061507831ca1b3ddf71a7574631 0 1540714890811 3 connected
dc7a392e05e8b9840164bb21ef662168e28d71b4 139.199.168.61:6380 master - 0 1540714885803 0 connected 5462-10922
ba7816cc7ed0f5c0360708048a68c29b6bf66193 139.199.168.61:6383 slave dc7a392e05e8b9840164bb21ef662168e28d71b4 0 1540714891813 4 connected
d0585a4fda8ab743bd5b3448f26f46f1e09c19c9 139.199.168.61:6384 slave 0573105a355722bc6dd5ab29dea072ce1a6956df 0 1540714888807 5 connected
節(jié)點id,節(jié)點ip/端口跃惫,是否是主節(jié)點叮叹,節(jié)點的槽位分配一覽無余。至此爆存,一個完整的redis cluster集群創(chuàng)建成功蛉顽。
節(jié)點通信(Gossip協(xié)議)
Gossip協(xié)議
常用的Gossip消息可分為:ping消息、pong消息先较、meet消息携冤、fail消息悼粮。
-
meet消息
用于通知新節(jié)點加入。消息發(fā)送者通知接收者加入到當前集群曾棕,meet消息通信正常完成后扣猫,接收節(jié)點會加入到集群中并進行周期性的ping、pong消息交換翘地。 -
ping消息
集群內每個節(jié)點每秒向多個其他節(jié)點發(fā)送ping消息申尤,用于檢測節(jié)點是否在線和交換彼此狀態(tài)信息,ping消息發(fā)送封裝了自身節(jié)點和部分其他節(jié)點的狀態(tài)數(shù)據(jù)衙耕。 -
pong消息
當接收到ping昧穿、meet消息時,作為響應消息回復給發(fā)送方確認消息正常通信橙喘。pong消息內部封裝了自身狀態(tài)數(shù)據(jù)时鸵。節(jié)點也可以向集群內廣播自身的pong消息來通知整個集群對自身狀態(tài)進行更新。 -
fail消息
當節(jié)點判定集群內另一個節(jié)點下線時渴杆,會向集群內廣播一個fail消息寥枝,其他節(jié)點接收到fail消息之后把對應節(jié)點更新為下線狀態(tài)宪塔。
通信過程
我們知道集群中的節(jié)點為了交換自身的槽位信息磁奖,節(jié)點與節(jié)點之間會不停的進行通信。通信采用Gossip協(xié)議某筐,工作原理是節(jié)點彼此不停的通信交換信息比搭,一段時間后所有的節(jié)點都會知道集群的完整信息,有點類似流言傳播南誊。節(jié)點ping其它節(jié)點的時候身诺,也會把其它節(jié)點的信息帶上,接收方會記錄這些節(jié)點的信息抄囚,然后再向這些節(jié)點發(fā)送ping信息霉赡。
槽位信息在哪里?
typedef struct {
char sig[4]; /* 信號標示 */
uint32_t totlen; /* 消息總長度 */
uint16_t ver; /* 協(xié)議版本*/
uint16_t type; /* 消息類型,用于區(qū)分meet,ping,pong等消息 */
uint16_t count; /* 消息體包含的節(jié)點數(shù)量幔托,僅用于meet,ping,ping消息類型*/
uint64_t currentEpoch; /* 當前發(fā)送節(jié)點的配置紀元 */
uint64_t configEpoch; /* 主節(jié)點/從節(jié)點的主節(jié)點配置紀元 */
uint64_t offset; /* 復制偏移量 */
char sender[CLUSTER_NAMELEN]; /* 發(fā)送節(jié)點的nodeId */
unsigned char myslots[CLUSTER_SLOTS/8]; /* 發(fā)送節(jié)點負責的槽信息 */
char slaveof[CLUSTER_NAMELEN]; /* 如果發(fā)送節(jié)點是從節(jié)點穴亏,記錄對應主節(jié)點的nodeId */
uint16_t port; /* 端口號 */
uint16_t flags; /* 發(fā)送節(jié)點標識,區(qū)分主從角色,是否下線等 */
unsigned char state; /* 發(fā)送節(jié)點所處的集群狀態(tài) */
unsigned char mflags[3]; /* 消息標識 */
union clusterMsgData data /* 消息正文 */;
} clusterMsg;
我們來看一下消息的格式重挑,這里面有個myslots的char數(shù)組嗓化,長度為16383/8,這其實是一個bitmap,每一個位代表一個槽谬哀,如果該位為1刺覆,表示這個槽是屬于這個節(jié)點的。節(jié)點計算出某個key的槽位以后史煎,只需要對比一下這個bitmap的第幾個位是否是1谦屑,如果是1則它可以處理這個key驳糯,如果不是則查找一下其他節(jié)點的myslots,直到找到匹配的節(jié)點氢橙,然后把節(jié)點信息返回給客戶端结窘。
redis cluster 的伸縮
redis cluster 的伸縮實際就是槽在各個節(jié)點之間的轉移。
smart客戶端
redis-cli
現(xiàn)在來做一個實例充蓝,打開redis-cli隧枫,連接6379,如果處理一個不屬于這個節(jié)點的key:
139.199.168.61:6379> set name 11
(error) MOVED 5798 139.199.168.61:6380
可以看到節(jié)點6379返回一個重定向指令谓苟,name
這個key的槽為5798官脓,這個槽在139.199.168.61:6380
這臺服務器上。我們再去6380試試涝焙,可以看到可以正常處理卑笨。
139.199.168.61:6380> set name 11
OK
如果你想客戶端自己幫我們重定向,可以在啟動客戶端的時候 加上 -c
:
[root@VM_90_159_centos redis-3.2.6]# /usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6379 -c
139.199.168.61:6379> set name 14
-> Redirected to slot [5798] located at 139.199.168.61:6380
OK
JedisCluster
先上一份Java JedisCluster 的代碼:
Set<HostAndPort> jedisClusterNode = new HashSet<>();
//添加節(jié)點信息
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6379));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6380));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6381));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6382));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6383));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6384));
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
//初始化JedisCluster
JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);
logger.debug("name = {}", jedisCluster.get("name"));
jedisCluster.close();
前面我們說過仑撞,客戶端也會保存一份槽與節(jié)點的映射關系圖赤兴,當執(zhí)行某個命令的時候,也會計算CRC16(key) & 16383
得到槽的位置隧哮,然后從映射關系中找到對應的節(jié)點信息桶良,再向節(jié)點發(fā)送請求,如果節(jié)點信息返回的是moved指令沮翔,它會重新更新映射關系陨帆。那么這份映射關系保存在哪里呢?