目錄
- 一、前言
- 二、Redis Sentinel 及 Redis Cluster 簡介
- 1、Redis Sentinel
- 1.1、Redis Sentinel 集群模式的 “仲裁會(huì)”
- 2、Redis Cluster
- 三、Redis Sentinel 及Redis Cluster 實(shí)踐
- 四、Redis的過期淘汰策略
- 五、Redis 使用過程中踩過的坑
一、前言
互聯(lián)網(wǎng)高速發(fā)展的今天,對(duì)應(yīng)用系統(tǒng)的抗壓能力要求越來越高,傳統(tǒng)的應(yīng)用層+數(shù)據(jù)庫已經(jīng)不能滿足當(dāng)前的需要。所以一大批內(nèi)存式數(shù)據(jù)庫和Nosql數(shù)據(jù)庫應(yīng)運(yùn)而生,其中redis,memcache,mongodb,hbase等被廣泛的使用來提高系統(tǒng)的吞吐性,所以如何正確使用cache是作為開發(fā)的一項(xiàng)基技能。本文主要介紹Redis Sentinel 及 Redis Cluster的區(qū)別及用法,Redis的基本操作可以自行去參看其官方文檔 。 其他幾種cache有興趣的可自行找資料去學(xué)習(xí)。
二、Redis Sentinel 及 Redis Cluster 簡介
1、Redis Sentinel
Redis-Sentinel(哨兵模式)是Redis官方推薦的高可用性(HA)解決方案,當(dāng)用Redis做Master-slave的高可用方案時(shí),假如master宕機(jī)了,Redis本身(包括它的很多客戶端)都沒有實(shí)現(xiàn)自動(dòng)進(jìn)行主備切換,而Redis-sentinel本身也是一個(gè)獨(dú)立運(yùn)行的進(jìn)程,它能監(jiān)控多個(gè)master-slave集群,發(fā)現(xiàn)master宕機(jī)后能進(jìn)行自懂切換。它的主要功能有以下幾點(diǎn):
- 不時(shí)地監(jiān)控redis是否按照預(yù)期良好地運(yùn)行;
- 如果發(fā)現(xiàn)某個(gè)redis節(jié)點(diǎn)運(yùn)行出現(xiàn)狀況,能夠通知另外一個(gè)進(jìn)程(例如它的客戶端);
- 能夠進(jìn)行自動(dòng)切換。當(dāng)一個(gè)master節(jié)點(diǎn)不可用時(shí),能夠選舉出master的多個(gè)slave(如果有超過一個(gè)slave的話)中的一個(gè)來作為新的master,其它的slave節(jié)點(diǎn)會(huì)將它所追隨的master的地址改為被提升為master的slave的新地址。
Redis master-slave 模式如下圖:

