上一篇文章介绍了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:
redis.clients
jedis
2.6.0
一:封装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 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,结束!