Redis集群一致性Hash效果的代码演示
在微服務(wù)領(lǐng)域,使用Redis做緩存可并不是一件容易的事情。
像新浪、推特這樣的應(yīng)用,許許多多的熱點(diǎn)數(shù)據(jù)全都存放在Redis這一層,打到DB層的請(qǐng)求并不多,可以說(shuō)非常依賴緩存了。如果緩存掛掉,流量全部穿透到DB層,其必然不堪其重,整個(gè)系統(tǒng)也會(huì)隨之癱瘓,后果非常嚴(yán)重。 由于緩存數(shù)據(jù)量很大,Redis快正是快在其基于內(nèi)存的快速存取,而計(jì)算機(jī)的內(nèi)存資源又是十分有限的,故分布式緩存集群面臨著伸縮性的要求。
一致性Hash存在的意義
Redis集群中的各實(shí)例之間是并不知道對(duì)方的,需要在客戶端實(shí)現(xiàn)路由法來(lái)將key路由到不同的redis節(jié)點(diǎn)。
該路由算法是關(guān)鍵,它必須讓新上線的緩存服務(wù)器對(duì)整個(gè)分布式緩存集群影響最小,使得擴(kuò)容后,整個(gè)緩存服務(wù)器集群中已經(jīng)緩存的數(shù)據(jù)盡可能還被訪問(wèn)到。
若是使用一般的對(duì)key進(jìn)行一次hash的算法,則會(huì)導(dǎo)致擴(kuò)容后命中率極低。 如下表所示,當(dāng)集群由3個(gè)節(jié)點(diǎn)擴(kuò)容到4個(gè)節(jié)點(diǎn)時(shí),會(huì)有75%的key無(wú)法命中。
| 1 | 1 | 1 | 是 |
| 2 | 2 | 2 | 是 |
| 3 | 0 | 3 | 否 |
| 4 | 1 | 0 | 否 |
| 5 | 2 | 1 | 否 |
| 6 | 0 | 2 | 否 |
| 7 | 1 | 3 | 否 |
| 8 | 2 | 0 | 否 |
| 9 | 0 | 1 | 否 |
| 10 | 1 | 2 | 否 |
| 11 | 2 | 3 | 否 |
| 12 | 0 | 0 | 是 |
這可太糟糕了,當(dāng)服務(wù)器數(shù)量為100臺(tái)時(shí),再增加一臺(tái)新服務(wù)器,不能命中率將達(dá)到99%,這和整個(gè)緩存服務(wù)掛了一個(gè)效果。
而一致性Hash正是為了解決這個(gè)問(wèn)題而出現(xiàn)的,該路由算法通過(guò)引入一個(gè)一致性Hash環(huán),以及進(jìn)一步增加虛擬節(jié)點(diǎn)層,來(lái)實(shí)現(xiàn)盡可能高的命中率。 使用該算法,當(dāng)節(jié)點(diǎn)由n擴(kuò)容為n+1時(shí),命中率可保持在n/(n+1)左右。
關(guān)于該算法的具體原理與網(wǎng)上已經(jīng)有一些說(shuō)得很透徹的文章,本文不再贅述。 下面主要從代碼實(shí)現(xiàn)及運(yùn)行的方式來(lái)對(duì)此算法的效果進(jìn)行展示。
本機(jī)部署多個(gè)Redis節(jié)點(diǎn)
要對(duì)一致性Hash進(jìn)行驗(yàn)證,要做好準(zhǔn)備工作,首先要有一個(gè)Redis集群。 這里我通過(guò)使用在本機(jī)上部署多個(gè)Redis實(shí)例指向不同端口來(lái)模擬這一形態(tài)。
建立項(xiàng)目目錄:$ mkdir redis-conf 將redis的配置copy一份過(guò)來(lái)并復(fù)制為5份,分別命名為redis-6379.conf~redis-6383.conf。
需要對(duì)其內(nèi)容進(jìn)行一些修改才能正常啟動(dòng),分別找到配置文件中的如下兩行并對(duì)數(shù)字進(jìn)行相應(yīng)修改。
port 6379 pidfile /var/run/redis_6379.pid 復(fù)制代碼然后就可以分別啟動(dòng)了:redis-server ./redis-6379 &
可以使用redis-cli -p 6379來(lái)指定連接的redis-server。 不妨進(jìn)行一次嘗試,比如在6379設(shè)置key 1 2,而到6380 get 1只能得到nil,說(shuō)明它們是各自工作的,已經(jīng)滿足可以測(cè)試的條件。
代碼實(shí)現(xiàn)
思路是這樣的: 部署4個(gè)節(jié)點(diǎn),從6379到6382,通過(guò)一致性Hash算法,將key: 0~99999共100000個(gè)key分別set到這4個(gè)服務(wù)器上,然后再部署一個(gè)節(jié)點(diǎn)6383,這時(shí)再?gòu)?到99999開(kāi)始get一遍,統(tǒng)計(jì)get到的次數(shù)來(lái)驗(yàn)證命中率是否為期望的80%左右(4/5)。
一致性Hash算法的實(shí)現(xiàn)嚴(yán)重借鑒了這篇文章,使用紅黑樹(shù)來(lái)做數(shù)據(jù)結(jié)構(gòu),來(lái)實(shí)現(xiàn)log(n)的查找時(shí)間復(fù)雜度,使用FNV1_32_HASH哈希算法來(lái)盡可能使key與節(jié)點(diǎn)分布得更加均勻,引入了虛擬節(jié)點(diǎn),來(lái)做負(fù)載均衡。
建議讀者詳細(xì)看下這篇文章,里面的講解非常詳細(xì)易懂。
下面是我改寫過(guò)后的代碼:
package org.guerbai.io.jedistry;import redis.clients.jedis.Jedis; import java.util.*;class JedisProxy {private static String[][] redisNodeList = {{"localhost", "6379"},{"localhost", "6380"},{"localhost", "6381"},{"localhost", "6382"},};private static Map<String, Jedis> serverConnectMap = new HashMap<>();private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();private static final int VIRTUAL_NODES = 100;static{for (String[] str: redisNodeList){addServer(str[0], str[1]);}System.out.println();}private static int getHash(String str){final int p = 16777619;int hash = (int)2166136261L;for (int i = 0; i < str.length(); i++)hash = (hash ^ str.charAt(i)) * p;hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;// 如果算出來(lái)的值為負(fù)數(shù)則取其絕對(duì)值if (hash < 0)hash = Math.abs(hash);return hash;}private static String getServer(String node){// 得到帶路由的結(jié)點(diǎn)的Hash值int hash = getHash(node);// 得到大于該Hash值的所有MapSortedMap<Integer, String> subMap =virtualNodes.tailMap(hash);// 第一個(gè)Key就是順時(shí)針過(guò)去離node最近的那個(gè)結(jié)點(diǎn)if (subMap.isEmpty()) {subMap = virtualNodes.tailMap(0);}Integer i = subMap.firstKey();// 返回對(duì)應(yīng)的虛擬節(jié)點(diǎn)名稱,這里字符串稍微截取一下String virtualNode = subMap.get(i);return virtualNode.substring(0, virtualNode.indexOf("&&"));}public static void addServer(String ip, String port) {for (int i = 0; i < VIRTUAL_NODES; i++){String virtualNodeName = ip + ":" + port + "&&VN" + String.valueOf(i);int hash = getHash(virtualNodeName);System.out.println("虛擬節(jié)點(diǎn)[" + virtualNodeName + "]被添加, hash值為" + hash);virtualNodes.put(hash, virtualNodeName);}serverConnectMap.put(ip+":"+port, new Jedis(ip, Integer.parseInt(port)));}public String get(String key) {String server = getServer(key);Jedis serverConnector = serverConnectMap.get(server);if (serverConnector.get(key) == null) {System.out.println(key + "not in host: " + server);}return serverConnector.get(key);}public void set(String key, String value) {String server = getServer(key);Jedis serverConnector = serverConnectMap.get(server);serverConnector.set(key, value);System.out.println("set " + key + " into host: " + server);}public void flushdb() {for (String str: serverConnectMap.keySet()) {System.out.println("清空host: " + str);serverConnectMap.get(str).flushDB();}}public float targetPercent(List<String> keyList) {int mingzhong = 0;for (String key: keyList) {String server = getServer(key);Jedis serverConnector = serverConnectMap.get(server);if (serverConnector.get(key) != null) {mingzhong++;}}return (float) mingzhong / keyList.size();}}public class ConsistencyHashDemo {public static void main(String[] args) {JedisProxy jedis = new JedisProxy();jedis.flushdb();List<String> keyList = new ArrayList<>();for (int i=0; i<100000; i++) {keyList.add(Integer.toString(i));jedis.set(Integer.toString(i), "value");}System.out.println("target percent before add a server node: " + jedis.targetPercent(keyList));JedisProxy.addServer("localhost", "6383");System.out.println("target percent after add a server node: " + jedis.targetPercent(keyList));} } 復(fù)制代碼以上代碼對(duì)參考文章進(jìn)行了一些改進(jìn)。
首先,參考文章的getServer方法會(huì)有些問(wèn)題,當(dāng)key大于最大的虛擬節(jié)點(diǎn)hash值時(shí)tailMap方法會(huì)返回空,找不到節(jié)點(diǎn)會(huì)報(bào)錯(cuò),其實(shí)這時(shí)應(yīng)該去找hash值最小的一個(gè)虛擬節(jié)點(diǎn)。我加了處理,把這個(gè)環(huán)連上了。
下面getHash方法為FNV1_32_HASH算法,可以不用太在意。
VIRTUAL_NODES的值比較重要,當(dāng)節(jié)點(diǎn)數(shù)目較少時(shí),虛擬節(jié)點(diǎn)數(shù)目越大,命中率越高。
在程序設(shè)計(jì)上也有很大的不同,我寫了JedisProxy類,來(lái)做為client訪問(wèn)Redis的中間層,在該類的static塊中利用服務(wù)器節(jié)點(diǎn)生成虛擬節(jié)點(diǎn)構(gòu)造好紅黑樹(shù),getServer里根據(jù)tailMap方法取出實(shí)際節(jié)點(diǎn)的地址,再由實(shí)際節(jié)點(diǎn)的地址直接拿到j(luò)edis對(duì)象,提供簡(jiǎn)單的get與set方法,先根據(jù)key拿特定的jedis對(duì)象,再進(jìn)行g(shù)et, set操作。
addServer靜態(tài)方法給了其動(dòng)態(tài)擴(kuò)容的能力,可以看到在main方法中,通過(guò)調(diào)用JedisProxy.addServer("localhost", "6383")便直接增加了節(jié)點(diǎn),不需要停應(yīng)用。 targetPercent方法是用來(lái)統(tǒng)計(jì)命中率用。
當(dāng)虛擬節(jié)點(diǎn)為5時(shí),命中率約為60%左右,把它加大到100后,可以到達(dá)預(yù)期的80%的命中率。
好的,完美。
轉(zhuǎn)載于:https://juejin.im/post/5c52cfcc51882542ff129941
總結(jié)
以上是生活随笔為你收集整理的Redis集群一致性Hash效果的代码演示的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: SQL Server如何链接到 Orac
- 下一篇: 入门系列之在Ubuntu 16.04使用