zoukankan      html  css  js  c++  java
  • Redis哈希一致性&对应API操作

    前面配置了三个节点的redis服务后,通过对key的hash取余来决定kev-value来存入哪个节点。但是考虑到对redis服务进行扩容和缩容时(增减redis节点),会出现数据的未命中,严重会导致雪崩,因此不使用哈希取余来分配key-value。redis采用的是哈希一致性的算法,这种算法会优化哈希取余未命中的问题,其中SharedJedis就是实现了这种算法的类,可以通过它底层进行哈希一致性计算后,分配key-value到具体的节点。

    哈希取余和哈希一致性

    当存在多个redis节点时,不管是哈希取余,还是哈希一致性,都是为了让key-value找到它的"归宿",即具体的redis节点,反过来通过key,可以找到对应的保存了数据的redis节点。

    (1)哈希取余,存在redis节点扩容和缩容数据未命中的问题,如图,假设增加一个redis04节点,则原来保存在redis01的某个key-value,扩容后通过key依然可以从redis01获取到数据就变成了概率事件。

    概率计算:

    a.假设redis01为0号分区,即key.hashCode()&Integer.MAX_VALUE%3=0的分区,则这个key进行31位保真运算后的整数值为3的倍数,以3n表示。

    b.扩容后,取余的分母变成4,因此这个key继续落到0号分区的概率,等于3n/4==0的概率,而3不能被4整除,因此就等效于n/4==0的概率,这个概率为25%。

    计算完后,发现命中的概率仅为25%,而未命中的概率高达75%,这就容易导致数据大量未命中后雪崩的发生。这是3个节点扩容的情况,以此类推,如果是m个节点,则扩容一个节点后,数据命中的概率为1/m+1,未命中概率为m/m+1,可见节点越多,未命中的概率越大,这是非常可怕的事情。

    (2)哈希一致性,是基于哈希环的,用一个0-2^31-1的数字区间,包含redis节点的位置信息,以及key-value的位置信息,然后通过某种规则,将redis节点和key-value联系起来,就可以实现上面说的找到key-value的"归宿"。

    a.哈希环

    内存中的对象数据,通过CRC16算法映射到这个区间,只要对象不变,对象在哈希环中的对应位置就不变,这个跟哈希取余有点类似,都是能确定位置,它就是一个记录地址的载体,通过它可以获取对象数据的地址信息,可以作为中间信息过渡。

     b.redis节点和key在哈希环中的映射

    根据CRC16算法,它们都会在哈希环中有个对应的位置,入图所示。接下来就需要将redis节点和key对应起来,采用的规则是以key为参照物,顺时针寻找最近的redis节点,这个节点就是key需要存储的位置,因此下图节点和key之间的对应关系就是(key1 key2→redis01),(key3 key4→redis02),(key5→redis03),这样就确定了key-value存储的位置了。

    c.redis节点扩容或缩容时,数据迁移和未命中的问题。

    以扩容为例,如图如果添加一个节点redis04,发现key4指向的节点发生了变化,变成了redis04,这样就造成了key4的数据迁移和未命中,这样不跟哈希取余一样的结局吗,其实还是有区别的。hash取余添加节点后,波及的范围是整体,而hash一致性,波及的范围只是添加了这个节点的哈希环的一个弧段,当前也就影响了redis02和redis03之间的区间,其他不受影响,因此造成的数据未命中也只是redis03和redis04之间的数据范围。如果哈希环中的redis节点越多,则影响的范围从概率上来说就越小,所以它是对hash取余的一个大的优化。

    d.虚拟节点的引入

    事实上,如果上面的redis节点通过CRC16算法计算后映射到hash环中的位置非常集中,这样势必会造成某些节点对应的数据非常少或非常多,产生数据的倾斜。为了解决这个问题引入了虚拟节点,默认一个真实的redis节点会对应160个虚拟节点,key顺时针如果找到了虚拟节点,通过虚拟节点就可以找到真实的节点。由于虚拟节点量大,在哈希环中均匀分布的概率就大,这样数据倾斜的概率就会降低。

    一般真实节点做映射会使用ip+端口号,如192.168.200.140:6379,则虚拟节点就是192.168.200.140:6379#1~192.168.200.140:6379#160来做映射。

    e.节点的权重

    如果想让某个节点能存储更多的数据怎么办,hash一致性也可以设置权重,可以配置更多的虚拟节点,就可以实现,使用API连接操作时可以在JedisShardInfo中指定。

    相关API操作

    哈希一致性有对应的api操作,在进行hash一致性的api操作之前,先捋一遍redis中目前常用的操作方式,暂时不考虑redis-cluster的情况。 

    (1)使用Jedis连接单个redis节点

    分为单个Jedis实例对象连接,和JedisPool来连接两种。

    a.单个实例对象连接,参考上篇https://www.cnblogs.com/youngchaolin/p/11983705.html#_label2。后续所有的api操作是在上次测试的环境中完成。

    b.JedisPool连接。

        //jedis连接池的使用,连接单个节点
        @Test
        public void test04(){
            JedisPool jedisPool=new JedisPool("192.168.200.140",6379);
            //从连接池中获取redis
            Jedis jedis=jedisPool.getResource();
            //使用jedis
            jedis.set("clyang","I have a dream");
            String s = jedis.get("clyang");
            System.out.println(s);
            //使用完后jedis归还到连接池
            jedisPool.returnResource(jedis);
            //关闭jedis连接
            jedis.close();
        }

    测试ok。

    127.0.0.1:6379> get clyang
    "I have a dream"

    (2)使用Jedis连接多个redis节点

    这里使用了两种方式,一种是通过Jedis实例对象连接多个redis节点,另外一种类似上面,也是通过JedisPool来连接多个redis节点,两者均使用hash取余。

    a.单个Jedis节点连接的情况,也是参考上篇https://www.cnblogs.com/youngchaolin/p/11983705.html#_label2

    b.使用JedisPool的方式连接,这里封装成了一个HashJedis类,在里面添加hash取余。

    HashJedis类

    package com.boe;
    
    import org.junit.Test;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 封装了hash取余算法的类,使用JedisPool来连接
     */
    public class HashJedis {
        /**
         * set,get,hset,hget等,所有jedis底层操作,都可以重写
         */
        private int N;
        private List<JedisPool> poolList=new ArrayList<>();
    
        public HashJedis(){
    
        }
    
        public HashJedis(List<String> nodes){
            N=nodes.size();
            for (String node : nodes) {
                System.out.println(node);
                String host=node.split(":")[0];
                int port=Integer.parseInt(node.split(":")[1]);
                //构造一个连接池对象
                JedisPool pool=new JedisPool(host,port);
                poolList.add(pool);
            }
        }
    
        //set、get方法的案例,完成分片
        public void set(String key,String value){
            //获取hash取余计算后的节点
            JedisPool jedisPool= hashKeyToNode(key);
            Jedis jedis=jedisPool.getResource();
            //对获取的节点进行get ,set操作等。
            try{
                jedis.set(key,value);
                System.out.println("分片set成功");
            }catch (Exception e){
                e.printStackTrace();
                System.out.println("分片set失败");
            }finally {
                /*if(jedis!=null){
                    jedis.close();
                }else{
                    jedis=null;
                }*/
                //将jedis归还到jedispool
                jedisPool.returnResource(jedis);
            }
        }
    
        public Object get(String key){
            //获取hash取余计算后的节点
            JedisPool jedisPool = hashKeyToNode(key);
            Jedis jedis=jedisPool.getResource();
            //对获取的节点进行get ,set操作等。
            try{
                String s = jedis.get(key);
                System.out.println("分片get成功");
                return s;
            }catch (Exception e){
                e.printStackTrace();
                System.out.println("分片get失败");
                return "";
            }finally {
                /*if(jedis!=null){
                    jedis.close();
                }else{
                    jedis=null;
                }*/
                jedisPool.returnResource(jedis);
            }
        }
    
        //自定义获取节点的方法
        public JedisPool hashKeyToNode(String key){
            int result=(key.hashCode()&Integer.MAX_VALUE)%N;
            //从上面保存的集合中取出节点
            JedisPool jedisPool = poolList.get(result);
            //Jedis jedis = jedisPool.getResource();
            //return jedis;
            return jedisPool;
        }
    
        //jedis本身也有上述封装的方法,叫做SharedJedis,底层使用的是hash一致性
    }

    测试方法,只要set进去了,就能通过key的hash取余找到存储的redis节点,将value获取到。

       @Test
        public void test01(){
            List<String> nodeList=new ArrayList<>();
            String node01="192.168.200.140:6379";
            String node02="192.168.200.140:6380";
            String node03="192.168.200.140:6381";
            nodeList.add(node01);
            nodeList.add(node02);
            nodeList.add(node03);
            //封装了hash取余以及JedisPool的对象
            HashJedis hashJedis=new HashJedis(nodeList);
            //set
            hashJedis.set("name","messi");
            //get
            Object name = hashJedis.get("name");
            System.out.println((String)name);
        }

    测试ok。

    127.0.0.1:6379> get name
    "messi"

    (3)通过SharedJedis来连接

    这才是主角,它是通过hash一致性来确认连接节点的,跟上面类似,它既有单个SharedJedis对象的连接操作,也有对象SharedJedisPool连接池的操作。这里两种都测试下,并且如上所说,可以对单个redis节点通过SharedJedis设置权重。

       //jedis本身也有封装的方法,叫做SharedJedis,底层使用hash一致性来实现
        @Test
        public void test02(){
            List<JedisShardInfo> list=new ArrayList<>();
            //第一个节点设置权重为3
            list.add(new JedisShardInfo("192.168.200.140",6379,500,500,3));
            list.add(new JedisShardInfo("192.168.200.140",6380));
            list.add(new JedisShardInfo("192.168.200.140",6381));
    
            //使用SharedJedis分片对象
            /*ShardedJedis shardedJedis=new ShardedJedis(list);
            shardedJedis.set("star","herry");
            System.out.println(shardedJedis.get("star"));*/
    
            //使用SharedJedisPool分片连接池对象
            JedisPoolConfig config=new JedisPoolConfig();
            config.setMaxTotal(200);
            config.setMaxIdle(8);
            config.setMinIdle(3);
            ShardedJedisPool pool=new ShardedJedisPool(config,list);
            //获取一个SharedJedis对象
            ShardedJedis resource = pool.getResource();
            //set测试
            for (int i = 0; i < 1000; i++) {
                String key= UUID.randomUUID().toString();
                resource.set(key,"");
            }
        }

    a.使用SharedJedis测试,往redis中存入数据,ok。

    127.0.0.1:6380> get star
    "herry"

    b.使用ShareJedisPool测试,往redis存入数据,并设置了6379端口的redis权重为3,其他两个端口的默认都为1,因此测试结果理论6379上面数据会是3/5的比例。

    redis01结果,621个数据。

    redis02结果,177个数据。

    redis03结果,202个数据。

    可以看出结果跟理论接近,只是实际上有些许数据倾斜。  

    以上就是对redis哈希一致性和相关API的记录,这里记录一下,后续知识继续补充。

    参考博文

    (1)https://www.jianshu.com/p/af7d933439a3 

  • 相关阅读:
    开源程序postGIS为postgresql提供存储空间地理数据的支持
    oracle官方文档
    postgresql转换符
    【转】oracle的Interval类型详解
    [转]千万不要把灯泡放进嘴里
    服务器运维指令
    莫名其妙的时区问题
    Linux内存思想
    oracle提高查询效率24法
    哈佛大学图书馆凌晨4点的景象
  • 原文地址:https://www.cnblogs.com/youngchaolin/p/12003706.html
Copyright © 2011-2022 走看看