1. 程式人生 > >一致性hash在 redis中應用

一致性hash在 redis中應用

1.ShardedJedis內部實現

首先我們來看一下ShardedJedis的一個繼承關係

看完了圖,那麼我們一步一步跟著我們的程式碼呼叫來看,以我們最簡單的 ShardedJedis.get(key)方法為例:

  public String get(String key) {
    Jedis j = getShard(key);
    return j.get(key);
  }

這邊有呼叫一個getShard 方法,引數為我們傳入的key,然後返回一個普通的jedis物件,那麼這個getShard是用來做什麼的呢,大家可能已經猜到了,這個方法就是會根據我們傳入的key

做一致性雜湊判斷,然後返回key落到的那個redis例項上的一個redis連線,不同的key返回的redis連線可能是不同的。

進入getShard 方法,你會發現這個實現是在Sharded類中實現的(看上面的類圖可以發現頂層的Sharded類),程式碼如下:

public R getShard(String key) {
    return resources.get(getShardInfo(key));
  }

  public S getShardInfo(byte[] key) {
    SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
    if (tail.isEmpty()) {
      return nodes.get(nodes.firstKey());
    }
    return tail.get(tail.firstKey());
  }

  public S getShardInfo(String key) {
    return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
  }

上面的方法是層層呼叫的關係,在這邊不細說,我們主要看下第二個方法(getShardInfo(byte[] key))實現(上面的nodes變數是一個TreeMap 型別, algo Hashing型別,即key分片所使用的hash演算法,這個在前一篇有簡單說過),那麼這段程式碼的含義我們大概成猜出來了,

  1. 就是在一個TreeMap中取出大於等於key之後的部分檢視SortMap
  2. 在SortMap取得第一個鍵值對的值,然後返回一個 物件,
  3. 然後根據這個物件,去resources(resources = new LinkedHashMap<ShardInfo<R>, R>())
    中get一個R物件

那麼這個S R物件各自代表什麼呢?看下面的程式碼

public class Sharded<R, S extends ShardInfo<R>>
public class BinaryShardedJedis extends Sharded<Jedis, JedisShardInfo> implements
    BinaryJedisCommands

可以得出  S = JedisShardInfo, R = Jedis 物件,即在TreeMap儲存了伺服器劃分的虛擬節點的資訊,LinkedHashMap中儲存了伺服器的物理連線。 

JedisShardInfo具體資訊如下:裡面包含了jedis伺服器的一些資訊,最重要的是它的父類中有一個weight欄位,作為本jedis伺服器的權值。

ok,那我們瞭解了實際上就是根據jedis伺服器的資訊去獲取一個jedis的連線,返回給上層呼叫。

我們可以梳理下這個邏輯:

  1. 當我們使用ShardedJedis去查一個key時,首先它會把這個key進行一個hash演算法
  2. 根據這個hash值然後去treeMap中,查出這個key落在哪個例項中,並返回redis例項對應的具體資訊
  3. 根據這個redis的例項資訊,到一個儲存jedis連結和例項資訊對應關係的LinkedHashMap中找到這個jedis連線
  4. 最終返回jedis連線,執行物件的命令操作(到這步後實際上和單機操作一樣了)

那麼我們的nodes 伺服器虛擬節點和resources 伺服器物理連線是什麼時候初始化的呢,接下來繼續看

2.redis虛擬節點(nodes)和物理連線(resources) 初始化

我們繼續看Sharded的構造方法

public Sharded(List<S> shards, Hashing algo) {
    this.algo = algo;
    initialize(shards);
  }

public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
    this.algo = algo;
    this.tagPattern = tagPattern;
    initialize(shards);
  }

這邊有一個initialize方法,就是用來對虛擬節點和物理連線進行初始化的,看其實現

private void initialize(List<S> shards) {
    nodes = new TreeMap<Long, S>();

    for (int i = 0; i != shards.size(); ++i) {
      final S shardInfo = shards.get(i);
      if (shardInfo.getName() == null) for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
        nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
      }
      else for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
        nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);
      }
      resources.put(shardInfo, shardInfo.createResource());
    }
  }

具體細節就不說了,根據上面的,我們就知道是在這邊進行了初始化,將每臺伺服器節點採用hash演算法劃分為160個虛擬節點(可以配置劃分權重),儲存在TreeMap中,

然後把每臺伺服器節點的資訊和物理連線以鍵值對儲存LinkedHashMap中。

然後通過一系列的查詢,發現Sharded的構造方法其實是在我們 jedisPool.getResource() 時就完成的

  //初始化ShardedJedisPool
        List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2, shardInfo3);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
        ShardedJedis jedis = jedisPool.getResource();

納尼? 每次jedisPool.getResource() 才初始化?那會不會造成很慢呢,其實不用擔心,其底層是使用了commons.pool來進行連線池的一些操作,會根據我們配置的連線池引數來生成對應的連線並儲存,其中的細節很複雜,博主沒有繼續深究,實現其實和資料庫連線池是一致的。

3.總結

ShardedJedis分散式具體的的實現思路:

  • redis伺服器節點劃分:將每臺伺服器節點採用hash演算法劃分為160個虛擬節點(可以配置劃分權重)

  • 將劃分虛擬節點採用TreeMap儲存

  • 對每個redis伺服器的物理連線採用LinkedHashMap儲存

  • 對Key or KeyTag 採用同樣的hash演算法,然後從TreeMap獲取大於等於鍵hash值得節點,取最鄰近節點儲存;當key的hash值大於虛擬節點hash值得最大值時,存入第一個虛擬節點

  • sharded採用的hash演算法:MD5 和 MurmurHash兩種;預設採用64位的MurmurHash演算法