近年來(lái)redis多實(shí)例用架構(gòu)的演變過(guò)程
redis是基于內(nèi)存的高性能key-value數(shù)據(jù)庫(kù),若要讓redis的數(shù)據(jù)更穩(wěn)定安全,需要引入多實(shí)例以及相關(guān)的高可用架構(gòu)。而近年來(lái)redis的高可用架構(gòu)亦不斷改進(jìn),先后出現(xiàn)了本地持久化、主從備份、哨兵模式、redis-cluster群集高可用架構(gòu)等等方案。
1、redis普通主從模式
通過(guò)持久化功能,Redis保證了即使在服務(wù)器重啟的情況下也不會(huì)損失(或少量損失)數(shù)據(jù),因?yàn)槌志没瘯?huì)把內(nèi)存中數(shù)據(jù)保存到硬盤(pán)上,重啟會(huì)從硬盤(pán)上加載數(shù)據(jù)。 。但是由于數(shù)據(jù)是存儲(chǔ)在一臺(tái)服務(wù)器上的,如果這臺(tái)服務(wù)器出現(xiàn)硬盤(pán)故障等問(wèn)題,也會(huì)導(dǎo)致數(shù)據(jù)丟失。為了避免單點(diǎn)故障,通常的做法是將數(shù)據(jù)庫(kù)復(fù)制多個(gè)副本以部署在不同的服務(wù)器上,這樣即使有一臺(tái)服務(wù)器出現(xiàn)故障,其他服務(wù)器依然可以繼續(xù)提供服務(wù)。為此, Redis 提供了復(fù)制(replication)功能,可以實(shí)現(xiàn)當(dāng)一臺(tái)數(shù)據(jù)庫(kù)中的數(shù)據(jù)更新后,自動(dòng)將更新的數(shù)據(jù)同步到其他數(shù)據(jù)庫(kù)上。
在復(fù)制的概念中,數(shù)據(jù)庫(kù)分為兩類(lèi),一類(lèi)是主數(shù)據(jù)庫(kù)(master),另一類(lèi)是從數(shù)據(jù)庫(kù)(slave)。主數(shù)據(jù)庫(kù)可以進(jìn)行讀寫(xiě)操作,當(dāng)寫(xiě)操作導(dǎo)致數(shù)據(jù)變化時(shí)會(huì)自動(dòng)將數(shù)據(jù)同步給從數(shù)據(jù)庫(kù)。而從數(shù)據(jù)庫(kù)一般是只讀的,并接受主數(shù)據(jù)庫(kù)同步過(guò)來(lái)的數(shù)據(jù)。一個(gè)主數(shù)據(jù)庫(kù)可以擁有多個(gè)從數(shù)據(jù)庫(kù),而一個(gè)從數(shù)據(jù)庫(kù)只能擁有一個(gè)主數(shù)據(jù)庫(kù)。
主從模式的配置,一般只需要再作為slave的redis節(jié)點(diǎn)的conf文件上加入“slaveof masterip masterport”, 或者作為slave的redis節(jié)點(diǎn)啟動(dòng)時(shí)使用如下參考命令:
1redis-server --port 6380 --slaveof masterIp masterPort
redis的普通主從模式,能較好地避免單獨(dú)故障問(wèn)題,以及提出了讀寫(xiě)分離,降低了Master節(jié)點(diǎn)的壓力。互聯(lián)網(wǎng)上大多數(shù)的對(duì)redis讀寫(xiě)分離的教程,都是基于這一模式或架構(gòu)下進(jìn)行的。但實(shí)際上這一架構(gòu)并非是目前最好的redis高可用架構(gòu)。
2、redis哨兵模式高可用架構(gòu)
當(dāng)主數(shù)據(jù)庫(kù)遇到異常中斷服務(wù)后,開(kāi)發(fā)者可以通過(guò)手動(dòng)的方式選擇一個(gè)從數(shù)據(jù)庫(kù)來(lái)升格為主數(shù)據(jù)庫(kù),以使得系統(tǒng)能夠繼續(xù)提供服務(wù)。然而整個(gè)過(guò)程相對(duì)麻煩且需要人工介入,難以實(shí)現(xiàn)自動(dòng)化。 為此,Redis 2.8開(kāi)始提供了哨兵工具來(lái)實(shí)現(xiàn)自動(dòng)化的系統(tǒng)監(jiān)控和故障恢復(fù)功能。 哨兵的作用就是監(jiān)控redis主、從數(shù)據(jù)庫(kù)是否正常運(yùn)行,主出現(xiàn)故障自動(dòng)將從數(shù)據(jù)庫(kù)轉(zhuǎn)換為主數(shù)據(jù)庫(kù)。
顧名思義,哨兵的作用就是監(jiān)控Redis系統(tǒng)的運(yùn)行狀況。它的功能包括以下兩個(gè)。
(1)監(jiān)控主數(shù)據(jù)庫(kù)和從數(shù)據(jù)庫(kù)是否正常運(yùn)行。
(2)主數(shù)據(jù)庫(kù)出現(xiàn)故障時(shí)自動(dòng)將從數(shù)據(jù)庫(kù)轉(zhuǎn)換為主數(shù)據(jù)庫(kù)。
可以用info replication查看主從情況 例子: 1主2從 1哨兵,可以用命令起也可以用配置文件里 可以使用雙哨兵,更安全,參考命令如下:
redis-server --port 6379
redis-server --port 6380 --slaveof 192.168.0.167 6379
redis-server --port 6381 --slaveof 192.168.0.167 6379
redis-sentinel sentinel.conf
其中,哨兵配置文件sentinel.conf參考如下:
sentinel monitor mymaster 192.168.0.167 6379 1
其中mymaster表示要監(jiān)控的主數(shù)據(jù)庫(kù)的名字。配置哨兵監(jiān)控一個(gè)系統(tǒng)時(shí),只需要配置其監(jiān)控主數(shù)據(jù)庫(kù)即可,哨兵會(huì)自動(dòng)發(fā)現(xiàn)所有復(fù)制該主數(shù)據(jù)庫(kù)的從數(shù)據(jù)庫(kù)。
Master與slave的切換過(guò)程:
(1)slave leader升級(jí)為master
(2)其他slave修改為新master的slave
(3)客戶(hù)端修改連接
(4)老的master如果重啟成功,變?yōu)樾耺aster的slave
3、redis-cluster群集高可用架構(gòu)
即使使用哨兵,redis每個(gè)實(shí)例也是全量存儲(chǔ),每個(gè)redis存儲(chǔ)的內(nèi)容都是完整的數(shù)據(jù),浪費(fèi)內(nèi)存且有木桶效應(yīng)。為了最大化利用內(nèi)存,可以采用cluster群集,就是分布式存儲(chǔ)。即每臺(tái)redis存儲(chǔ)不同的內(nèi)容。
采用redis-cluster架構(gòu)正是滿(mǎn)足這種分布式存儲(chǔ)要求的集群的一種體現(xiàn)。redis-cluster架構(gòu)中,被設(shè)計(jì)成共有16384個(gè)hash slot。每個(gè)master分得一部分slot,其算法為:hash_slot = crc16(key) mod 16384 ,這就找到對(duì)應(yīng)slot。采用hash slot的算法,實(shí)際上是解決了redis-cluster架構(gòu)下,有多個(gè)master節(jié)點(diǎn)的時(shí)候,數(shù)據(jù)如何分布到這些節(jié)點(diǎn)上去。key是可用key,如果有{}則取{}內(nèi)的作為可用key,否則整個(gè)可以是可用key。群集至少需要3主3從,且每個(gè)實(shí)例使用不同的配置文件。
在redis-cluster架構(gòu)中,redis-master節(jié)點(diǎn)一般用于接收讀寫(xiě),而redis-slave節(jié)點(diǎn)則一般只用于備份,其與對(duì)應(yīng)的master擁有相同的slot集合,若某個(gè)redis-master意外失效,則再將其對(duì)應(yīng)的slave進(jìn)行升級(jí)為臨時(shí)redis-master。在redis的官方文檔中,對(duì)redis-cluster架構(gòu)上,有這樣的說(shuō)明:在cluster架構(gòu)下,默認(rèn)的,一般redis-master用于接收讀寫(xiě),而redis-slave則用于備份,當(dāng)有請(qǐng)求是在向slave發(fā)起時(shí),會(huì)直接重定向到對(duì)應(yīng)key所在的master來(lái)處理。但如果不介意讀取的是redis-cluster中有可能過(guò)期的數(shù)據(jù)并且對(duì)寫(xiě)請(qǐng)求不感興趣時(shí),則亦可通過(guò)readonly命令,將slave設(shè)置成可讀,然后通過(guò)slave獲取相關(guān)的key,達(dá)到讀寫(xiě)分離。具體可以參閱redis官方文檔(https://redis.io/commands/readonly)等相關(guān)內(nèi)容:
Enables read queries for a connection to a Redis Cluster slave node.
Normally slave nodes will redirect clients to the authoritative master for the hash slot involved in a given command, however clients can use slaves in order to scale reads using the READONLY command.
READONLY tells a Redis Cluster slave node that the client is willing to read possibly stale data and is not interested in running write queries.
When the connection is in readonly mode, the cluster will send a redirection to the client only if the operation involves keys not served by the slave's master node. This may happen because:
The client sent a command about hash slots never served by the master of this slave.
The cluster was reconfigured (for example resharded) and the slave is no longer able to serve commands for a given hash slot.
例如,我們假設(shè)已經(jīng)建立了一個(gè)三主三從的redis-cluster架構(gòu),其中A、B、C節(jié)點(diǎn)都是redis-master節(jié)點(diǎn),A1、B1、C1節(jié)點(diǎn)都是對(duì)應(yīng)的redis-slave節(jié)點(diǎn)。在我們只有master節(jié)點(diǎn)A,B,C的情況下,對(duì)應(yīng)redis-cluster如果節(jié)點(diǎn)B失敗,則群集無(wú)法繼續(xù),因?yàn)槲覀儧](méi)有辦法再在節(jié)點(diǎn)B的所具有的約三分之一的hash slot集合范圍內(nèi)提供相對(duì)應(yīng)的slot。然而,如果我們?yōu)槊總€(gè)主服務(wù)器節(jié)點(diǎn)添加一個(gè)從服務(wù)器節(jié)點(diǎn),以便最終集群由作為主服務(wù)器節(jié)點(diǎn)的A,B,C以及作為從服務(wù)器節(jié)點(diǎn)的A1,B1,C1組成,那么如果節(jié)點(diǎn)B發(fā)生故障,系統(tǒng)能夠繼續(xù)運(yùn)行。節(jié)點(diǎn)B1復(fù)制B,并且B失效時(shí),則redis-cluster將促使B的從節(jié)點(diǎn)B1作為新的主服務(wù)器節(jié)點(diǎn)并且將繼續(xù)正確地操作。但請(qǐng)注意,如果節(jié)點(diǎn)B和B1在同一時(shí)間發(fā)生故障,則Redis群集無(wú)法繼續(xù)運(yùn)行。
Redis群集配置參數(shù):在繼續(xù)之前,讓我們介紹一下Redis Cluster在redis.conf文件中引入的配置參數(shù)。有些命令的意思是顯而易見(jiàn)的,有些命令在你閱讀下面的解釋后才會(huì)更加清晰。
(1)cluster-enabled :如果想在特定的Redis實(shí)例中啟用Redis群集支持就設(shè)置為yes。 否則,實(shí)例通常作為獨(dú)立實(shí)例啟動(dòng)。
(2)cluster-config-file :請(qǐng)注意,盡管有此選項(xiàng)的名稱(chēng),但這不是用戶(hù)可編輯的配置文件,而是Redis群集節(jié)點(diǎn)每次發(fā)生更改時(shí)自動(dòng)保留群集配置(基本上為狀態(tài))的文件。
(3)cluster-node-timeout :Redis群集節(jié)點(diǎn)可以不可用的最長(zhǎng)時(shí)間,而不會(huì)將其視為失敗。 如果主節(jié)點(diǎn)超過(guò)指定的時(shí)間不可達(dá),它將由其從屬設(shè)備進(jìn)行故障切換。
(4)cluster-slave-validity-factor :如果設(shè)置為0,無(wú)論主設(shè)備和從設(shè)備之間的鏈路保持?jǐn)嚅_(kāi)連接的時(shí)間長(zhǎng)短,從設(shè)備都將嘗試故障切換主設(shè)備。 如果該值為正值,則計(jì)算最大斷開(kāi)時(shí)間作為節(jié)點(diǎn)超時(shí)值乘以此選項(xiàng)提供的系數(shù),如果該節(jié)點(diǎn)是從節(jié)點(diǎn),則在主鏈路斷開(kāi)連接的時(shí)間超過(guò)指定的超時(shí)值時(shí),它不會(huì)嘗試啟動(dòng)故障切換。
(5)cluster-migration-barrier :主設(shè)備將保持連接的最小從設(shè)備數(shù)量,以便另一個(gè)從設(shè)備遷移到不受任何從設(shè)備覆蓋的主設(shè)備。有關(guān)更多信息,請(qǐng)參閱本教程中有關(guān)副本遷移的相應(yīng)部分。
(6)cluster-require-full-coverage :如果將其設(shè)置為yes,則默認(rèn)情況下,如果key的空間的某個(gè)百分比未被任何節(jié)點(diǎn)覆蓋,則集群停止接受寫(xiě)入。 如果該選項(xiàng)設(shè)置為no,則即使只處理關(guān)于keys子集的請(qǐng)求,群集仍將提供查詢(xún)。
以下是最小的Redis集群配置文件:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
注意:
(1)redis-cluster最小配置為三主三從,當(dāng)1個(gè)主故障,大家會(huì)給對(duì)應(yīng)的從投票,把從立為主,若沒(méi)有從數(shù)據(jù)庫(kù)可以恢復(fù)則redis群集就down了。
(2)在這個(gè)redis cluster中,如果你要在slave讀取數(shù)據(jù),那么需要帶上readonly指令。redis cluster的核心的理念,主要是用slave做高可用的,每個(gè)master掛一兩個(gè)slave,主要是做數(shù)據(jù)的熱備,當(dāng)master故障時(shí)的作為主備切換,實(shí)現(xiàn)高可用的。redis cluster默認(rèn)是不支持slave節(jié)點(diǎn)讀或者寫(xiě)的,跟我們手動(dòng)基于replication搭建的主從架構(gòu)不一樣的。slave node要設(shè)置readonly,然后再get,這個(gè)時(shí)候才能在slave node進(jìn)行讀取。對(duì)于redis -cluster主從架構(gòu),若要進(jìn)行讀寫(xiě)分離,官方其實(shí)是不建議的,但也能做,只是會(huì)復(fù)雜一些。具體見(jiàn)下面的章節(jié)。
(3)redis-cluster的架構(gòu)下,實(shí)際上本身master就是可以任意擴(kuò)展的,你如果要支撐更大的讀吞吐量,或者寫(xiě)吞吐量,或者數(shù)據(jù)量,都可以直接對(duì)master進(jìn)行橫向擴(kuò)展就可以了。也擴(kuò)容master,跟之前擴(kuò)容slave進(jìn)行讀寫(xiě)分離,效果是一樣的或者說(shuō)更好。
(4)可以使用自帶客戶(hù)端連接:使用redis-cli -c -p cluster中任意一個(gè)端口,進(jìn)行數(shù)據(jù)獲取測(cè)試。
Java中對(duì)redis-cluster數(shù)據(jù)的一般讀取方法簡(jiǎn)介
使用Jedis讀寫(xiě)redis-cluster的數(shù)據(jù)
由于Jedis類(lèi)一般只能對(duì)一臺(tái)redis-master進(jìn)行數(shù)據(jù)操作,所以面對(duì)redis-cluster多臺(tái)master與slave的群集,Jedis類(lèi)就不能滿(mǎn)足了。這個(gè)時(shí)候我們需要引用另外一個(gè)操作類(lèi):JedisCluster類(lèi)。
例如我們有6臺(tái)機(jī)器組成的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機(jī)器對(duì)應(yīng)端口:7000、7004、7005
slave對(duì)應(yīng)端口:7001、7002、7003
使用JedisCluster對(duì)redis-cluster進(jìn)行數(shù)據(jù)操作的參考代碼如下:
// 添加nodes服務(wù)節(jié)點(diǎn)到Set集合Set<HostAndPort> hostAndPortsSet = new HashSet<HostAndPort>();// 添加節(jié)點(diǎn)hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7000));
hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7001));
hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7002));
hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7003));
hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7004));
hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7005));
// Jedis連接池配置JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxTotal(500);
jedisPoolConfig.setMinIdle(0);
jedisPoolConfig.setMaxWaitMillis(2000); // 設(shè)置2秒jedisPoolConfig.setTestOnBorrow(true);
JedisCluster jedisCluster = new JedisCluster(hostAndPortsSet ,jedisPoolConfig);String result = jedisCluster.get("event:10");
System.out.println(result);
運(yùn)行結(jié)果截圖如下圖所示:
第一節(jié)中我們已經(jīng)介紹了redis-cluster架構(gòu)下master提供讀寫(xiě)功能,而slave一般只作為對(duì)應(yīng)master機(jī)器的數(shù)據(jù)備份不提供讀寫(xiě)。如果我們只在hostAndPortsSet中只配置slave,而不配置master,實(shí)際上還是可以讀到數(shù)據(jù),但其內(nèi)部操作實(shí)際是通過(guò)slave重定向到相關(guān)的master主機(jī)上,然后再將結(jié)果獲取和輸出。
上面是普通項(xiàng)目使用JedisCluster的簡(jiǎn)單過(guò)程,若在spring boot項(xiàng)目中,可以定義JedisConfig類(lèi),使用@Configuration、@Value、@Bean等一些列注解完成JedisCluster的配置,然后再注入該JedisCluster到相關(guān)service邏輯中引用,這里介紹略。
使用Lettuce讀寫(xiě)redis-cluster數(shù)據(jù)
Lettuce 和 Jedis 的定位都是Redis的client。Jedis在實(shí)現(xiàn)上是直接連接的redis server,如果在多線程環(huán)境下是非線程安全的,這個(gè)時(shí)候只有使用連接池,為每個(gè)Jedis實(shí)例增加物理連接,每個(gè)線程都去拿自己的 Jedis 實(shí)例,當(dāng)連接數(shù)量增多時(shí),物理連接成本就較高了。
Lettuce的連接是基于Netty的,連接實(shí)例(StatefulRedisConnection)可以在多個(gè)線程間并發(fā)訪問(wèn),應(yīng)為StatefulRedisConnection是線程安全的,所以一個(gè)連接實(shí)例(StatefulRedisConnection)就可以滿(mǎn)足多線程環(huán)境下的并發(fā)訪問(wèn),當(dāng)然這個(gè)也是可伸縮的設(shè)計(jì),一個(gè)連接實(shí)例不夠的情況也可以按需增加連接實(shí)例。
其中spring boot 2.X版本中,依賴(lài)的spring-session-data-redis已經(jīng)默認(rèn)替換成Lettuce了。
同樣,例如我們有6臺(tái)機(jī)器組成的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機(jī)器對(duì)應(yīng)端口:7000、7004、7005
slave對(duì)應(yīng)端口:7001、7002、7003
在spring boot 2.X版本中使用Lettuce操作redis-cluster數(shù)據(jù)的方法參考如下:
(1)pom文件參考如下:
parent中指出spring boot的版本,要求2.X以上:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- lookup parent from repository -->
依賴(lài)中需要加入spring-boot-starter-data-redis,參考如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)springboot的配置文件要包含如下內(nèi)容:spring.redis.database=0
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-wait=500
spring.redis.cluster.timeout=1000
spring.redis.cluster.max-redirects=3
spring.redis.cluster.nodes=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
(3)新建RedisConfiguration類(lèi),參考代碼如下:@Configuration
public class RedisConfiguration {
[@Resource](https://my.oschina.net/u/929718) private LettuceConnectionFactory myLettuceConnectionFactory;
<a >@Bean</a>
public RedisTemplate<String, Serializable> redisTemplate() {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
//template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setValueSerializer(new StringRedisSerializer());
template.setConnectionFactory(myLettuceConnectionFactory);
return template;
}
}
(4)新建RedisFactoryConfig類(lèi),參考代碼如下:
@Configuration
public class RedisFactoryConfig {
@Autowired
private Environment environment;
<a >@Bean</a>
public RedisConnectionFactory myLettuceConnectionFactory() {
Map<String, Object> source = new HashMap<String, Object>();
source.put("spring.redis.cluster.nodes", environment.getProperty("spring.redis.cluster.nodes"));
source.put("spring.redis.cluster.timeout", environment.getProperty("spring.redis.cluster.timeout"));
source.put("spring.redis.cluster.max-redirects", environment.getProperty("spring.redis.cluster.max-redirects"));
RedisClusterConfiguration redisClusterConfiguration;
redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
return new LettuceConnectionFactory(redisClusterConfiguration);
}
}
(5)在業(yè)務(wù)類(lèi)service中注入Lettuce相關(guān)的RedisTemplate,進(jìn)行相關(guān)操作。以下是我化簡(jiǎn)到了springbootstarter中進(jìn)行,參考代碼如下:
@SpringBootApplication
public class NewRedisClientApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(NewRedisClientApplication.class, args);
RedisTemplate redisTemplate = (RedisTemplate)context.getBean("redisTemplate");
String rtnValue = (String)redisTemplate.opsForValue().get("event:10");
System.out.println(rtnValue);
}
}
運(yùn)行結(jié)果的截圖如下:
以上的介紹,是采用Jedis以及Lettuce對(duì)redis-cluster數(shù)據(jù)的簡(jiǎn)單讀取。Jedis也好,Lettuce也好,其對(duì)于redis-cluster架構(gòu)下的數(shù)據(jù)的讀取,都是默認(rèn)是按照redis官方對(duì)redis-cluster的設(shè)計(jì),自動(dòng)進(jìn)行重定向到master節(jié)點(diǎn)中進(jìn)行的,哪怕是我們?cè)谂渲弥辛谐隽怂械膍aster節(jié)點(diǎn)和slave節(jié)點(diǎn)。查閱了Jedis以及Lettuce的github上的源碼,默認(rèn)不支持redis-cluster下的讀寫(xiě)分離,可以看出Jedis若要支持redis-cluster架構(gòu)下的讀寫(xiě)分離,需要自己改寫(xiě)和構(gòu)建多一些包裝類(lèi),定義好Master和slave節(jié)點(diǎn)的邏輯;而Lettuce的源碼中,實(shí)際上預(yù)留了方法(setReadForm(ReadFrom.SLAVE))進(jìn)行redis-cluster架構(gòu)下的讀寫(xiě)分離,相對(duì)來(lái)說(shuō)修改會(huì)簡(jiǎn)單一些,具體可以參考后面的章節(jié)。
redis-cluster架構(gòu)下的讀寫(xiě)能力的優(yōu)化方案
在上面的一些章節(jié)中,已經(jīng)有講到redis近年來(lái)的高可用架構(gòu)的演變,以及在redis-cluster架構(gòu)下,官方對(duì)redis-master、redis-slave的其實(shí)有使用上的建議,即redis-master節(jié)點(diǎn)一般用于接收讀寫(xiě),而redis-slave節(jié)點(diǎn)則一般只用于備份,其與對(duì)應(yīng)的master擁有相同的slot集合,若某個(gè)redis-master意外失效,則再將其對(duì)應(yīng)的slave進(jìn)行升級(jí)為臨時(shí)redis-master。但如果不介意讀取的是redis-cluster中有可能過(guò)期的數(shù)據(jù)并且對(duì)寫(xiě)請(qǐng)求不感興趣時(shí),則亦可通過(guò)readonly命令,將slave設(shè)置成可讀,然后通過(guò)slave獲取相關(guān)的key,達(dá)到讀寫(xiě)分離。
實(shí)際上本身master就是可以任意擴(kuò)展的,所以如果要支撐更大的讀吞吐量,或者寫(xiě)吞吐量,或者數(shù)據(jù)量,都可以直接對(duì)master進(jìn)行橫向水平擴(kuò)展就可以了。也就是說(shuō),擴(kuò)容master,跟之前擴(kuò)容slave并進(jìn)行讀寫(xiě)分離,效果是一樣的或者說(shuō)更好。
所以下面我們將按照redis-cluster架構(gòu)下分別進(jìn)行水平擴(kuò)展Master,以及在redis-cluster架構(gòu)下對(duì)master、slave進(jìn)行讀寫(xiě)分離兩套方案進(jìn)行講解。
(一)水平擴(kuò)展Master實(shí)例來(lái)進(jìn)行redis-cluster性能的提升
redis官方在線文檔以及一些互聯(lián)網(wǎng)的參考資料都表明,在redis-cluster架構(gòu)下,實(shí)際上不建議做物理的讀寫(xiě)分離。那么如果我們真的不做讀寫(xiě)分離的話(huà),能否通過(guò)簡(jiǎn)單的方法進(jìn)行redis-cluster下的性能的提升?我們可以通過(guò)master的水平擴(kuò)展,來(lái)橫向擴(kuò)展讀寫(xiě)吞吐量,并且能支撐更多的海量數(shù)據(jù)。
對(duì)master進(jìn)行水平擴(kuò)展有兩種方法,一種是單機(jī)上面進(jìn)行master實(shí)例的增加(建議每新增一個(gè)master,也新增一個(gè)對(duì)應(yīng)的slave),另一種是新增機(jī)器部署新的master實(shí)例(同樣建議每新增一個(gè)master,也新增一個(gè)對(duì)應(yīng)的slave)。當(dāng)然,我們也可以進(jìn)行這兩種方法的有效結(jié)合。
(1)單機(jī)上通過(guò)多線程建立新redis-master實(shí)例,即邏輯上的水平擴(kuò)展:
一般的,對(duì)于redis單機(jī),單線程的讀吞吐是4w/s~5W/s,寫(xiě)吞吐為2w/s。
單機(jī)合理開(kāi)啟redis多線程情況下(一般線程數(shù)為CPU核數(shù)的倍數(shù)),總吞吐量會(huì)有所上升,但每個(gè)線程的平均處理能力會(huì)有所下降。例如一個(gè)2核CPU,開(kāi)啟2線程的時(shí)候,總讀吞吐能上升是6W/s~7W/s,即每個(gè)線程平均約3W/s再多一些。但過(guò)多的redis線程反而會(huì)限制了總吞吐量。
(2)擴(kuò)展更多的機(jī)器,部署新redis-master實(shí)例,即物理上的水平擴(kuò)展:
例如,我們可以再原來(lái)只有3臺(tái)master的基礎(chǔ)上,連入新機(jī)器繼續(xù)新實(shí)例的部署,最終水平擴(kuò)展為6臺(tái)master(建議每新增一個(gè)master,也新增一個(gè)對(duì)應(yīng)的slave)。例如之前每臺(tái)master的處理能力假設(shè)是讀吞吐5W/s,寫(xiě)吞吐2W/s,擴(kuò)展前一共的處理能力是:15W/s讀,6W/s寫(xiě)。如果我們水平擴(kuò)展到6臺(tái)master,讀吞吐可以達(dá)到總量30W/s,寫(xiě)可以達(dá)到12w/s,性能能夠成倍增加。
(3)若原本每臺(tái)部署redis-master實(shí)例的機(jī)器都性能良好,則可以通過(guò)上述兩者的結(jié)合,進(jìn)行一個(gè)更優(yōu)的組合。
使用該方案進(jìn)行redis-cluster性能的提升的優(yōu)點(diǎn)有:
(1)符合redis官方要求和數(shù)據(jù)的準(zhǔn)確性。
(2)真正達(dá)到更大吞吐量的性能擴(kuò)展。
(3)無(wú)需代碼的大量更改,只需在配置文件中重新配置新的節(jié)點(diǎn)信息。
當(dāng)然缺點(diǎn)也是有的:
(1)需要新增機(jī)器,提升性能,即成本會(huì)增加。
(2)若不新增機(jī)器,則需要原來(lái)的實(shí)例所運(yùn)行的機(jī)器性能較好,能進(jìn)行以多線程的方式部署新實(shí)例。但隨著線程的增多,而機(jī)器的能力不足以支撐的時(shí)候,實(shí)際上總體能力會(huì)提升不太明顯。
(3)redis-cluster進(jìn)行新的水平擴(kuò)容后,需要對(duì)master進(jìn)行新的hash slot重新分配,這相當(dāng)于需要重新加載所有的key,并按算法平均分配到各個(gè)Master的slot當(dāng)中。
(二)引入Lettuce以及修改相關(guān)方法,達(dá)到對(duì)redis-cluster的讀寫(xiě)分離
通過(guò)上面的一些章節(jié),我們已經(jīng)可以了解到Lettuce客戶(hù)端讀取redis的一些操作,使用Lettuce能體現(xiàn)出了簡(jiǎn)單,安全,高效。實(shí)際上,查閱了Lettuce對(duì)redis的讀寫(xiě),許多地方都進(jìn)行了redis的讀寫(xiě)分離。但這些都是基于上述redis架構(gòu)中最普通的主從分離架構(gòu)下的讀寫(xiě)分離,而對(duì)于redis-cluster架構(gòu)下,Lettuce可能是遵循了redis官方的意見(jiàn),在該架構(gòu)下,Lettuce在源碼中直接設(shè)置了只由master上進(jìn)行讀寫(xiě)(具體參見(jiàn)gitHub的Lettuce項(xiàng)目):
那么如果真的需要讓Lettuce改為能夠讀取redis-cluster的slave,進(jìn)行讀寫(xiě)分離,是否可行?實(shí)際上還是可以的。這就需要我們自己在項(xiàng)目中進(jìn)行二次加工,即不使用spring-boot中的默認(rèn)Lettuce初始化方法,而是自己去寫(xiě)一個(gè)屬于自己的Lettuce的新RedisClusterClient的連接,并且對(duì)該RedisClusterClient的連接進(jìn)行一個(gè)比較重要的設(shè)置,那就是由connection.setReadFrom(ReadFrom.MASTER)改為connection.setReadFrom(ReadFrom.SLAVE)。
下面我們開(kāi)始對(duì)之前章節(jié)中的Lettuce讀取redis-cluster數(shù)據(jù)的例子,進(jìn)行改寫(xiě),讓Lettuce能夠支持該架構(gòu)下的讀寫(xiě)分離:
spring boot 2.X版本中,依賴(lài)的spring-session-data-redis已經(jīng)默認(rèn)替換成Lettuce了。
同樣,例如我們有6臺(tái)機(jī)器組成的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機(jī)器對(duì)應(yīng)端口:7000、7004、7005
slave對(duì)應(yīng)端口:7001、7002、7003
在spring boot 2.X版本中使用Lettuce操作redis-cluster數(shù)據(jù)的方法參考如下:
(1)pom文件參考如下:
parent中指出spring boot的版本,要求2.X以上:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- lookup parent from repository -->
依賴(lài)中需要加入spring-boot-starter-data-redis,參考如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)springboot的配置文件要包含如下內(nèi)容:
spring.redis.database=0
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-wait=500
spring.redis.cluster.timeout=1000
spring.redis.cluster.max-redirects=3
spring.redis.cluster.nodes=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
(3)我們回到RedisConfiguration類(lèi)中,刪除或屏蔽之前的RedisTemplate方法,新增自定義的redisClusterConnection方法,并且設(shè)置好讀寫(xiě)分離,參考代碼如下:
@Configuration
public class RedisConfiguration {
@Autowired
private Environment environment;
<a >@Bean</a>
public StatefulRedisClusterConnection redisClusterConnection(){
String strRedisClusterNodes = environment.getProperty("spring.redis.cluster.nodes");
String[] listNodesInfos = strRedisClusterNodes.split(",");
List<RedisURI> listRedisURIs = new ArrayList<RedisURI>();
for(String tmpNodeInfo : listNodesInfos){
String[] tmpInfo = tmpNodeInfo.split(":");
listRedisURIs.add(new RedisURI(tmpInfo[0],Integer.parseInt(tmpInfo[1]),Duration.ofDays(10)));
}
RedisClusterClient clusterClient = RedisClusterClient.create(listRedisURIs);
StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
connection.setReadFrom(ReadFrom.SLAVE);
return connection;
}
}
其中,這三行代碼是能進(jìn)行redis-cluster架構(gòu)下讀寫(xiě)分離的核心:
RedisClusterClient clusterClient = RedisClusterClient.create(listRedisURIs);
StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
connection.setReadFrom(ReadFrom.SLAVE);
在業(yè)務(wù)類(lèi)service中注入Lettuce相關(guān)的redisClusterConnection,進(jìn)行相關(guān)讀寫(xiě)操作。以下是我直接化簡(jiǎn)到了springbootstarter中進(jìn)行,參考代碼如下:
@SpringBootApplication
public class NewRedisClientApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(NewRedisClientApplication.class, args);
StatefulRedisClusterConnection<String, String> redisClusterConnection = (StatefulRedisClusterConnection)context.getBean("redisClusterConnection");
System.out.println(redisClusterConnection.sync().get("event:10"));
}
}
運(yùn)行的結(jié)果如下圖所示:
可以看到,經(jīng)過(guò)改寫(xiě)的redisClusterConnection的確能讀取到redis-cluster的數(shù)據(jù)。但這一個(gè)數(shù)據(jù)我們還需要驗(yàn)證一下到底是不是通過(guò)slave讀取到的,又或者還是通過(guò)slave重定向給master才獲取到的?
帶著疑問(wèn),我們可以開(kāi)通debug模式,在redisClusterConnection.sync().get(“event:10”)等類(lèi)似的獲取數(shù)據(jù)的代碼行上面打上斷點(diǎn)。通過(guò)代碼的走查,我們可以看到,在ReadFromImpl類(lèi)中,最終會(huì)select到key所在的slave節(jié)點(diǎn),進(jìn)行返回,并在該slave中進(jìn)行數(shù)據(jù)的讀取:
ReadFromImpl顯示:
另外我們通過(guò)connectFuture中的顯示也驗(yàn)證了對(duì)于slave的readonly生效了:
這樣,就達(dá)到了通過(guò)Lettuce客戶(hù)端對(duì)redis-cluster的讀寫(xiě)分離了。
使用該方案進(jìn)行redis-cluster性能的提升的優(yōu)點(diǎn)有:
(1)直接通過(guò)代碼級(jí)更改,而不需要配置新的redis-cluster環(huán)境。
(2)無(wú)需增加機(jī)器或升級(jí)硬件設(shè)備。
但同時(shí),該方案也有缺點(diǎn):
(1)非官方對(duì)redis-cluster的推薦方案,因?yàn)樵趓edis-cluster架構(gòu)下,進(jìn)行讀寫(xiě)分離,有可能會(huì)讀到過(guò)期的數(shù)據(jù)。
(2)需對(duì)項(xiàng)目進(jìn)行全面的替換,將Jedis客戶(hù)端變?yōu)長(zhǎng)ettuce客戶(hù)端,對(duì)代碼的改動(dòng)較大,而且使用Lettuce時(shí),使用的并非spring boot的自帶集成Lettuce的redisTemplate配置方法,而是自己配置讀寫(xiě)分離的 redisClusterConnetcion,日后遇到問(wèn)題的時(shí)候,可能官方文檔的支持率或支撐能力會(huì)比較低。
(3)需修改redis-cluster的master、slave配置,在各個(gè)節(jié)點(diǎn)中都需要加入slave-read-only yes。
(4)性能的提升沒(méi)有水平擴(kuò)展master主機(jī)和實(shí)例來(lái)得直接干脆。
總結(jié)
總體上來(lái)說(shuō),redis-cluster高可用架構(gòu)方案是目前最好的redis架構(gòu)方案,redis的官方對(duì)redis-cluster架構(gòu)是建議redis-master用于接收讀寫(xiě),而redis-slave則用于備份(備用),默認(rèn)不建議讀寫(xiě)分離。但如果不介意讀取的是redis-cluster中有可能過(guò)期的數(shù)據(jù)并且對(duì)寫(xiě)請(qǐng)求不感興趣時(shí),則亦可通過(guò)readonly命令,將slave設(shè)置成可讀,然后通過(guò)slave獲取相關(guān)的key,達(dá)到讀寫(xiě)分離。Jedis、Lettuce都可以進(jìn)行redis-cluster的讀寫(xiě)操作,而且默認(rèn)只針對(duì)Master進(jìn)行讀寫(xiě),若要對(duì)redis-cluster架構(gòu)下進(jìn)行讀寫(xiě)分離,則Jedis需要進(jìn)行源碼的較大改動(dòng),而Lettuce開(kāi)放了setReadFrom()方法,可以進(jìn)行二次封裝成讀寫(xiě)分離的客戶(hù)端,相對(duì)簡(jiǎn)單,而且Lettuce比Jedis更安全。redis-cluster架構(gòu)下可以直接通過(guò)水平擴(kuò)展master來(lái)達(dá)到性能的提升。
歡迎學(xué)Java和大數(shù)據(jù)的朋友們加入java架構(gòu)交流: 855835163
群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時(shí)間來(lái)學(xué)習(xí)提升自己,不要再用"沒(méi)有時(shí)間“來(lái)掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來(lái)的自己一個(gè)交代!