Redis集群搭建及原理

一、哨兵模式
在 redis3.0之前,redis使用的哨兵架構(gòu),它借助 sentinel 工具來監(jiān)控 master 節(jié)點(diǎn)的狀態(tài);如果 master 節(jié)點(diǎn)異常,則會做主從切換,將一臺 slave 作為 master。
哨兵模式的缺點(diǎn):
(1)當(dāng)master掛掉的時候,sentinel 會選舉出來一個 master,選舉的時候是沒有辦法去訪問Redis的,會存在訪問瞬斷的情況;若是在電商網(wǎng)站大促的時候master給掛掉了,幾秒鐘損失好多訂單數(shù)據(jù);
(2)哨兵模式,對外只有master節(jié)點(diǎn)可以寫,slave節(jié)點(diǎn)只能用于讀。盡管Redis單節(jié)點(diǎn)最多支持10W的QPS,但是在電商大促的時候,寫數(shù)據(jù)的壓力全部在master上。
(3)Redis的單節(jié)點(diǎn)內(nèi)存不能設(shè)置過大,若數(shù)據(jù)過大在主從同步將會很慢;在節(jié)點(diǎn)啟動的時候,時間特別長;(從節(jié)點(diǎn)上有主節(jié)點(diǎn)的所有數(shù)據(jù))
二、Redis集群
1、Redis集群的介紹

