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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM