上一篇文章介紹了Redis主從複製實現了redis高可用,但是主從複製存在着一些問題:
1. 當master節點出現故障時,往往需要手動進行故障的轉移(這裏的手動也指寫腳本之類的)
2. 當master節點出現故障時,就導致了另一個問題:寫能力和存儲能力受到限制
本次通過redis-sentinel哨兵實現故障的自動轉移
redis-sentinel故障轉移的流程:
1.當多個sentinel發現並確認了master有問題
2.接着會選舉出一個sentinel作爲領導
3.再選舉出一個slave作爲master
4.通知其餘的slave,新的master是誰
5.通知客戶端一個主從的變化
6.最後,sentinel會等待舊的master復活,然後將新master成爲slave
實操演練:
首先啓動三個redis節點(redis-7000, redis-7001, redis-7002),一主兩從模式,基本配置如下
主節點:redis-7000.conf
port 7000
daemonize yes
pidfile /var/run/redis-7000.pid
logfile "7000.log"
dir "/opt/redis/data"
從節點:redis-7001.conf redis-7002.conf
port 7001(redis-7002.conf是7002)
daemonize yes
pidfile /var/run/redis-7000.pid (redis-7002.conf是7002 )
logfile "7000.log" (redis-7002.conf是7002 )
dir "/opt/redis/data"
slaveof 127.0.0.1 7000
驗證:進入任意一個redis節點,執行info replication命令即可查看當前節點的狀態
可以看出端口7000的節點時master節點
可以看出當前節點時slave從節點,並且可以看出該從節點的主節點地址
其次啓動三個哨兵(redis-26379, redis-26380, redis-26381)
配置(三個配置基本相似, 就端口號的地方改一下)
port 26379
daemonize yes
dir "/opt/redis/data/sentinel"
logfile "26379.log"
sentinel monitor mymaster 127.0.0.1 7000 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
#sentinel auth-pass mymaster luyue
配置解釋:
sentinel monitor mymaster 127.0.0.1 7000 2 : mymaster(監控的主節點的名字) 127.0.0.1(監控的主節點的ip) 7000(監控的主節點的端口號) 2(故障發現,至少要多少個sentinel發現主節點是故障的才進行轉移)
sentinel down-after-milliseconds mymaster 30000: 對master進行判斷,類似不斷ping,若30秒後節點依然不通,則認爲是故障的
sentinel parallel-syncs mymaster 1 : slave對新的master進行復制,若舊master故障, 1代表每次只能多少個slave對master進行復制
sentinel failover-timeout mymaster 180000 : 故障轉移時間
sentinel auth-pass mymaster : 若master節點配置了masterauth,則需要,作用就是安全認證
!!!:redis-sentinel可以監控多套主從配置,只要將mymaster 名字變掉就行
驗證:連接sentinel,隨便一個
輸入info命令,可以看到sentinels=3, 則證明sentinel啓動成功
ps:接下來的主題是java如何連接redis-sentinel,與redis-sentinel基本無關,可跳過
通過jedis連接redis-sentinel
POM:
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.6.0</version>
</dependency>
一:封裝SentinelJedisPool
package com.mmall.common;
import com.google.common.collect.Sets;
import com.mmall.util.PropertiesUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import java.util.Set;
/**
* @author Luyue
* @date 2018/8/11 14:29
**/
public class SentinelJedisPool {
private static JedisSentinelPool pool;
//最大連接數
private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getKey("redis.max.total", "20"));
//最大空閒連接數
private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getKey("redis.max.idle", "10"));
//最小空閒連接數
private static Integer minIdle = Integer.parseInt(PropertiesUtil.getKey("redis.min.idle", "2"));
//通過連接池拿去jedis連接時,校驗並返回可用連接
private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getKey("redis.test.borrow", "true"));
//通過連接池返還jedis連接時,校驗該連接
private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getKey("redis.test.return", "true"));
/**
* 初始化連接池
*/
private static void initPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMinIdle(minIdle);
jedisPoolConfig.setTestOnBorrow(testOnBorrow);
jedisPoolConfig.setTestOnReturn(testOnReturn);
//當連接池無空閒連接時,是否阻塞
jedisPoolConfig.setBlockWhenExhausted(true);
//sentinel節點的地址,格式:ip:port
Set<String> sentinels = Sets.newHashSet("xxx.xxx.xxx.xxx:26379", "xxx.xxx.xxx.xxx:26380", "xxx.xxx.xxx.xxx:26381");
pool = new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig, 2 * 1000, "luyue");
}
static {
initPool();
}
/**
* 獲取一個連接
* @return
*/
public static Jedis getJedis() {
return pool.getResource();
}
/**
* 返還錯誤的連接
* @param jedis
*/
public static void returnBrokenJedis(Jedis jedis) {
pool.returnBrokenResource(jedis);
}
/**
* 返還連接
* @param jedis
*/
public static void returnJedis(Jedis jedis) {
pool.returnResource(jedis);
}
}
二. 封裝API
package com.mmall.util;
import com.mmall.common.SentinelJedisPool;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
/** 單機redis連接池
* @author Luyue
* @date 2018/8/11 14:29
**/
@Slf4j
public class SentinelJedisPoolUtil {
/**
* 存儲鍵值
* @param key
* @param value
* @return
*/
public static String set(String key, String value) {
Jedis jedis = null;
String response = null;
try {
jedis = SentinelJedisPool.getJedis();
response = jedis.set(key, value);
} catch (Exception e) {
log.error("set key:{}, value:{} is error:{}", key, value, e);
SentinelJedisPool.returnBrokenJedis(jedis);
return response;
}
SentinelJedisPool.returnJedis(jedis);
return response;
}
/**
* 存儲鍵值,並設置有效時間
* @param key
* @param value
* @param exTime
* @return
*/
public static String setEx(String key, String value, int exTime) {
Jedis jedis = null;
String response = null;
try {
jedis = SentinelJedisPool.getJedis();
response = jedis.setex(key, exTime, value);
} catch (Exception e) {
log.error("setEx key:{}, value:{} is error:{}", key, value, e);
SentinelJedisPool.returnBrokenJedis(jedis);
return response;
}
SentinelJedisPool.returnJedis(jedis);
return response;
}
/**
* 獲取value
* @param key
* @return
*/
public static String get(String key) {
Jedis jedis = null;
String response = null;
try {
jedis = SentinelJedisPool.getJedis();
response = jedis.get(key);
} catch (Exception e) {
log.error("get key:{}, is error:{}", key, e);
SentinelJedisPool.returnBrokenJedis(jedis);
return response;
}
SentinelJedisPool.returnJedis(jedis);
return response;
}
/**
* 設置鍵的有效時間
* @param key
* @param exTime
* @return
*/
public static Long expire(String key, int exTime) {
Jedis jedis = null;
Long response = null;
try {
jedis = SentinelJedisPool.getJedis();
response = jedis.expire(key, exTime);
} catch (Exception e) {
log.error("expire key:{}, is error:{}", key, e);
SentinelJedisPool.returnBrokenJedis(jedis);
return response;
}
SentinelJedisPool.returnJedis(jedis);
return response;
}
/**
* 刪除鍵
* @param key
* @return
*/
public static Long del(String key) {
Jedis jedis = null;
Long response = null;
try {
jedis = SentinelJedisPool.getJedis();
response = jedis.del(key);
} catch (Exception e) {
log.error("del key:{}, is error:{}", key, e);
SentinelJedisPool.returnBrokenJedis(jedis);
return response;
}
SentinelJedisPool.returnJedis(jedis);
return response;
}
}
三. 開始測試
在測試之前,有一個點,非常非常重要,因爲我們上述配置沒有設置密碼,所以redis會默認採取保護模式,這就導致了客戶端沒有權限獲取連接,報DENIED Redis is running in protected mode錯誤.
兩種辦法:
1. 設置密碼,requirepass和masterauth
2. 關閉保護模式: protected-mode no(redis-7000.conf和sentinel.conf都加上)
一般我兩種辦法都加上,一種可能也會報錯, 在然後將bind 127.0.0.1 配置給註釋掉, 最後sentinel配置文件中的127.0.0.1必須填主機的線上地址
redis-7000.conf:
redis-sentinel-26379.conf(其他兩個哨兵配置除了端口,其他一樣)
ok,所有redis節點,sentinel節點開啓
接着寫java測試代碼:
運行結果:
切換到debug模式,查看jedis是否真的獲取到了
完全ok,結束!