從上圖片中可以看到,一個(gè)master 節(jié)點(diǎn)可以掛多個(gè)slave ,Redis Sentinel 管理Redis 節(jié)點(diǎn)結(jié)構(gòu)如下:
上圖中可以得出Sentinel其實(shí)就是Client和Redis之間的橋梁,所有的客戶端都通過Sentinel程序獲取Redis的Master服務(wù)。首先Sentinel是集群部署的,Client可以鏈接任何一個(gè)Sentinel服務(wù)所獲的結(jié)果都是一致的。其次,所有的Sentinel服務(wù)都會(huì)對(duì)Redis的主從服務(wù)進(jìn)行監(jiān)控,當(dāng)監(jiān)控到Master服務(wù)無響應(yīng)的時(shí)候,Sentinel內(nèi)部進(jìn)行仲裁,從所有的 Slave選舉出一個(gè)做為新的Master。并且把其他的slave作為新的Master的Slave。最后通知所有的客戶端新的Master服務(wù)地址。如果舊的Master服務(wù)地址重新啟動(dòng),這個(gè)時(shí)候,它將被設(shè)置為Slave服務(wù)。
Sentinel 可以管理master-slave節(jié)點(diǎn),看似Redis的穩(wěn)定性得到一個(gè)比較好的保障。但是如果Sentinel是單節(jié)點(diǎn)的話,如果Sentinel宕機(jī)了,那master-slave這種模式就不能發(fā)揮其作用了。幸好Sentinel也支持集群模式,Sentinel的集群模式主要有以下幾個(gè)好處:
- 即使有一些sentinel進(jìn)程宕掉了,依然可以進(jìn)行redis集群的主備切換;
- 如果只有一個(gè)sentinel進(jìn)程,如果這個(gè)進(jìn)程運(yùn)行出錯(cuò),或者是網(wǎng)絡(luò)堵塞,那么將無法實(shí)現(xiàn)redis集群的主備切換(單點(diǎn)問題);
- 如果有多個(gè)sentinel,redis的客戶端可以隨意地連接任意一個(gè)sentinel來獲得關(guān)于redis集群中的信息。
Redis Sentinel 集群模式可以增強(qiáng)整個(gè)Redis集群的穩(wěn)定性與可靠性,但是當(dāng)某個(gè)節(jié)點(diǎn)的master節(jié)點(diǎn)掛了要重新選取出新的master節(jié)點(diǎn)時(shí),Redis Sentinel的集群模式選取的復(fù)雜度顯然高于單點(diǎn)的Redis Sentinel 模式,此時(shí)需要一個(gè)比較靠譜的選取算法。下面就來介紹Redis Sentinel 集群模式的 “仲裁會(huì)”(多個(gè)Redis Sentinel共同商量誰是Redis 的 master節(jié)點(diǎn))
1.1、Redis Sentinel 集群模式的 “仲裁會(huì)”
當(dāng)一個(gè)master被sentinel集群監(jiān)控時(shí),需要為它指定一個(gè)參數(shù),這個(gè)參數(shù)指定了當(dāng)需要判決master為不可用,并且進(jìn)行failover時(shí),所需要的sentinel數(shù)量,本文中我們暫時(shí)稱這個(gè)參數(shù)為票數(shù),不過,當(dāng)failover主備切換真正被觸發(fā)后,failover并不會(huì)馬上進(jìn)行,還需要sentinel中的大多數(shù)sentinel授權(quán)后才可以進(jìn)行failover。當(dāng)ODOWN時(shí),failover被觸發(fā)。failover一旦被觸發(fā),嘗試去進(jìn)行failover的sentinel會(huì)去獲得“大多數(shù)”sentinel的授權(quán)(如果票數(shù)比大多數(shù)還要大的時(shí)候,則詢問更多的sentinel)這個(gè)區(qū)別看起來很微妙,但是很容易理解和使用。例如,集群中有5個(gè)sentinel,票數(shù)被設(shè)置為2,當(dāng)2個(gè)sentinel認(rèn)為一個(gè)master已經(jīng)不可用了以后,將會(huì)觸發(fā)failover,但是,進(jìn)行failover的那個(gè)sentinel必須先獲得至少3個(gè)sentinel的授權(quán)才可以實(shí)行failover。如果票數(shù)被設(shè)置為5,要達(dá)到ODOWN狀態(tài),必須所有5個(gè)sentinel都主觀認(rèn)為master為不可用,要進(jìn)行failover,那么得獲得所有5個(gè)sentinel的授權(quán)。
2、Redis Cluster
使用Redis Sentinel 模式架構(gòu)的緩存體系,在使用的過程中,隨著業(yè)務(wù)的增加不可避免的要對(duì)Redis進(jìn)行擴(kuò)容,熟知的擴(kuò)容方式有兩種,一種是垂直擴(kuò)容,一種是水平擴(kuò)容。垂直擴(kuò)容表示通過加內(nèi)存方式來增加整個(gè)緩存體系的容量比如將緩存大小由2G調(diào)整到4G,這種擴(kuò)容不需要應(yīng)用程序支持;水平擴(kuò)容表示表示通過增加節(jié)點(diǎn)的方式來增加整個(gè)緩存體系的容量比如本來有1個(gè)節(jié)點(diǎn)變成2個(gè)節(jié)點(diǎn),這種擴(kuò)容方式需要應(yīng)用程序支持。垂直擴(kuò)容看似最便捷的擴(kuò)容,但是受到機(jī)器的限制,一個(gè)機(jī)器的內(nèi)存是有限的,所以垂直擴(kuò)容到一定階段不可避免的要進(jìn)行水平擴(kuò)容,如果預(yù)留出很多節(jié)點(diǎn)感覺又是對(duì)資源的一種浪費(fèi)因?yàn)閷?duì)業(yè)務(wù)的發(fā)展趨勢(shì)很快預(yù)測(cè)。Redis Sentinel 水平擴(kuò)容一直都是程序猿心中的痛點(diǎn),因?yàn)樗綌U(kuò)容牽涉到數(shù)據(jù)的遷移。遷移過程一方面要保證自己的業(yè)務(wù)是可用的,一方面要保證盡量不丟失數(shù)據(jù)所以數(shù)據(jù)能不遷移就盡量不遷移。針對(duì)這個(gè)問題,Redis Cluster就應(yīng)運(yùn)而生了,下面簡單介紹一下RedisCluster。
Redis Cluster是Redis的分布式解決方案,在Redis 3.0版本正式推出的,有效解決了Redis分布式方面的需求。當(dāng)遇到單機(jī)內(nèi)存、并發(fā)、流量等瓶頸時(shí),可以采用Cluster架構(gòu)達(dá)到負(fù)載均衡的目的。分布式集群首要解決把整個(gè)數(shù)據(jù)集按照分區(qū)規(guī)則映射到多個(gè)節(jié)點(diǎn)的問題,即把數(shù)據(jù)集劃分到多個(gè)節(jié)點(diǎn)上,每個(gè)節(jié)點(diǎn)負(fù)責(zé)整個(gè)數(shù)據(jù)的一個(gè)子集。Redis Cluster采用哈希分區(qū)規(guī)則中的虛擬槽分區(qū)。虛擬槽分區(qū)巧妙地使用了哈??臻g,使用分散度良好的哈希函數(shù)把所有的數(shù)據(jù)映射到一個(gè)固定范圍內(nèi)的整數(shù)集合,整數(shù)定義為槽(slot)。Redis Cluster槽的范圍是0 ~ 16383。槽是集群內(nèi)數(shù)據(jù)管理和遷移的基本單位。采用大范圍的槽的主要目的是為了方便數(shù)據(jù)的拆分和集群的擴(kuò)展,每個(gè)節(jié)點(diǎn)負(fù)責(zé)一定數(shù)量的槽。Redis Cluster采用虛擬槽分區(qū),所有的鍵根據(jù)哈希函數(shù)映射到0 ~ 16383,計(jì)算公式:slot = CRC16(key)16383。每一個(gè)實(shí)節(jié)點(diǎn)負(fù)責(zé)維護(hù)一部分槽以及槽所映射的鍵值數(shù)據(jù)。下圖展現(xiàn)一個(gè)五個(gè)節(jié)點(diǎn)構(gòu)成的集群,每個(gè)節(jié)點(diǎn)平均大約負(fù)責(zé)3276
個(gè)槽,以及通過計(jì)算公式映射到對(duì)應(yīng)節(jié)點(diǎn)的對(duì)應(yīng)槽的過程。

