Java Redis哨兵
我的架构师之路 人气:0前言:
本文将采用文字+代码的方式,讲解redis版哨兵的实现,所有代码都将写在一个类中,每个属性和方法都会结合文字加以说明。
1. 哨兵(Sentinel)主要功能如下:
1、不时的监控redis节点是否良好运行,如果节点不可达就会对节点进行下线标识
2、如果被标识的是主节点,哨兵就会选举一个redis从(slave)节点成为新的主节点继续对外提供读写服务, 进而实现自动故障转移,保证系统的高可用。
3、在redis主节点 和 从节点 进行切换后,主节点配置文件master_redis.conf、从节点配置文件slave_redis.conf都要发生改变。
2. 准备工作:
- Redis集群推荐一主两从,共三个节点。
- jedis-2.9.0.jar 客户端框架
3. 代码实现
JavaSentinel.java
package com.middleware.redis.sentinels; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.*; /** * java版哨兵 * * @author 93733 * */ public class JavaSentinel { // 主节点ip:端口 127.0.0.1:6379 static String masterAddress = "127.0.0.1:6379"; // 所有 slave static final Vector<String> slaveRedisServers = new Vector<String>(); // 坏掉的实例 static final Vector<String> badRedisServers = new Vector<String>(); // 连接池对象 static JedisPool jedisPool ; // 连接池配置信息对象 private static JedisPoolConfig config = new JedisPoolConfig(); /** * 配置连接池信息 * @return */ static { // 最大连接数10 config.setMaxTotal(10); //最大空闲连接数5 config.setMaxIdle(5); } /** * 获取jedis 实例 * @param * @return */ public Jedis newJedisInstance() { return jedisPool.getResource() ; } volatile static JavaSentinel javaSentinel; /** * 创建JavaSentinel对象 * @param isOpenSentinel 是否开启哨兵 true 开启, false 不开启 * @return * * 1) 如果开启哨兵, 我们创建一个定时任务, 延迟1秒,间隔3秒执行一次 * 2)每次执行时, 任务如下: * // 检测 master是否可以 * checkMaster(); * // 更新slave列表 * updateSlaves(); * // 检测坏掉的实例是否恢复正常 * checkBadServer(); * * 3)初始化 jedisPool 对象 和 javaSentinel对象 * */ public static synchronized JavaSentinel getInstance(boolean isOpenSentinel){ // 是否开启java哨兵 if(isOpenSentinel){ // 定时任务 new Timer().schedule(new TimerTask() { @Override public void run() { // 检测 master是否可以 checkMaster(); // 更新slave列表 updateSlaves(); // 检测坏掉的实例是否恢复正常 checkBadServer(); } }, 1000, 3000); } if(null == javaSentinel){ /** * 初始化redis连接池对象 */ String[] serverInfo = masterAddress.split(":"); String masterHost = serverInfo[0] ; int masterPort = Integer.parseInt(serverInfo[1]) ; jedisPool = new JedisPool(config, masterHost, masterPort, 100000); //初始化当前类对象 javaSentinel = new JavaSentinel(); } return javaSentinel; } /** * 该方法通过ping 方式, 检验当前redis主节点是否在线 * * 如若发生异常, 则主节点挂掉, 需要做如下两步: * 1)如果捕获到了异常证明: redis节点挂掉, 我们需要将当前主节点address保存到badRedisServers集合中 * 2)调用changeMaster() 方法,选举从节点作为新的主 */ private static void checkMaster() { // 主从切换 // 检查状态 System.out.println("检查master状态:" + masterAddress); String masterHost = masterAddress.split(":")[0]; int masterPort = Integer.parseInt(masterAddress.split(":")[1]); try { Jedis jedis = new Jedis(masterHost, masterPort); jedis.ping(); jedis.close(); } catch (Exception e) { // master挂掉啦 badRedisServers.add(masterAddress); // 切换master changeMaster(); } } /** * 切换master * * 1) 从slaveRedisServers集合中, 获取一个从节点地址 * 2)通过地址创建jedis对象尝试ping动作,验证器是否在线 * 3)没发生异常,证明在线,我们需要禁用它从死掉master继续同步数据 * 4)修改属性masterAddress 为新选举出来的slave地址 * 5)如果发生异常,则将当前slave存放在badRedisServers集合中, 进入下一次循环重试1-4 动作 * 6)选举成功后,将当前slave从 slaveRedisServers集合中移除掉 * * 7)遍历slaveRedisServers集合,将其他从节点 主从复制配置更新到刚刚选举出来的新主节点身上 */ private static void changeMaster() { Iterator<String> iterator = slaveRedisServers.iterator(); while (iterator.hasNext()) { String slaveAddress = iterator.next(); try { String slaveHost = slaveAddress.split(":")[0]; int slavePort = Integer.parseInt(slaveAddress.split(":")[1]); Jedis jedis = new Jedis(slaveHost, slavePort); /*确保当前从节点在线*/ jedis.ping(); /*禁用当前从节点同步复制*/ jedis.slaveofNoOne(); jedis.close(); masterAddress = slaveAddress; System.out.println("产生新的master:" + masterAddress); break; } catch (Exception e) { badRedisServers.add(slaveAddress); } finally { iterator.remove(); } } // 所有slave切到新的master for (String slave : slaveRedisServers) { String slaveHost = slave.split(":")[0]; int slavePort = Integer.parseInt(slave.split(":")[1]); Jedis jedis = new Jedis(slaveHost, slavePort); jedis.slaveof(masterAddress.split(":")[0], Integer.parseInt(masterAddress.split(":")[1])); jedis.close(); } } /** * 更新当前所有从节点到 slaveRedisServers中 * * 1)根据masterAddress 创建主节点Jedis对象 * 2)获取主节点replication配置信息jedis.info("replication"); * 3)根据配置信息, 获取到当前主节点从节点个数 * 4)循环遍历从节点个数, 如果个数大于0, 则清空当前 slaveRedisServers集合 * 5)从配置信息中截取出所有从节点的ip:端口后,放入到 slaveRedisServers集合中 * */ private static void updateSlaves() { // 获取所有slave try { String masterHost = masterAddress.split(":")[0]; int masterPort = Integer.parseInt(masterAddress.split(":")[1]); Jedis jedis = new Jedis(masterHost, masterPort); String info_replication = jedis.info("replication"); // 解析info replication String[] lines = info_replication.split("\r\n"); int slaveCount = Integer.parseInt(lines[2].split(":")[1]); if (slaveCount > 0) { slaveRedisServers.clear(); for (int i = 0; i < slaveCount; i++) { String host = lines[3 + i].split(",")[0].split("=")[1]; String port = lines[3 + i].split(",")[1].split("=")[1]; slaveRedisServers.add(host + ":" + port); } } System.out.println("更新slave列表:" + Arrays.toString(slaveRedisServers.toArray(new String[] {}))); jedis.close(); } catch (Exception e) { e.printStackTrace(); System.out.println("更新slave失败:" + e.getMessage()); } } /** * 检测坏掉的实例是否恢复正常 * 1)如果调用 pint() 没有发生异常, 证明恢复正常 * 2)恢复正常后,先将当前节点主从复制的配置通过slaveof() 挂载当前节点上 * 3)将当前节点地址从 badRedisServers集合中remove()掉, 并添加到 slaveRedisServers集合中。 * */ private static void checkBadServer() { // 获取所有slave Iterator<String> iterator = badRedisServers.iterator(); while (iterator.hasNext()) { String bad = iterator.next(); try { String badHost = bad.split(":")[0]; int badPort = Integer.parseInt(bad.split(":")[1]); Jedis badServer = new Jedis(badHost, badPort); badServer.ping(); // 如果ping没有问题,则挂在当前的master badServer.slaveof(masterAddress.split(":")[0], Integer.parseInt(masterAddress.split(":")[1])); badServer.close(); slaveRedisServers.add(bad); iterator.remove(); System.out.println(bad + " 恢复正常,当前master:" + masterAddress); } catch (Exception e) { } } } }
加载全部内容