在後端服務開發的過程當中, 遇到了這樣一個問題: 須要在 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 哈希算法.