Redis Cluster節(jié)點(diǎn)相互之前的關(guān)系如下圖所示:

三、Redis Sentinel 及Redis Cluster 實(shí)踐
Redis Sentinel 與Redis Cluster 使用需要引入如下jar包
dependency>
groupId>redis.clients/groupId>
artifactId>jedis/artifactId>
version>2.9.0/version>
/dependency>
dependency>
groupId>org.apache.commons/groupId>
artifactId>commons-pool2/artifactId>
version>2.5.0/version>
/dependency>
1、Redis Sentinel 使用
package com.knowledge.cache.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.exceptions.JedisConnectionException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author hzwangjunqiang1@corp.netease.com
* @desc redis sentinel 使用
*/
public class RedisSentinelClient {
private static JedisSentinelPool pool = null;
private static String redisHosts = "127.0.0.1:26378;127.0.0.1:26379;127.0.0.1:26380";
private static String redisMaster = "";//master name
private static String password = "";//密碼,可選
private static final int MAX_IDLE = 200;//最大空閑數(shù)
private static final int MAX_TOTAL = 400;//最大連接數(shù)
private static final int MIN_IDLE = 200;//最小空閑數(shù)
static {
//redis 連接池配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxIdle(MAX_IDLE);
poolConfig.setMaxTotal(MAX_TOTAL);
poolConfig.setMinIdle(MIN_IDLE);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
SetString> hosts = new HashSetString>(Arrays.asList(redisHosts.split(";")));
if (StringUtils.isBlank(password)) {
pool = new JedisSentinelPool(redisMaster, hosts, poolConfig);
} else {
pool = new JedisSentinelPool(redisMaster, hosts, poolConfig, password);
}
}
public String get(String key) throws JedisConnectionException {
Jedis jedis = pool.getResource();
try {
return jedis.get(key);
} catch (JedisConnectionException e) {
throw e;
} finally {
jedis.close();
}
}
}
2、Redis Cluster 使用
package com.knowledge.cache.redis;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.exceptions.JedisConnectionException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* @author hzwangjunqiang1@corp.netease.com
* @desc Redis cluster 使用
*/
public class RedisClusterClient {
private static JedisCluster jedisCluster = null;
private static String redisHosts = "127.0.0.1:6378;127.0.0.1:6379;127.0.0.1:6380"; //如:127.0.0.1:26379;127.0.0.1:26378
private static String password = "";//密碼,可選
private static final int CONNECT_TIMEOUT = 1000;//連接超時(shí)時(shí)間
private static final int SO_TIMEOUT = 1000;//響應(yīng)超時(shí)時(shí)間
private static final int MAX_ATTEMPTS = 5;//最大嘗試次數(shù)
private static final int MAX_IDLE = 200;//最大空閑數(shù)
private static final int MAX_TOTAL = 400;//最大連接數(shù)
private static final int MIN_IDLE = 200;//最小空閑數(shù)
static {
//連接池配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxIdle(MAX_IDLE);
poolConfig.setMaxTotal(MAX_TOTAL);
poolConfig.setMinIdle(MIN_IDLE);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
//Redis Cluster 初始化
SetString> hosts = new HashSetString>(Arrays.asList(redisHosts.split(";")));
SetHostAndPort> nodes = new LinkedHashSetHostAndPort>();
for (String host : hosts) {
HostAndPort hostAndPort = new HostAndPort(host.split(":")[0], Integer.parseInt(host.split(":")[1]));
nodes.add(hostAndPort);
}
if (StringUtils.isBlank(password)) {
jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, poolConfig);
} else {
jedisCluster = new JedisCluster(nodes, CONNECT_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, password, poolConfig);
}
}
/**
* @param key
* @return
* @throws JedisConnectionException
*/
public String get(String key) throws JedisConnectionException {
try {
return jedisCluster.get(key);
} catch (JedisConnectionException e) {
throw e;
}
}
/**
* @param key
* @param value
* @return
* @throws JedisConnectionException
*/
public String set(String key, String value) throws JedisConnectionException {
try {
return jedisCluster.set(key, value);
} catch (JedisConnectionException e) {
throw e;
}
}
}
以上介紹了Redis Sentinel 及 Redis Cluster的初始化過程及簡單的使用,其他比較復(fù)雜的應(yīng)用可以參考Redis 的官方API
四、Redis的過期淘汰策略
1、定時(shí)刪除
- 含義:在設(shè)置key的過期時(shí)間的同時(shí),為該key創(chuàng)建一個(gè)定時(shí)器,讓定時(shí)器在key的過期時(shí)間來臨時(shí),對(duì)key進(jìn)行刪除
- 優(yōu)點(diǎn):保證內(nèi)存被盡快釋放
- 缺點(diǎn):1)若過期key很多,刪除這些key會(huì)占用很多的CPU時(shí)間,在CPU時(shí)間緊張的情況下,CPU不能把所有的時(shí)間用來做要緊的事兒,還需要去花時(shí)間刪除這些key;2)定時(shí)器的創(chuàng)建耗時(shí),若為每一個(gè)設(shè)置過期時(shí)間的key創(chuàng)建一個(gè)定時(shí)器(將會(huì)有大量的定時(shí)器產(chǎn)生),性能影響嚴(yán)重
2、懶漢式刪除
- 含義:key過期的時(shí)候不刪除,每次通過key獲取值的時(shí)候去檢查是否過期,若過期,則刪除,返回null。
- 優(yōu)點(diǎn):刪除操作只發(fā)生在通過key取值的時(shí)候發(fā)生,而且只刪除當(dāng)前key,所以對(duì)CPU時(shí)間的占用是比較少的,而且此時(shí)的刪除是已經(jīng)到了非做不可的地步(如果此時(shí)還不刪除的話,我們就會(huì)獲取到了已經(jīng)過期的key了
- 缺點(diǎn):若大量的key在超出超時(shí)時(shí)間后,很久一段時(shí)間內(nèi),都沒有被獲取過,那么可能發(fā)生內(nèi)存泄露(無用的垃圾占用了大量的內(nèi)存)
3、定期刪除
含義:每隔一段時(shí)間執(zhí)行一次刪除過期key操作
優(yōu)點(diǎn):
1)通過限制刪除操作的時(shí)長和頻率,來減少刪除操作對(duì)CPU時(shí)間的占用--處理"定時(shí)刪除"的缺點(diǎn);
2)定期刪除過期key--處理"懶漢式刪除"的缺點(diǎn)
缺點(diǎn):
1)在內(nèi)存友好方面,不如"定時(shí)刪除"(會(huì)造成一定的內(nèi)存占用,但是沒有懶漢式那么占用內(nèi)存);
2)在CPU時(shí)間友好方面,不如"懶漢式刪除"(會(huì)定期的去進(jìn)行比較和刪除操作,cpu方面不如懶漢式,但是比定時(shí)好)
難點(diǎn):
1)合理設(shè)置刪除操作的執(zhí)行時(shí)長(每次刪除執(zhí)行多長時(shí)間)和執(zhí)行頻率(每隔多長時(shí)間做一次刪除)(這個(gè)要根據(jù)服務(wù)器運(yùn)行情況來定了),每次執(zhí)行時(shí)間太長,或者執(zhí)行頻率太高對(duì)cpu都是一種壓力;
2) 每次進(jìn)行定期刪除操作執(zhí)行之后,需要記錄遍歷循環(huán)到了哪個(gè)標(biāo)志位,以便下一次定期時(shí)間來時(shí),從上次位置開始進(jìn)行循環(huán)遍歷
memcached只是用了惰性刪除,而redis同時(shí)使用了惰性刪除與定期刪除,這也是二者的一個(gè)不同點(diǎn)(可以看做是redis優(yōu)于memcached的一點(diǎn));
五、Redis 使用過程中踩過的坑
1、在生產(chǎn)環(huán)境中一定要配置GenericObjectPoolConfig中的 maxIdle、maxTotal、minIdle.因?yàn)槔锩婺J(rèn)值太低了,如果生產(chǎn)環(huán)境中流量比較大的話,就會(huì)出現(xiàn)等待redis的連接的情況。
2、使用Redis Sentinel 一定要在最后執(zhí)行jedis.close方法來釋放資源,這個(gè)close方法是表示將正常的連接放回去連接池中,將不正常的連接給關(guān)閉。之前jedis低版本中都是調(diào)用returnResource方法來釋放資源,如果連接不正常了會(huì)被重復(fù)使用,這時(shí)會(huì)出現(xiàn)很詭異的異常。所以建議使用比較高版本的jedis
3、為了更好的使用redis 連接池,建議采用 JedisPoolConfig來替代GenericObjectPoolConfig。JedisPoolConfig里面有一些默認(rèn)的參數(shù)
4、maxIdle,maxTotal 最佳實(shí)踐為 maxIdle = maxTotal
到此這篇關(guān)于淺析Redis Sentinel 與 Redis Cluster的文章就介紹到這了,更多相關(guān)Redis Sentinel與Redis Cluster內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- Sentinel Dashboard限流規(guī)則保存方式
- Java之springcloud Sentinel案例講解
- Spring Cloud Alibaba之Sentinel實(shí)現(xiàn)熔斷限流功能
- Sentinel熔斷規(guī)則原理示例詳解分析