由于 Redis 的主从复制有一个致命的问题,即当 Master 服务器宕掉时,不会自动切换 Master,需要人为干预。鉴于这个问题,Redis 提供了哨兵功能。哨兵是 Redis 集群架构中非常重要的一个组件,哨兵的出现主要是解决了主从复制出现故障时需要人为干预的问题,哨兵实现了自动化的故障恢复。
注意:如何搭建 Redis 哨兵模式,请参考“Redis 哨兵模式”或者“Redis 多哨兵模式”。
单哨兵模式指只有一个哨兵,该哨兵负责监听所有的主从节点。如果该哨兵出现问题,那么主从模式的自动化故障恢复将失效。下面使用 Jedis 通过哨兵连接到 Redis,实现读写,代码如下:
Set<String> sentinels = new HashSet<>();
sentinels.add("127.0.0.1:26379");
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);
// 永远返回的都是 Master 连接
Jedis jedis = pool.getResource();
// 获取 Master 的地址信息
HostAndPort hostAndPort = pool.getCurrentHostMaster();
System.out.println("master = " + hostAndPort.getHost() + ":" + hostAndPort.getPort());
// 写入数据
jedis.set("title", "www.hxstrive.com");
// 读取数据
String result = jedis.get("title");
System.out.println("result = " + result);多哨兵模式指配置多个哨兵,每个哨兵均对主从节点进行监听。如果某一个哨兵出现了问题,那么其他哨兵还是能够实现主从模式的自动化故障恢复。下面使用 Jedis 通过哨兵连接到 Redis,实现读写,代码如下:
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");
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);
// 永远返回的都是 Master 连接
Jedis jedis = pool.getResource();
// 获取 Master 的地址信息
HostAndPort hostAndPort = pool.getCurrentHostMaster();
System.out.println("master = " + hostAndPort.getHost() + ":" + hostAndPort.getPort());
// 写入数据
jedis.set("title", "www.hxstrive.com");
// 读取数据
String result = jedis.get("title");
System.out.println("result = " + result);有的读者可能会有疑问?Jedis 在哨兵模式下是不是会自动实现读写分离,遗憾的是 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());
   if (master.equals(connection)) {
     // connected to the correct master
     return jedis;
   } else {
     returnBrokenResource(jedis);
   }
 }
}上面源码中,if (master.equals(connection)) {} 代码表明返回的所有 Jedis 连接均是 Master 的连接。这就没有办法了,需要我们自己去实现读写分离,下面将通过一段简单的代码来实现读写分离,该代码仅仅用于学习,代码如下:
public class SentinelTest3 {
   class MyJedis {
       /** 主节点 */
       private Jedis master;
       /** 从节点,可能会有多个 */
       private List<Jedis> slaves = new ArrayList<>();
       public void setMaster(Jedis master) {
           this.master = master;
       }
       public void addSlaves(Jedis slave) {
           this.slaves.add(slave);
       }
       public String get(String key) {
           Jedis jedis = slaves.get((int)(Math.random() * slaves.size()));
           System.out.println(">> get:" + jedis);
           return jedis.get(key);
       }
       public void set(String key, String value) {
           master.set(key, value);
       }
   }
   public static void main(String[] args) {
       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");
       JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);
       // 永远返回的都是 Master 连接
       Jedis master = pool.getResource();
       // 获取 Master 的地址信息
       HostAndPort hostAndPort = pool.getCurrentHostMaster();
       System.out.println("master = " + hostAndPort.getHost() + ":" + hostAndPort.getPort() + " " + master);
       // 构造我们自己的对象
       SentinelTest demo = new SentinelTest();
       MyJedis myJedis = demo.new MyJedis();
       myJedis.setMaster(master);
       // 解析主从信息,提取从节点信息
       Pattern pattern = Pattern.compile("^slave\\d+:ip=(.+),port=(\\d+),state=.+$");
       String[] infos = master.info("replication").split("(\\r\\n)|(\\n)");
       for(String info : infos) {
           Matcher matcher = pattern.matcher(info);
           if(matcher.find()) {
               Jedis slave = new Jedis(matcher.group(1), Integer.valueOf(matcher.group(2)));
               myJedis.addSlaves(slave);
           }
       }
       // 写入数据
       myJedis.set("title", "www.hxstrive.com");
       // 读取数据
       String result = myJedis.get("title");
       System.out.println("result = " + result);
       result = myJedis.get("title");
       System.out.println("result = " + result);
   }
}上面代码中,通过自定义的 MyJedis 类来包装了 get 和 set 操作,在包装中的方法中,根据具体业务使用 Master 或 Slave 的连接来操作 Redis。
