由于 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。