業務最初的應用場景中,我們也許使用單機redis就可以應付業務要求,但并非一直可行。
比如單機的讀寫能力問題,單機的可用性問題,單機的資料安全性問題。這些都是許多網際網路應用經常會遇到的問題,也基本上都有一套理論去解決它,隻是百花齊放。
哨兵是Redis中解決高可用問題的解決方案之一,我們就一起來看看 Redis是如何實作的吧!不過此方案,僅提供思路供參考,不要以此為标準方案。
前面介紹的主從複制功能,可以說已經一定程度上解決了資料安全性問題問題,即有了備份資料,我們可以可以做讀寫分離了。隻是,可用性問題還未解決,即當 master 當機或出現其他故障時,整個寫服務就不可用了。解決方法是,手動操作,要麼重新開機master使其恢複服務,要麼把master切換為其他slave機器。
如果服務的可用性需要人工介入的話,那就算不得高可用了,是以我們需要一個自動處理機制。這就是哨兵模式。
一、哨兵系統介紹
哨兵系統要解決的問題核心,自然是高可用問題。而如何解決,則是其設計問題。而最終呈現給使用者的,應該一個個的功能單元,即其提供的能力。如下:
監控(Monitoring): Sentinel 會不斷地檢查你的主伺服器和從伺服器是否運作正常。
提醒(Notification): 當被監控的某個 Redis 伺服器出現問題時, Sentinel 可以通過 API 向管理者或者其他應用程式發送通知。
自動故障遷移(Automatic failover): 當一個主伺服器不能正常工作時, Sentinel 會開始一次自動故障遷移操作, 它會将失效主伺服器的其中一個從伺服器更新為新的主伺服器, 并讓失效主伺服器的其他從伺服器改為複制新的主伺服器;
配置提供者: Sentinel充當用戶端服務發現的授權來源:用戶端連接配接到Sentinels,以詢問負責給定服務的目前Redis主伺服器的位址。 如果發生故障轉移,Sentinels将報告新位址。(這也是用戶端接入入口)
哨兵系統的架構圖如下:
(一)服務端架構

