如何简单地实现易用的ShardedJedisSentinelPool

来源:互联网 发布:带宽测试软件 编辑:程序博客网 时间:2024/04/29 22:56

Jedis包中有个很恶心的问题,那就是包里面有支持分片的ShardeJedis客户端类,也有支持哨兵的池类JedisSentinelPool,就是没有既支持分片又支持哨兵的池类,所以必须自己自定义一个ShardedJedisSentinelPool,定义这个类,在网上有个很受欢迎的版本,是继承了Pool<ShardeJedis>类后重写相关的池操作的方法,个人觉得这种方案太麻烦,而且据反馈也有很多考虑不全面的细节,造成bug。一开始我试图去继承JedisSentinelPool类并重写相关方法把分配功能加进去,发现很难搞。后来改变了思路,其实可以建立一个JedisSentinelPool数组,每个元素其实对应监视一个主备的redis服务节点,然后可以根据分片规则去确定从哪个JedisSentinelPool中获取jedis。这个方案比较简单实用,唯一难点就是要去实现跟ShardeJedis一样的一致性哈希分片规则。好在sharde.java中有相关源码,只要稍作修改即可。实现源码如下:

public class ShardedJedisSentinelPool {
private Map<String,JedisSentinelPool> poolMap = new HashMap<String,JedisSentinelPool>();
private Hashing algo = Hashing.MURMUR_HASH;
private TreeMap<Long, JedisShardInfo> nodes;

public ShardedJedisSentinelPool(Set<HostAndPort> hostInfo, Set<String> sentinels){
List<JedisShardInfo> list = new ArrayList<JedisShardInfo>();
for(HostAndPort iter:hostInfo){
JedisShardInfo info = new JedisShardInfo(iter.getHost(),iter.getPort());
list.add(info);
}
initialize(list);

for(HostAndPort iter:hostInfo){
String masterName = "master-"+iter.getHost()+":"+iter.getPort();
JedisSentinelPool sentinuelPool = new JedisSentinelPool(masterName, sentinels);
poolMap.put(masterName, sentinuelPool);
}
}

public Jedis getResource(String key){
JedisShardInfo shardInfo = getShardInfo(key.getBytes());
String masterName = "master-"+shardInfo.getHost()+":"+shardInfo.getPort();
JedisSentinelPool sentinuelPool = poolMap.get(masterName);
Jedis sentinueJedis = sentinuelPool.getResource();
return sentinueJedis;
}

private void initialize(List<JedisShardInfo> shards) {
   nodes = new TreeMap<Long, JedisShardInfo>();


   for (int i = 0; i != shards.size(); ++i) {
     final JedisShardInfo shardInfo = shards.get(i);
     if (shardInfo.getName() == null) for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
       nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
     }
     else for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
       nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);
     }
   }
 }

 public JedisShardInfo getShardInfo(byte[] key) {
   SortedMap<Long, JedisShardInfo> tail = nodes.tailMap(algo.hash(key));
   if (tail.isEmpty()) {
     return nodes.get(nodes.firstKey());
   }
   return tail.get(tail.firstKey());
 }
 
 public void close(){
 for(Entry<String, JedisSentinelPool> iter:poolMap.entrySet()){
 iter.getValue().destroy();
 }
 }
}

 

--在这个方法中,构造函数先初始化了分片规则得到了每个主节点和160个虚拟节点的映射关系TreeMap<Long, JedisShardInfo> nodes,然后初始化了JedisSentinelPool数组,对于每个需要根据key获取jedis的用户,先经过hash(key),然后nodes.get(hash(key))得到对应的JedisShardInfo,然后根据JedisShardInfo对象获取服务主节点的ip和端口号组成masterName,这样就可以根据masterName去哨兵池JedisSentinelPool动态获取真正可用的jedis对象。这样一来如果某个redis服务主节点挂了,哨兵集群会发现并且把从节点当作新的主节点。这样就能达到高可用的效果。

其中的一致性哈希算法过程稍微解释下:

当我们传入一个JedisShardInfo数组进去后,就会为每个JedisShardInfo对象分配160个虚拟节点,每个虚拟节点的可key由this.algo.hash("SHARD-" + i +"-NODE-" + n)或者this.algo.hash(shardInfo.getName() +"*" + shardInfo.getWeight() + n)计算得出,这样就形成160个分散的虚拟key和1个JedisShardInfo的映射关系的map, JedisShardInfo数组里每个JedisShardInfo都会得到160个这样的映射关系(虚拟节点),并且最后都全部加入大的map(TreeMap<Long, JedisShardInfo> nodes)中。当我们需要获取某个真实key值对应的真正JedisShardInfo(里面包含服务节点的ip和port信息),就对该key也执行hash操作algo.hash(key),然后通过TreeMap.tailMap获取虚拟节点的key大于或等于algo.hash(key)的第一个虚拟节点已经该虚拟节点对应的JedisShardInfo,如果algo.hash(key)大于所有虚拟节点的key ,那么由于一致性哈希环的概念,该key将取所有虚拟节点中的第一个(也就是最小那一个)并获取第一个虚拟节点对应的JedisShardInfo。

这里还有个小的知识点就是TreeMap或则TreeXXXX的容器的作用一般就是通过红黑树的数据结构进行了排序,它们的作用就是这是一个有序的容器。
原创粉丝点击