Redis集群是一個由多個主從節(jié)點(diǎn)群組成的分布式服務(wù)集群,它具有復(fù)制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成節(jié)點(diǎn)移除和故障轉(zhuǎn)移的功能。需要將每個節(jié)點(diǎn)設(shè)置成集群模式,這種集群模式?jīng)]有中心節(jié)點(diǎn),可水平擴(kuò)展,據(jù)官方文檔稱可以線性擴(kuò)展到上萬個節(jié)點(diǎn)(官方推薦不超過1000個節(jié)點(diǎn))。redis集群的性能和高可用性均優(yōu)于之前版本的哨兵模式,且集群配置非常簡單。
2、Redis集群的優(yōu)點(diǎn):
(1)Redis集群有多個master,可以減小訪問瞬斷問題的影響;
若集群中有一個master掛了,正好需要向這個master寫數(shù)據(jù),這個操作需要等待一下;但是向其他master節(jié)點(diǎn)寫數(shù)據(jù)是不受影響的。
(2)Redis集群有多個master,可以提供更高的并發(fā)量;
(3)Redis集群可以分片存儲,這樣就可以存儲更多的數(shù)據(jù);
3、Redis集群的搭建
Redis的集群搭建最少需要3個master節(jié)點(diǎn),我們這里搭建3個master,每個下面掛一個slave節(jié)點(diǎn),總共6個Redis節(jié)點(diǎn);(3臺機(jī)器,每臺機(jī)器一主一從)
第1臺機(jī)器:192.168.1.1 8001端口 8002端口
第2臺機(jī)器:192.168.1.2 8001端口 8002端口
第3臺機(jī)器:192.168.1.3 8001端口 8002端口
第一步:創(chuàng)建文件夾
mkdir -p /usr1/redis/redis-cluster/8001 /usr1/redis/redis-cluster/8002
復(fù)制代碼第二步:將redis安裝目錄下的 redis.conf 文件分別拷貝到8001目錄下
cp /usr1/redis-5.0.3/redis.conf /usr1/redis/redis-cluster/8001
復(fù)制代碼第三步:修改redis.conf文件以下內(nèi)容
port 8001 daemonize yes
pidfile "/var/run/redis\_8001.pid" #指定數(shù)據(jù)文件存放位置,必須要指定不同的目錄位置,不然會丟失數(shù)據(jù) dir /usr1/redis/redis-cluster/8001/ #啟動集群模式
cluster\-enabled yes
#集群節(jié)點(diǎn)信息文件,這里800x最好和port對應(yīng)上
cluster\-config-file nodes-8001.conf
# 節(jié)點(diǎn)離線的超時時間
cluster\-node-timeout 5000 #去掉bind綁定訪問ip信息
#bind 127.0.0.1 #關(guān)閉保護(hù)模式
protected\-mode no
#啟動AOF文件
appendonly yes
#如果要設(shè)置密碼需要增加如下配置:
#設(shè)置redis訪問密碼
requirepass redis\-pw
#設(shè)置集群節(jié)點(diǎn)間訪問密碼,跟上面一致
masterauth redis\-pw
復(fù)制代碼第四步,把上面修改好的配置文件拷貝到8002文件夾下,并將8001修改為8002:
cp /usr1/redis/redis-cluster/8001/redis.conf /usr1/redis/redis-cluster/8002 cd /usr1/redis/redis-cluster/8002/ vim redis.conf
#批量修改字符串
:%s/8001/8002/g
復(fù)制代碼第五步,將本機(jī)(192.168.1.1)機(jī)器上的文件拷貝到另外兩臺機(jī)器上
scp /usr1/redis/redis-cluster/8001/redis.conf [email protected]:/usr1/redis/redis-cluster/8001/
scp /usr1/redis/redis-cluster/8002/redis.conf [email protected]:/usr1/redis/redis-cluster/8002/
scp /usr1/redis/redis-cluster/8001/redis.conf [email protected]:/usr1/redis/redis-cluster/8001/
scp /usr1/redis/redis-cluster/8002/redis.conf [email protected]:/usr1/redis/redis-cluster/8002/
復(fù)制代碼第六步,分別啟動這6個redis實(shí)例,然后檢查是否啟動成功
/usr1/redis/redis-5.0.3/src/redis-server /usr1/redis/redis-cluster/8001/redis.conf /usr1/redis/redis-5.0.3/src/redis-server /usr1/redis/redis-cluster/8002/redis.conf
ps -ef | grep redis
復(fù)制代碼
第七步,使用 redis-cli 創(chuàng)建整個 redis 集群(redis5.0版本之前使用的ruby腳本 redis-trib.rb)
運(yùn)行以上命令,完成搭建
/usr1/redis/redis-5.0.3/src/redis-cli -a redis-pw --cluster create --cluster-replicas 1 192.168.1.1:8001 192.168.1.1:8002 192.168.1.2:8001 192.168.1.2:8002 192.168.1.3:8001 192.168.1.3:8002
復(fù)制代碼
說明:
-a :密碼;
--cluster-replicas 1:表示1個master下掛1個slave;--cluster-replicas 2:表示1個master下掛2個slave。
擴(kuò)展:
查看幫助命令:src/redis‐cli --cluster help
create:創(chuàng)建一個集群環(huán)境host1:port1 ... hostN:portN
call:可以執(zhí)行redis命令
add\-node:將一個節(jié)點(diǎn)添加到集群里,第一個參數(shù)為新節(jié)點(diǎn)的ip:port,第二個參數(shù)為集群中任意一個已經(jīng)存在的節(jié)點(diǎn)的ip:port
del\-node:移除一個節(jié)點(diǎn)
reshard:重新分片
check:檢查集群狀態(tài)
復(fù)制代碼第八步,驗(yàn)證集群
(1)連接任意一個客戶端
/usr1/redis/redis-5.0.3/src/redis-cli -a redis-pw -c -h 192.168.1.1 -p 8001
復(fù)制代碼說明:‐a表示服務(wù)端密碼;‐c表示集群模式;-h指定ip地址;-p表示端口號
(2)查看集群的信息:cluster info

(3) 查看節(jié)點(diǎn)列表: cluster nodes slave 對應(yīng)的 master 從上面也可以看到;
從上面可以看到 slave掛在哪個 master 下面;