(二)請求處理流程圖
二、哨兵系統搭建步驟
哨兵可以搭建在 redis服務所在機器,也可以在單獨的機器執行個體上搭建。
1. 有多個在運作的 redis master/slave 執行個體;
主從服務的搭建,slaveof 設定,請參照主從配置篇。
2. 編寫哨兵配置檔案;
# Example sentinel.conf
# 定義sentinel 服務端口号
port 26379
# 針對 使用端口映射的方式的啟動,指定ip:port
# sentinel announce-ip <ip>
# sentinel announce-port <port>
# 工作目錄定義
dir /tmp
# 要監視的redis master 定義, 可配置多個 master-name 不同即可
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 定義master/slave 的密碼,要求同一主從服務所有密碼必須保持一緻
# sentinel auth-pass <master-name> <password>
# 定義master 不可達持續多少毫秒後開始定義為節點下線,預設30s
sentinel down-after-milliseconds mymaster 30000
# sentinel parallel-syncs <master-name> <numslaves>
# 在故障轉移期間同時與新的master同步的slave數量
sentinel parallel-syncs mymaster 1
# 定義進行故障轉移的逾時時間,預設3分鐘
sentinel failover-timeout mymaster 180000
# 發生故障轉移時調用的通知腳本,被調用時會傳遞兩個參數: eventType, eventDescription
# sentinel notification-script mymaster /var/redis/notify.sh
# master 變更時調用腳本配置
# 調用時會傳遞如下參數
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
3. 啟動哨兵節點;
# 使用 redis-sentinel 程式啟動, 這個程式不一定會有,需要自己編譯
redis-sentinel /path/to/sentinel.conf
# 使用 redis-server 程式啟動, 一定可用
# 測試時可以加上 --protected-mode no, 在不設定密碼情況下通路redis
redis-server /path/to/sentinel.conf --sentinel
4. 驗證哨兵運作情況
通過redis-cli 連接配接到sentinel 服務内部:
redis-cli -p 26379 # 連接配接到sentinel
info sentinel # 檢視哨兵資訊
SENTINEL slaves mymaster # 檢視master下的slave伺服器情況
SENTINEL sentinels mymaster # 檢視master的哨兵伺服器清單
SENTINEL get-master-addr-by-name mymaster # 擷取master位址資訊
5. 故障模拟
将master節點關閉後,等待一段時間,再擷取master位址看看。master已經切換了。
SENTINEL get-master-addr-by-name mymaster # 擷取master位址資訊
三、哨兵實作高可用的運作原理
1. Sentinel 的定時任務
- 每個 Sentinel 以每秒鐘一次的頻率向它所知的主伺服器、從伺服器以及其他 Sentinel 執行個體發送一個 PING 指令。
- 如果一個執行個體(instance)距離最後一次有效回複 PING 指令的時間超過 down-after-milliseconds 選項所指定的值, 那麼這個執行個體會被 Sentinel 标記為主觀下線。 一個有效回複可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
- 如果一個主伺服器被标記為主觀下線, 那麼正在監視這個主伺服器的所有 Sentinel 要以每秒一次的頻率确認主伺服器的确進入了主觀下線狀态。
- 如果一個主伺服器被标記為主觀下線, 并且有足夠數量的 Sentinel (至少要達到配置檔案指定的數量)在指定的時間範圍内同意這一判斷, 那麼這個主伺服器被标記為客觀下線。
- 在一般情況下, 每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有主伺服器和從伺服器發送 INFO 指令。 當一個主伺服器被 Sentinel 标記為客觀下線時, Sentinel 向下線主伺服器的所有從伺服器發送 INFO 指令的頻率會從 10 秒一次改為每秒一次。
- 當沒有足夠數量的 Sentinel 同意主伺服器已經下線, 主伺服器的客觀下線狀态就會被移除。 當主伺服器重新向 Sentinel 的 PING 指令傳回有效回複時, 主伺服器的主觀下線狀态就會被移除。
2. 自動發現 Sentinel 和從伺服器
一個 Sentinel 可以與其他多個 Sentinel 進行連接配接, 各個 Sentinel 之間可以互相檢查對方的可用性, 并進行資訊交換。
Sentinel 可以通過釋出與訂閱功能來自動發現正在監視相同主伺服器的其他 Sentinel , 這一功能是通過向pub/sub頻道 sentinel:hello 發送資訊來實作的。
Sentinel 可以通過詢問主伺服器來獲得所有從伺服器的資訊。
- 每個 Sentinel 會以每兩秒一次的頻率, 通過釋出與訂閱功能, 向被它監視的所有主伺服器和從伺服器的 sentinel:hello 頻道發送一條資訊, 資訊中包含了 Sentinel 的 IP 位址、端口号和運作 ID (runid)。
- 每個 Sentinel 都訂閱了被它監視的所有主伺服器和從伺服器的 sentinel:hello 頻道, 查找之前未出現過的 sentinel (looking for unknown sentinels)。 當一個 Sentinel 發現一個新的 Sentinel 時, 它會将新的 Sentinel 添加到一個清單中, 這個清單儲存了 Sentinel 已知的, 監視同一個主伺服器的所有其他 Sentinel 。
- Sentinel 發送的資訊中還包括完整的主伺服器目前配置(configuration)。 如果一個 Sentinel 包含的主伺服器配置比另一個 Sentinel 發送的配置要舊, 那麼這個 Sentinel 會立即更新到新配置上。
- 在将一個新 Sentinel 添加到監視主伺服器的清單上面之前, Sentinel 會先檢查清單中是否已經包含了和要添加的 Sentinel 擁有相同運作 ID 或者相同位址(包括 IP 位址和端口号)的 Sentinel , 如果是的話, Sentinel 會先移除清單中已有的那些擁有相同運作 ID 或者相同位址的 Sentinel , 然後再添加新 Sentinel 。
3. 故障轉移
一次故障轉移操作由以下步驟組成:
- 發現主伺服器已經進入客觀下線狀态。
- 對我們的目前紀元進行自增(詳情請參考 Raft leader election ), 并嘗試在這個紀元中當選。
- 如果當選失敗, 那麼在設定的故障遷移逾時時間的兩倍之後, 重新嘗試當選。 如果當選成功, 那麼執行以下步驟。
- 選出一個從伺服器,并将它更新為主伺服器。
- 向被選中的從伺服器發送
指令,讓它轉變為主伺服器。SLAVEOF NO ONE
- 通過釋出與訂閱功能, 将更新後的配置傳播給所有其他 Sentinel , 其他 Sentinel 對它們自己的配置進行更新。
- 向已下線主伺服器的從伺服器發送 SLAVEOF 指令, 讓它們去複制新的主伺服器。
- 當所有從伺服器都已經開始複制新的主伺服器時, 領頭 Sentinel 終止這次故障遷移操作。
每當一個 Redis 執行個體被重新配置(reconfigured) —— 無論是被設定成主伺服器、從伺服器、又或者被設定成其他主伺服器的從伺服器 —— Sentinel 都會向被重新配置的執行個體發送一個 CONFIG REWRITE 指令, 進而確定這些配置會持久化在硬碟裡。
Sentinel 使用以下規則來選擇新的主伺服器:
- 在失效主伺服器屬下的從伺服器當中, 那些被标記為主觀下線、已斷線、或者最後一次回複 PING 指令的時間大于五秒鐘的從伺服器都會被淘汰。
- 在失效主伺服器屬下的從伺服器當中, 那些與失效主伺服器連接配接斷開的時長超過 down-after 選項指定的時長十倍的從伺服器都會被淘汰。
- 在經曆了以上兩輪淘汰之後剩下來的從伺服器中, 我們選出複制偏移量(replication offset)最大的那個從伺服器作為新的主伺服器; 如果複制偏移量不可用, 或者從伺服器的複制偏移量相同, 那麼帶有最小運作 ID 的那個從伺服器成為新的主伺服器。
四、用戶端使用哨兵系統
哨兵系統搭建好之後,就可以提供服務了。那麼,如何提供服務呢?從最前面的兩張架構圖中,我們可以看到,sentinel 差不多是作為一個配置中心或者存在的,它隻會為用戶端提供master/slave的相關資訊,而并不會直接代替redis執行個體進行存取操作。是以,哨兵模式,需要用戶端做更多的工作,原來的直接連接配接redis變為間接從sentinel擷取資訊,再連接配接,還要維護可能的資訊變更。
當然,這種工作一般是要交給sdk做的,實作原理也差不多,我們就以 jedis 作為切入點,詳解下用戶端如何使用sentinel.
1. 引入pom依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2. 單元測試
public class RedisSentinelTest {
@Test
public void testSentinel() throws Exception {
// 池化基礎資訊配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMaxIdle(5);
jedisPoolConfig.setMinIdle(5);
// 哨兵連接配接資訊配置
String masterName = "mymaster";
Set<String> sentinels = new HashSet<>();
sentinels.add("127.0.0.1:26379");
sentinels.add("127.0.0.1:26378");
sentinels.add("127.0.0.1:26377");
// 在redis需要使用密碼通路時,傳入即可
String password = null;
// 使用 JedisSentinelPool 封裝哨兵的通路細節
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels, jedisPoolConfig, password);
Jedis jedis = pool.getResource();
String key = "key1";
String value = "Value1";
jedis.set(key, value);
System.out.println("set a value to Redis over. " + key + "->" + value);
value = jedis.get("key1");
System.out.println("get a value from Redis over. " + key + "->" + value);
pool.close();
}
}
3. sentinel 處理過程解析
jedis的sdk中已經将哨兵封裝得和普通的redis執行個體請求差不多了,是以,我們需要深入了解下其處理過程。
首先是在初始化 JedisSentinelPool 時,其會與sentinel清單中選擇一個與其建立連接配接:
// redis.clients.jedis.JedisSentinelPool#JedisSentinelPool
public JedisSentinelPool(String masterName, Set<String> sentinels) {
this(masterName, sentinels, new GenericObjectPoolConfig(), Protocol.DEFAULT_TIMEOUT, null,
Protocol.DEFAULT_DATABASE);
}
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, int timeout, final String password,
final int database) {
this(masterName, sentinels, poolConfig, timeout, timeout, password, database);
}
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, final int timeout, final int soTimeout,
final String password, final int database) {
this(masterName, sentinels, poolConfig, timeout, soTimeout, password, database, null);
}
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
final String password, final int database, final String clientName) {
this.poolConfig = poolConfig;
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.password = password;
this.database = database;
this.clientName = clientName;
// 從sentinel中擷取master資訊,關鍵
HostAndPort master = initSentinels(sentinels, masterName);
// 初始化連接配接池,非本文重點
initPool(master);
}
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
// 依次周遊 sentinels, 直到找到一個可用的sentinel
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
jedis = new Jedis(hap.getHost(), hap.getPort());
// 向sentinel發送指令請求: SENTINEL get-master-addr-by-name mymaster, 擷取master位址資訊
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2) {
log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
+ ".");
continue;
}
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
break;
} catch (JedisException e) {
// resolves #1036, it should handle JedisException there's another chance
// of raising JedisDataException
log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
+ ". Trying next one.");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
if (master == null) {
if (sentinelAvailable) {
// can connect to sentinel, but master name seems to not
// monitored
throw new JedisException("Can connect to sentinel, but " + masterName
+ " seems to be not monitored...");
} else {
throw new JedisConnectionException("All sentinels down, cannot determine where is "
+ masterName + " master is running...");
}
}
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
// 為每個 sentinel, 建立一個監聽線程, 監聽 sentinel 的 +switch-master 資訊
// 當master發生變化時,重新初始化連接配接池
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
// 每個 sentinel 監聽線程事務處理流程如下
// redis.clients.jedis.JedisSentinelPool.MasterListener#run
@Override
public void run() {
running.set(true);
while (running.get()) {
j = new Jedis(host, port);
try {
// double check that it is not being shutdown
if (!running.get()) {
break;
}
// SUBSCRIBE +switch-master
j.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
log.fine("Sentinel " + host + ":" + port + " published: " + message + ".");
String[] switchMasterMsg = message.split(" ");
// 格式為: masterName xx xx masterHost masterPort
if (switchMasterMsg.length > 3) {
if (masterName.equals(switchMasterMsg[0])) {
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
log.fine("Ignoring message on +switch-master for master name "
+ switchMasterMsg[0] + ", our master name is " + masterName);
}
} else {
log.severe("Invalid message received on Sentinel " + host + ":" + port
+ " on channel +switch-master: " + message);
}
}
}, "+switch-master");
} catch (JedisConnectionException e) {
if (running.get()) {
log.log(Level.SEVERE, "Lost connection to Sentinel at " + host + ":" + port
+ ". Sleeping 5000ms and retrying.", e);
try {
Thread.sleep(subscribeRetryWaitTimeMillis);
} catch (InterruptedException e1) {
log.log(Level.SEVERE, "Sleep interrupted: ", e1);
}
} else {
log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
}
} finally {
j.close();
}
}
}
從上面流程我們也就可以看出用戶端是如何處理 sentinel 和 redis 的關系的了。簡單來說就是通過 sentinel get-master-addr-by-name xxx, 擷取master位址資訊,然後連接配接過去就可以了。在master發生變化時,通過pub/sub訂閱sentinel資訊,進而進行連接配接池的重置。
這個連接配接池又是如何處理的呢?我們可以簡單看一下:
// redis.clients.jedis.JedisSentinelPool#initPool
private void initPool(HostAndPort master) {
if (!master.equals(currentHostMaster)) {
currentHostMaster = master;
if (factory == null) {
factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
soTimeout, password, database, clientName, false, null, null, null);
initPool(poolConfig, factory);
} else {
factory.setHostAndPort(currentHostMaster);
// although we clear the pool, we still have to check the
// returned object
// in getResource, this call only clears idle instances, not
// borrowed instances
internalPool.clear();
}
log.info("Created JedisPool to master at " + master);
}
}
// redis.clients.util.Pool#initPool
public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
if (this.internalPool != null) {
try {
closeInternalPool();
} catch (Exception e) {
}
}
this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
}
當要向redis寫入資料時,會先從連接配接池裡擷取一個連接配接執行個體,其池化架構使用的是 GenericObjectPool 的通用能力,調用 JedisFactory 的 makeObject() 方法進行建立 :
// redis.clients.jedis.JedisSentinelPool#getResource
@Override
public Jedis getResource() {
while (true) {
// 調用父類方法擷取執行個體
Jedis jedis = super.getResource();
jedis.setDataSource(this);
// get a reference because it can change concurrently
final HostAndPort master = currentHostMaster;
final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient()
.getPort());
// host:port 比對,如果master未變化,說明擷取到了正确的連接配接,傳回
if (master.equals(connection)) {
// connected to the correct master
return jedis;
}
// 如果master 發生了切換,則将目前連接配接釋放,繼續嘗試擷取master連接配接
else {
returnBrokenResource(jedis);
}
}
}
// redis.clients.util.Pool#getResource
public T getResource() {
try {
return internalPool.borrowObject();
} catch (NoSuchElementException nse) {
throw new JedisException("Could not get a resource from the pool", nse);
} catch (Exception e) {
throw new JedisConnectionException("Could not get a resource from the pool", e);
}
}
// org.apache.commons.pool2.impl.GenericObjectPool#borrowObject()
@Override
public T borrowObject() throws Exception {
return borrowObject(getMaxWaitMillis());
}
// org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long)
public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
assertOpen();
final AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
PooledObject<T> p = null;
// Get local copy of current config so it is consistent for entire
// method execution
final boolean blockWhenExhausted = getBlockWhenExhausted();
boolean create;
final long waitTime = System.currentTimeMillis();
while (p == null) {
create = false;
p = idleObjects.pollFirst();
if (p == null) {
// 沒有擷取到連接配接時,主動建立一個
p = create();
if (p != null) {
create = true;
}
}
if (blockWhenExhausted) {
if (p == null) {
if (borrowMaxWaitMillis < 0) {
p = idleObjects.takeFirst();
} else {
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
}
}
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
} else {
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
}
if (!p.allocate()) {
p = null;
}
if (p != null) {
try {
// 確定激活目前資料庫
factory.activateObject(p);
} catch (final Exception e) {
try {
destroy(p);
} catch (final Exception e1) {
// Ignore - activation failure is more important
}
p = null;
if (create) {
final NoSuchElementException nsee = new NoSuchElementException(
"Unable to activate object");
nsee.initCause(e);
throw nsee;
}
}
if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (final Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
try {
destroy(p);
destroyedByBorrowValidationCount.incrementAndGet();
} catch (final Exception e) {
// Ignore - validation failure is more important
}
p = null;
if (create) {
final NoSuchElementException nsee = new NoSuchElementException(
"Unable to validate object");
nsee.initCause(validationThrowable);
throw nsee;
}
}
}
}
}
updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
return p.getObject();
}
/**
* Attempts to create a new wrapped pooled object.
* <p>
* If there are {@link #getMaxTotal()} objects already in circulation
* or in process of being created, this method returns null.
*
* @return The new wrapped pooled object
*
* @throws Exception if the object factory's {@code makeObject} fails
*/
private PooledObject<T> create() throws Exception {
int localMaxTotal = getMaxTotal();
// This simplifies the code later in this method
if (localMaxTotal < 0) {
localMaxTotal = Integer.MAX_VALUE;
}
// Flag that indicates if create should:
// - TRUE: call the factory to create an object
// - FALSE: return null
// - null: loop and re-test the condition that determines whether to
// call the factory
Boolean create = null;
while (create == null) {
synchronized (makeObjectCountLock) {
final long newCreateCount = createCount.incrementAndGet();
if (newCreateCount > localMaxTotal) {
// The pool is currently at capacity or in the process of
// making enough new objects to take it to capacity.
createCount.decrementAndGet();
if (makeObjectCount == 0) {
// There are no makeObject() calls in progress so the
// pool is at capacity. Do not attempt to create a new
// object. Return and wait for an object to be returned
create = Boolean.FALSE;
} else {
// There are makeObject() calls in progress that might
// bring the pool to capacity. Those calls might also
// fail so wait until they complete and then re-test if
// the pool is at capacity or not.
makeObjectCountLock.wait();
}
} else {
// The pool is not at capacity. Create a new object.
makeObjectCount++;
create = Boolean.TRUE;
}
}
}
if (!create.booleanValue()) {
return null;
}
final PooledObject<T> p;
try {
// 調用指定factory的 makeObject() 方法
p = factory.makeObject();
} catch (final Exception e) {
createCount.decrementAndGet();
throw e;
} finally {
synchronized (makeObjectCountLock) {
makeObjectCount--;
makeObjectCountLock.notifyAll();
}
}
final AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getLogAbandoned()) {
p.setLogAbandoned(true);
}
createdCount.incrementAndGet();
allObjects.put(new IdentityWrapper<T>(p.getObject()), p);
return p;
}
// 使用 JedisFactory 建立一個連接配接到 master
// redis.clients.jedis.JedisFactory#makeObject
@Override
public PooledObject<Jedis> makeObject() throws Exception {
final HostAndPort hostAndPort = this.hostAndPort.get();
final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
try {
jedis.connect();
// 如果存在密碼設定,則進行 auth xxx 操作
// redis 配置: requirepass xxx
if (null != this.password) {
jedis.auth(this.password);
}
if (database != 0) {
jedis.select(database);
}
if (clientName != null) {
jedis.clientSetname(clientName);
}
} catch (JedisException je) {
jedis.close();
throw je;
}
return new DefaultPooledObject<Jedis>(jedis);
}
// redis.clients.jedis.JedisFactory#activateObject
@Override
public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
final BinaryJedis jedis = pooledJedis.getObject();
if (jedis.getDB() != database) {
jedis.select(database);
}
}
擷取到client連接配接後,主可以任意地通過網絡io與真實redis進行互動了。哨兵也不會成為性能問題了。
五、幾點思考
哨兵模式的出現,僅為了解決單機的高可用問題,而并不會解決單機容量問題(叢集模式會處理這個問題)。在目前的網際網路環境中,應用面也許沒有那麼廣。但思路是值得借鑒的。
Sentinel 在配置時隻需配置master位址即可,其slave資訊,sentinel資訊,都是通過master來推斷的。是以,一定要確定在啟動時master是可用的,否則系統本身必須無法啟動。看起來是個脆弱的協定。
Sentinel 的動态切換資訊會寫到配置檔案中去,而這個檔案最初又是由管理者寫的,即動态配置與靜态混合在一起。容易讓人混淆,且容易改錯。看起來并不是那麼完美。(也許設計者有其考慮吧)
如果redis中設定了密碼,則要求必須保持全部一緻,這在一定程度上會有些誤會。
redis Sentinel 本質上是一個對等叢集系統,提供服務注冊及選主服務,連接配接任意節點結果都是一樣的,節點間保持通過pub/sub兩兩通信。
redis 本身就是一款高性能和高成本效益的緩存産品。而sentinel為了解決一個高可用問題,帶來的額外支出并不小,這也必然會影響我們的選擇!
市面上有很多做故障檢測和切換的工具,如nginx、keepalived、zookeeper,但都無法做到自動選主功能,因為這是應用相關性強的服務,隻能是應用自身實作。但為什麼不把高可用選主等功能融合到redis的服務中呢?畢竟這種功能的抽離,并沒有太多地複用性。看市面很多産品,高可用都是其自身實作的一個功能,隻需做好必要配置即可,無需其他負擔。redis的哨兵架構倒是特立獨行了。
不要害怕今日的苦,你要相信明天,更苦!