一致性哈希算法 CARP 原理解析, 附 Golang 實現

一致性哈希算法 CARP 原理解析, 附 Golang 實現

在後端服務開發的過程當中, 遇到了這樣一個問題: 須要在 mysql 前面部署 redis 作一層緩存, 要求 redis 是集羣部署, 而且每臺 redis 節點只緩存總數據量的 1/N, N 爲 redis 的個數.mysql

看到這裏你們都能想到到一個方法是使用 hash(key)%N 來選取 redis 進行 value 的存取, 這種方式固然能夠很均勻的將數據分配到 N 個 redis 服務上, 而且實現起來也很是的簡單. 可是使用這種哈希取餘的方式有一個很大的問題, 那就是當 redis 集羣擴容或者縮容, 或者發生宕機的時候, 也就是上述公式中的 N 發生變化的時候, 這個時候 hash(key)%N 的值保持不變的機率很是小, 換句話說, 緩存系統會在這種狀況下整個失效, 這對於後端的 mysql 來講瞬時壓力會很是大, 極可能形成 mysql 的奔潰, 進而形成整個服務的不可用.redis

因此必須想一種辦法來應對上述的狀況, 即當一臺 redis 服務掛掉的時候, 能不能作到只有 1/N 的緩存失效呢?算法

答案就是使用一致性哈希算法 CARP, 嚴格來說 CARP 並非一種算法, 而是一種協議, Cache Array Routing Protocol,Cache 羣組路由協議. 下面來介紹些下它的工做原理:sql

首先假設有 N 個 redis 服務, 分別是 redis1, redis2 .... redisN, key 是想要獲取的數據在 redis 中的鍵. 後端

第一步, 計算全部的服務名與 key 的哈希值:數組

hash_v1 = hash(redis1 + key)
hash_v2 = hash(redis2 + key)
...
hash_vN = hash(redisN + key)

第二步, 計算值最大的 hash_vX, 那麼選中的即是 redisX:緩存

hash_vX = max(hash_v1, hash_v2, ..., hash_vN)

爲何這麼作就能夠達到增長一臺 redis 服務的時候, 只有 1/(N+1) 的緩存失效呢?函數

hash_vX1 = max(hash_v1, hash_v2, ..., hash_vN)
hash_vX2 = max(hash_v1, hash_v2, ..., hash_vN, hash_vN+1)

看上面兩個表達式, 若是要求 hash_vX2 大於 hash_vX1, 那麼必須 hash_vN+1 大於前面 N 個哈希值, 那麼你選取的 hash 函數足夠散列的話, hash_vN+1 大於前面 N 個哈希值的機率爲 1/(N+1). 你能夠自行算一下減小一臺 redis 服務時, 緩存失效的機率.性能

最後, 附上 Golang 實現版本:code

package carp

import (
    "crypto/md5"
    "errors"
)

var (
    ErrHaveNoRedis = errors.New("have no redis")
)

// 根據 carp 算法, 從 redisArr 的數組中選擇合適的 redis, 返回值爲下標
func GetTargetIndex(key string, redisArr []string) (idx int, err error) {
    if len(redisArr) < 1 {
        return -1, ErrHaveNoTarget
    } else if len(redisArr) == 1 {
        return 0, nil
    }

    hashArr := make([]string, len(redisArr))
    for k, v := range redisArr {
        hashArr[k] = hashString(v + key)
    }
    idx = minIdx(hashArr)
    return
}

// 返回 arr 數組中最小值的下標
func minIdx(arr []string) (idx int) {
    if len(arr) < 1 {
        return -1
    }

    idx, min := 0, arr[0]

    for k, v := range arr {
        if v < min {
            idx = k
            min = v
        }
    }
    return
}

func hashString(str string) (hash string) {
    md5sum := md5.Sum([]byte(str))
    return string(md5sum[:])
}

上面的代碼中使用了 md5 的哈希算法, 若是你對性能有更高的要求可使用 FNV 哈希算法.

相關文章
相關標籤/搜索