怎么在Redis集群中使用JedisCluster操作单个Redis实例

2,143 阅读5分钟

应用场景

如果你的某一个Redis操作必须要使用事务或者脚本保证其原子性或者需要进行多个key的操作的时候,但是公司部署的又是Redis集群。这种情况下就必须使用单实例的Redis来操作了。


方法一、简单粗暴

把集群的每个Master都连接一遍,然后在客户端进行slot映射缓存每个Master对应的JedisPool

方法二、从JedisCluster中获取

介绍这种方法前先介绍一下怎么把多个不同的key写到同一个Redis实例中。下面上官网原文:

键哈希标签(Keys hash tags)

计算哈希槽可以实现哈希标签(hash tags),但这有一个例外。哈希标签是确保两个键都在同一个哈希槽里的一种方式。将来也许会使用到哈希标签,例如为了在集群稳定的情况下(没有在做碎片重组操作)允许某些多键操作。

为了实现哈希标签,哈希槽是用另一种不同的方式计算的。基本来说,如果一个键包含一个 “{…}” 这样的模式,只有 { 和 } 之间的字符串会被用来做哈希以获取哈希槽。但是由于可能出现多个 { 或 },计算的算法如下:

  • 如果键包含一个 { 字符。
  • 那么在 { 的右边就会有一个 }。
  • 在 { 和 } 之间会有一个或多个字符,第一个 } 一定是出现在第一个 { 之后。

然后不是直接计算键的哈希,只有在第一个 { 和它右边第一个 } 之间的内容会被用来计算哈希值。

例子:

  • 比如这两个键 {user1000}.following 和 {user1000}.followers 会被哈希到同一个哈希槽里,因为只有 user1000 这个子串会被用来计算哈希值。
  • 对于 foo{}{bar} 这个键,整个键都会被用来计算哈希值,因为第一个出现的 { 和它右边第一个出现的 } 之间没有任何字符。
  • 对于 foozap 这个键,用来计算哈希值的是 {bar 这个子串,因为它是第一个 { 及其右边第一个 } 之间的内容。
  • 对于 foo{bar}{zap} 这个键,用来计算哈希值的是 bar 这个子串,因为算法会在第一次有效或无效(比如中间没有任何字节)地匹配到 { 和 } 的时候停止。
  • 按照这个算法,如果一个键是以 {} 开头的话,那么就当作整个键会被用来计算哈希值。当使用二进制数据做为键名称的时候,这是非常有用的。

代码验证一下:

public static void main(String[] args) {   
   String keyPre = "{dhfhdksaf}";    
   System.out.println(JedisClusterCRC16.getSlot(keyPre+"2") == JedisClusterCRC16.getSlot(keyPre+"1"));
}

通过使用这个方法就可以将多个Key写到同一个实例了。

获取单个Redis实例

先说一下核心原理:

JedisCluster中有两个缓存Map,其中一个是node的Map<String,JedisPool>,具体可以看上一篇文章JedisCluster源码阅读,这个Map可以通过cluster.getClusterNodes()方法获取到,这样我们就可以获取到JedisPool了,然后通过这个JedisPool的jedis实例去执行集群命令cluster node就可以获取集权中的节点信息,这些节点信息中包含了每个Redis实例的slot信息:


最后就可以根据key的slot判断应该使用哪个JedisPool了。

以上这个方法适用于2.9.x版本之前的jedis

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

这种方式还是需要些过多的代码,其实Jedis的源码中已经有这种代码了的,只不过没有开放出来,在2.10.X版本之后就已经提供了一个方法来获取。我们先介绍低版本的方法。

节点代码

//节点信息
public class RedisNode {
    private String ip;
    private String port;
    //slot信息
    private List<Slot> slots;
    private JedisPool jedisPool;

    public RedisNode(){
        slots = new ArrayList<>();
    }

    //Slot对象,保存最大值和最小值
    static class Slot{
        private int min;
        private int max;
        public Slot(int min,int max){
            this.min = min;
            this.max = max;
        }
    }

    /**
     * 判断slot是否在这个节点
     * @param slot
     * @return
     */
    public boolean containsSlot(int slot){
        for(Slot s : slots){
            if( s.min <= slot && slot <= s.max){
                return true;
            }
        }
        return false;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }

    public JedisPool getJedisPool() {
        return jedisPool;
    }

    public void setJedisPool(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    public void setSlots(int min,int max){
        slots.add(new Slot(min,max));
    }

}

单节点连接池

public class RedisNodePool {

    public static final String MASTER = "master";
    /**
     * 用于存放当前Redis集群中的Master节点
     */
    private List<RedisNode> list;

    public RedisNodePool(){
        list = new CopyOnWriteArrayList<>();
    }

    public void add(RedisNode redisNode){
        list.add(redisNode);
    }

    /**
     * 初始化连接池
     * 如果连接发生异常了,可以重新调用这个方法初始化,这个时候会重新从JedisCluster中获取连接
     * @param cluster
     */
    public void init(JedisCluster cluster){
        if(!list.isEmpty()){
            list.clear();
        }
        //获取JedisCluster中的节点缓存信息
        Map<String, JedisPool> nodesMap = cluster.getClusterNodes();
        for(Map.Entry entry : nodesMap.entrySet()){
            JedisPool jedisPool = nodesMap.get(entry.getKey());
            Jedis jedis = jedisPool.getResource();
            String nodeInfo = jedis.clusterNodes();
            //多个节点换行
            String [] nodes = nodeInfo.split("\n");
            for(String node : nodes){
                String [] array = node.split(" ");
                RedisNode redisNode = new RedisNode();
                //<9的节点没有slot
                if(!array[2].contains(MASTER) || array.length < 9){
                    continue;
                }
                String [] ipAndPort = array[1].split("\\@")[0].split("\\:");
                redisNode.setIp(ipAndPort[0]);
                redisNode.setPort(ipAndPort[1]);
                for (int i = 8; i < array.length; i++) {
                    String[] slot = array[i].split("-");
                    if(slot.length == 1){
                        //有可能有的节点存在单个不连续的slot
                        redisNode.setSlots(Integer.valueOf(slot[0]),Integer.valueOf(slot[0]));
                    }else {
                        redisNode.setSlots(Integer.valueOf(slot[0]),Integer.valueOf(slot[1]));
                    }
                }
                //设置连接池
                redisNode.setJedisPool(jedisPool);
                list.add(redisNode);
            }
        }
    }

    /**
     * 根据Key获取一个Jedis
     * @param key
     * @return
     */
    public Jedis getJedis(String key){
        //计算key的slot
        int slot = JedisClusterCRC16.getSlot(key);
        for(RedisNode redisNode : list){
            //判断key的slot在哪一个master节点
            if(redisNode.containsSlot(slot)){
                return redisNode.getJedisPool().getResource();
            }
        }
        throw new IllegalArgumentException("redis node not found,key="+key);
    }

    /**
     * 关闭Jedis的连接
     * @param jedis
     */
    public void close(Jedis jedis){
        if(jedis!=null){
            jedis.close();
        }else{
            throw new NullPointerException("jedis is null");
        }
    }

}

调用方法

@Test
    public void test2(){
        redisNodePool.getJedis("key").set("key","hello");
        System.out.println(redisNodePool.getJedis("key").get("key"));
    }

下面我们来看一个2.10.x之后的版本的获取方式

我们先看看源码,这个是JedisCluster的父类的代码,通过持有连接的Handler去获取某一个slot对应的Jedis,原理也是我们之前说的。 

public class BinaryJedisCluster implements BasicCommands, BinaryJedisClusterCommands, MultiKeyBinaryJedisClusterCommands, JedisClusterBinaryScriptingCommands, Closeable {
    public static final int HASHSLOTS = 16384;
    protected static final int DEFAULT_TIMEOUT = 2000;
    protected static final int DEFAULT_MAX_ATTEMPTS = 5;
    protected int maxAttempts;
    protected JedisClusterConnectionHandler connectionHandler;

   

    public Map<String, JedisPool> getClusterNodes() {
        return this.connectionHandler.getNodes();
    }
    //获取某一个slot在缓存池中对应的JedisPool中的连接
    public Jedis getConnectionFromSlot(int slot) {
        return this.connectionHandler.getConnectionFromSlot(slot);
    }

	//其余代码忽略
}

使用方法

public static void main(String[] args) {
    JedisCluster jedisCluster;
    Jedis jedis = jedisCluster.getConnectionFromSlot(JedisClusterCRC16.getSlot("key"));
}