前面配置了三個節點的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的記錄,這里記錄一下,后續知識繼續補充。
參考博文