在 /usr1/redis/redis-cluster/8001/nodes-8001.conf 文件中存儲了節(jié)點(diǎn)信息;
(4)進(jìn)行數(shù)據(jù)操作驗(yàn)證;
(5)關(guān)閉集群則需要逐個進(jìn)行關(guān)閉,使用命令:
/usr/local/redis‐5.0.3/src/redis‐cli ‐a redis-pw ‐c ‐h 192.168.1.1 ‐p 8001 shutdown /usr/local/redis‐5.0.3/src/redis‐cli ‐a redis-pw ‐c ‐h 192.168.1.1 ‐p 8002 shutdown
......
復(fù)制代碼注意:在創(chuàng)建集群的時候,需要把所有節(jié)點(diǎn)機(jī)器上的防火關(guān)閉,保證 Redis的服務(wù)端口和集群節(jié)點(diǎn)通信的 gossip 端口能通;
systemctl stop firewalld # 臨時關(guān)閉防火墻
systemctl disable firewalld # 禁止開機(jī)啟動
4、Redis集群原理分析
Redis Cluster 將所有數(shù)據(jù)劃分為 16384 個 slots(槽位),每個節(jié)點(diǎn)負(fù)責(zé)其中一部分槽位。槽位的信息存儲于每個節(jié)點(diǎn)中。只有master節(jié)點(diǎn)會被分配槽位,slave節(jié)點(diǎn)不會分配槽位。
當(dāng)Redis Cluster 的客戶端來連接集群時,它也會得到一份集群的槽位配置信息,并將其緩存在客戶端本地。這樣當(dāng)客戶端要查找某個 key 時,可以直接定位到目標(biāo)節(jié)點(diǎn)。同時因?yàn)椴畚坏男畔⒖赡軙嬖诳蛻舳伺c服務(wù)器不一致的情況,還需要糾正機(jī)制來實(shí)現(xiàn)槽位信息的校驗(yàn)調(diào)整。
槽位定位算法

Cluster 默認(rèn)會對 key 值使用 crc16 算法進(jìn)行 hash 得到一個整數(shù)值,然后用這個整數(shù)值對 16384 進(jìn)行取模來得到具體槽位。
HASH_SLOT = CRC16(key) % 16384
跳轉(zhuǎn)重定位
當(dāng)客戶端向一個節(jié)點(diǎn)發(fā)出了指令,首先當(dāng)前節(jié)點(diǎn)會計(jì)算指令的 key 得到槽位信息,判斷計(jì)算的槽位是否歸當(dāng)前節(jié)點(diǎn)所管理;若槽位不歸當(dāng)前節(jié)點(diǎn)管理,這時它會向客戶端發(fā)送一個特殊的跳轉(zhuǎn)指令攜帶目標(biāo)操作的節(jié)點(diǎn)地址,告訴客戶端去連這個節(jié)點(diǎn)去獲取數(shù)據(jù)??蛻舳耸盏街噶詈蟪颂D(zhuǎn)到正確的節(jié)點(diǎn)上去操作,還會同步更新糾正本地的槽位映射表緩存,后續(xù)所有 key 將使用新的槽位映射表。

