redis常用主从读写分离模式,为了保证redis服务的高可用性,这时候可以通过哨兵服务来监控redis服务的状态,哨兵通过某种方式来确定redis当前Master节点是否可用,若当前节点不可用,则通过一定的规则在从服务器中选取一个节点提升为Master。
哨兵负责监控所有的服务节点,当节点不可用时,将节点剔除,等节点恢复可用性后,再将节点添加到从节点。
假设初始时,有server1、2、3三个服务器,server1为master,2、3为slave,然后sentinel负责监控:
然后sentinel检查到server1挂掉以后,将server1剔除,选取server2作为master:
后来sentinel发现server1又恢复正常了,就把server1添加到集群中,但是此时server1只能作为slave而不能直接回到老大位置了:
这就是哨兵的大致原理,具体哨兵如何判断服务是否可用,因为哨兵实际应该是分布式系统,所以可以采用各个哨兵服务进行投票的方式来选取,具体选取那个slave作为master也可以采用一定的算法。
这里只是实现一个demo,判断服务可用只是简单的ping一下redis,选取slave提升为master也是直接选取一个可用节点进行提权。
思路如下:维护master节点值,维护从节点列表和崩溃节点列表,然后开启定时任务,对每个节点进行扫描,然后按上述规则进行相应的状态变更。采用的redis连接池为lettuce5
代码实现如下:
public class MyRedisSentinel {
//节点存储格式 host:port | host:port:password ---> 127.0.0.1:6379 | 127.0.0.1:6379:12345678
//主节点
private volatile String master;
//从节点
private Vector<String> slaveRedisServers = new Vector<>();
//崩溃节点
private Vector<String> badRedisServers = new Vector<>();
public MyRedisSentinel(String[] addresses) {
master = addresses[0];
slaveRedisServers.addAll(Arrays.asList(addresses).subList(1, addresses.length));
//初始化从节点,使其slaveof 主节点
initSlaves();
//开启定时任务 定时检查主节点 从节点 崩溃节点的状态
new ScheduledThreadPoolExecutor(1).scheduleAtFixedRate(
() -> {
updateBadServers();
updateSlaveServers();
updateMaster();
}, 0, 2000, TimeUnit.MILLISECONDS);
}
/**
* 获取当前可用主节点连接
*
* @return StatefulRedisConnection
*/
public StatefulRedisConnection<String, String> getNowMaster() {
try {
return getClient(master.split(":"));
}catch (Exception e){
try {
Thread.sleep(2000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
return getNowMaster();
}
/**
* 根据uri获取连接
*
* @param params uri(host:port | host:port:password)
* @return StatefulRedisConnection
*/
private StatefulRedisConnection<String, String> getClient(String[] params) {
RedisURI redisURI = RedisURI.Builder.redis(params[0], Integer.parseInt(params[1])).build();
RedisClient redisClient = RedisClient.create(redisURI);
StatefulRedisConnection<String, String> statefulRedisConnection = redisClient.connect();
if (params.length > 2) {
statefulRedisConnection.sync().auth(params[2]);
}
return statefulRedisConnection;
}
/**
* 初始化从节点 使其slaveof 主节点
* 如果从节点崩溃 将其加入到崩溃节点
*/
private void initSlaves() {
String[] masterParams = master.split(":");
for (String address : slaveRedisServers) {
String[] params = address.split(":");
try (StatefulRedisConnection client = getClient(params)) {
client.sync().slaveof(masterParams[0], Integer.parseInt(masterParams[1]));
}
}
}
/**
* 检查主节点状态 如果主节点崩溃 从从节点中选取一个可用节点当主节点
*/
private void updateMaster() {
if (!checkNode(master)) {
while (!checkNode(master)) {
badRedisServers.add(master);
master = slaveRedisServers.firstElement();
slaveRedisServers.remove(master);
}
System.out.println("--------切换master为 : " + master);
try(StatefulRedisConnection connection = getClient(master.split(":"))) {
connection.sync().slaveofNoOne();
initSlaves();
}
}
System.out.println("当前主节点:");
System.out.println(master);
}
/**
* 检查崩溃节点的状态 如果恢复正常 加入到从节点
*/
private void updateBadServers() {
Iterator<String> iterable = badRedisServers.iterator();
String[] masterParams = master.split(":");
while (iterable.hasNext()) {
String uri = iterable.next();
if (checkNode(uri)) {
try(StatefulRedisConnection<String, String> connection = getClient(uri.split(":"));) {
connection .sync().slaveof(masterParams[0], Integer.parseInt(masterParams[1]));
slaveRedisServers.add(uri);
iterable.remove();
System.out.println("------节点 : " + uri + " 恢复正常");
}catch (Exception e){
e.printStackTrace();
}
}
}
System.out.println("当前崩溃节点:");
badRedisServers.forEach(System.out::println);
}
/**
* 检查从节点状态 如果从节点崩溃 剔除并添加崩溃节点
*/
private void updateSlaveServers() {
Iterator<String> iterator = slaveRedisServers.iterator();
while (iterator.hasNext()) {
String uri = iterator.next();
if (!checkNode(uri)) {
badRedisServers.add(uri);
iterator.remove();
System.out.println("------节点 : " + uri + " 节点崩溃");
}
}
System.out.println("当前从节点:");
slaveRedisServers.forEach(System.out::println);
}
/**
* 检查节点是否正常
*
* @param uri host:port | host:port:password
* @return boolean
*/
private boolean checkNode(String uri) {
String[] params = uri.split(":");
boolean rs = false;
try(StatefulRedisConnection client = getClient(params)) {
client.sync().ping();
rs = true;
} catch (Exception e) {
System.err.println("节点 " + uri + " 崩溃了");
}
return rs;
}
}
测试:
启动redis主从服务,启动方法参考redis主从服务配置
启动redis服务后,运行以下测试代码:
@Test
public void testRedisSentinel(){
MyRedisSentinel myRedisSentinel = new MyRedisSentinel(new String[]{"127.0.0.1:6379:123456",
"127.0.0.1:6380:123456","127.0.0.1:6381:123456","127.0.0.1:6382:123456"
});
int count = 1;
while (count < 10000){
StatefulRedisConnection<String, String> connection = myRedisSentinel.getNowMaster();
connection.sync().set("key" + count, "" + count);
connection.close();
count++;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后关闭master节点服务,查看redis状态,
再开启关闭的节点服务,查看redis状态。