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