Redis集群節(jié)點(diǎn)之間的通信機(jī)制
維護(hù)集群的元數(shù)據(jù)有集中式和 gossip兩種方式,Redis 的集群節(jié)點(diǎn)之間的通信采取 gossip 協(xié)議進(jìn)行通信
(1)集中式:
優(yōu)點(diǎn):元數(shù)據(jù)的更新和讀取,時效性非常好,一旦元數(shù)據(jù)出現(xiàn)變更立即就會更新到集中式的存儲中,其他節(jié)點(diǎn)讀取的時候立即就可以立即感知到;
缺點(diǎn):所有的元數(shù)據(jù)的更新壓力全部集中在一個地方,可能導(dǎo)致元數(shù)據(jù)的存儲壓力。zookeeper使用該方式
(2)gossip:
gossip協(xié)議包含多種消息,包括ping,pong,meet,fail等等。。
優(yōu)點(diǎn):元數(shù)據(jù)的更新比較分散,不是集中在一個地方,更新請求會陸陸續(xù)續(xù),打到所有節(jié)點(diǎn)上去更新,有一定的延時,降低了壓力;
缺點(diǎn):元數(shù)據(jù)更新有延時可能導(dǎo)致集群的一些操作會有一些滯后。
每個節(jié)點(diǎn)都有一個專門用于節(jié)點(diǎn)間通信的端口,就是自己提供服務(wù)的端口號+10000,比如7001,那么用于節(jié)點(diǎn)間通信的就是17001端口。每個節(jié)點(diǎn)每隔一段時間都會往另外幾個節(jié)點(diǎn)發(fā)送ping消息,同時其他幾點(diǎn)接收到ping消息之后返回pong消息。
網(wǎng)絡(luò)抖動
網(wǎng)絡(luò)抖動就是非常常見的一種現(xiàn)象,突然之間部分連接變得不可訪問,然后很快又恢復(fù)正常。
為解決這種問題,Redis Cluster 提供了一種選項(xiàng) cluster--node--timeout ,表示當(dāng)某個節(jié)點(diǎn)失聯(lián)的時間超過了配置的 timeout時,才可以認(rèn)定該節(jié)點(diǎn)出現(xiàn)故障,需要進(jìn)行主從切換。如果沒有這個選項(xiàng),網(wǎng)絡(luò)抖動會導(dǎo)致主從頻繁切換 (數(shù)據(jù)的重新復(fù)制)。
Redis集群選舉原理
當(dāng) slave 發(fā)現(xiàn)自己的 master 變?yōu)?fail 狀態(tài)時,便嘗試進(jìn)行 FailOver,以期成為新的 master。由于掛掉的 master 有多個 slave,所以這些 slave 要去競爭成為 master 節(jié)點(diǎn),過程如下:
(1)slave1,slave2都 發(fā)現(xiàn)自己連接的 master 狀態(tài)變?yōu)?Fail;
(2)它們將自己記錄的集群 currentEpoch(選舉周期) 加1,并使用 gossip 協(xié)議去廣播 FailOver_auth_request 信息;
(3)其他節(jié)點(diǎn)接收到slave1、salve2的消息(只有master響應(yīng)),判斷請求的合法性,并給 slave1 或 slave2 發(fā)送 FailOver_auth_ack(對每個 epoch 只發(fā)一次ack);在一個選舉周期中,一個master只會響應(yīng)第一個給它發(fā)消息的slave;
(4)slave1 收集返回的 FailOver_auth_ack,它收到超過半數(shù)的 master 的 ack 后變成新 master;(這也是集群為什么至少需要3個master的原因,如果只有兩個master,其中一個掛了之后,只剩下一個主節(jié)點(diǎn)是不能選舉成功的)
(5)新的master節(jié)點(diǎn)去廣播 Pong 消息通知其他集群節(jié)點(diǎn),不需要再去選舉了。
從節(jié)點(diǎn)并不是在主節(jié)點(diǎn)一進(jìn)入 FAIL 狀態(tài)就馬上嘗試發(fā)起選舉,而是有一定延遲,一定的延遲確保我們等待FAIL狀態(tài)在集群中傳播,slave如果立即嘗試選舉,其它masters或許尚未意識到FAIL狀態(tài),可能會拒絕投票。
延遲計(jì)算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
SLAVE_RANK:表示此slave已經(jīng)從master復(fù)制數(shù)據(jù)的總量的rank。Rank越小代表已復(fù)制的數(shù)據(jù)越新。這種方式下,持有最新數(shù)據(jù)的slave將會首先發(fā)起選舉(理論上)
Redis集群為什么至少需要三個master節(jié)點(diǎn),并且推薦節(jié)點(diǎn)數(shù)為奇數(shù)?
因?yàn)樾耺aster的選舉需要大于半數(shù)的集群master節(jié)點(diǎn)同意才能選舉成功,如果只有兩個master節(jié)點(diǎn),當(dāng)其中一個掛了,是達(dá)不到選舉新master的條件的。
奇數(shù)個master節(jié)點(diǎn)可以在滿足選舉該條件的基礎(chǔ)上節(jié)省一個節(jié)點(diǎn),比如三個master節(jié)點(diǎn)和四個master節(jié)點(diǎn)的集群相比,大家如果都掛了一個master節(jié)點(diǎn)都能選舉新master節(jié)點(diǎn),如果都掛了兩個master節(jié)點(diǎn)都沒法選舉新master節(jié)點(diǎn)了,所以奇數(shù)的master節(jié)點(diǎn)更多的是從節(jié)省機(jī)器資源角度出發(fā)說的。
集群是否完整才能對外提供服務(wù)
當(dāng)redis.conf的配置cluster-require-full-coverage為no時,表示當(dāng)負(fù)責(zé)一個插槽的主庫下線且沒有相應(yīng)的從庫進(jìn)行故障恢復(fù)時,集群仍然可用,如果為yes則集群不可用。
5、Java 操作 Redis 集群
方式一(Jedis):
(1)引入jedis的maven坐標(biāo)
<dependency\>
<groupId\>redis.clients</groupId\>
<artifactId\>jedis</artifactId\>
<version\>2.9.0</version\>
</dependency\>
復(fù)制代碼(2)Java編寫的代碼,如下:
public class JedisClusterTest { public static void main(String\[\] args) throws IOException {
JedisPoolConfig config \= new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.1.1", 8001));
jedisClusterNode.add(new HostAndPort("192.168.1.1", 8002));
jedisClusterNode.add(new HostAndPort("192.168.1.2", 8001));
jedisClusterNode.add(new HostAndPort("192.168.1.2", 8002));
jedisClusterNode.add(new HostAndPort("192.168.1.3", 8001));
jedisClusterNode.add(new HostAndPort("192.168.1.3", 8002));
JedisCluster jedisCluster \= null; try { //connectionTimeout:指的是連接一個url的連接等待時間 //soTimeout:指的是連接上一個url,獲取response的返回等待時間
jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, "redis-pw", config);
System.out.println(jedisCluster.set("name", "zhangsan"));
System.out.println(jedisCluster.get("name"));
} catch (Exception e) {
e.printStackTrace();
} finally { if (jedisCluster != null) {
jedisCluster.close();
}
}
}
}
復(fù)制代碼方式二:SpringBoot 整合 Redis
(1)引入maven坐標(biāo)
<dependency\>
<groupId\>org.springframework.boot</groupId\>
<artifactId\>spring‐boot‐starter‐data‐redis</artifactId\>
</dependency\>
<dependency\>
<groupId\>org.apache.commons</groupId\>
<artifactId\>commons‐pool2</artifactId\>
</dependency\>
復(fù)制代碼(2)配置文件
server:
port: 8080 spring:
redis:
database: 0 timeout: 3000 password: redis\-pw
cluster:
nodes: 192.168.0.61:8001,192.168.0.62:8002,192.168.0.63:8003,192.168.0.61:8004,192.168.0.62:8005,192.168.0.6
3:8006 lettuce:
pool:
max‐idle: 50 min‐idle: 10 max‐active: 100 max‐wait: 1000
復(fù)制代碼(3)java代碼,如下:
@RestController public class TestController { private static final Logger logger = LoggerFactory.getLogger(TestController.class);
@Autowired private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/test\_cluster") public void testCluster() throws InterruptedException {
stringRedisTemplate.opsForValue().set("user:name", "wangwu");
System.out.println(stringRedisTemplate.opsForValue().get("user:name"));
}
}
復(fù)制代碼作者:風(fēng)止雨歇 鏈接:www.cnblogs.com/yufeng218/p…來源:cnblogs
