假設你有10000個併發請求,同時請求單臺redis
(又是redis
:p ),此時redis
是處理不了這麼多併發請求的。git
一個比較簡單的想法就是對於系統進行橫向擴展(也就是加機器),而且對於一些讀寫請求進行hash
路由。github
好比,目前你有四臺redis
服務器[0,1,2,3],此時client發起請求set("name","tom")
golang
此時的路由變成redis
1.crc32('name') % 4 // 先找到寫哪臺redis
2.set("name","tom") // 真實的寫操做
複製代碼
這樣看起來的確提高了系統的可用性,可是假設業務量暴漲,4臺redis
也處理不過來了,那麼咱們此時的想法必定也是加機器(:p),可是加機器可能致使以前存儲在redis
上的key
失效,以加當前機器基礎上兩臺機器爲例:算法
crc32('name') % 6 != crc32('name') % 4
複製代碼
從上面的代碼能夠看出此時直接加機器的方式會致使key
失效(可能會致使緩存雪崩,或者對於一些強依賴cache
的服務,會形成部分數據丟失,服務不可用),此時就引入了一致性hash
的概念數據庫
In computer science, consistent hashing is a special kind of hashing such that when a hash table is resized, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots.數組
上述是wikipad
給出的示意,翻譯過來就是緩存
在計算機科學中,一致性hash是一種特殊的hash,這樣當調整hash表的大小時,平均只須要從新映射K/n個key,其中K是keys的數量,n是slot的數量。bash
實際理解起來仍是有點抽象,舉個例子,目前有10個key,3臺服務器,假設你加兩臺機器那麼須要變化的就是 10 / 5 = 2個key服務器
一致性Hash
算法也是使用取模的方法,只是,剛纔描述的取模法是對服務器的數量進行取模,而一致性Hash
算法是對2^32取模
如上圖是一致性hash
實現,相似圓環
實際上還有一個問題,一致性Hash算法在服務節點太少時,容易由於節點分部不均勻而形成數據傾斜
如上圖,A節點(機器)附近的key比較多,而B節點只有一個key,那麼怎麼解決這種問題呢?
虛擬節點的概念被引入了
如圖,由於引入了虛擬節點,使得key分佈的更均勻了(NODEA#1,NODEA#2
爲NODEA
的虛擬節點。
讓咱們來看一個golang consistent
庫實現 eg:https://github.com/stathat/consistent
咱們先來看下一致性hash
的結構體
type Consistent struct {
circle map[uint32]string // 存儲crc32後該key的值
members map[string]bool // 存儲鍵值
sortedHashes uints // 排序後的數組
NumberOfReplicas int // 其實是虛擬節點
count int64 // 整個結構體的數量
scratch [64]byte // 這個字段沒有用
sync.RWMutex // 讀寫鎖
}
複製代碼
初始化
func New() *Consistent {
c := new(Consistent)
c.NumberOfReplicas = 20 // 默認每一個節點虛擬節點的數量
c.circle = make(map[uint32]string) // 初始化circle
c.members = make(map[string]bool) // 初始化members
return c
}
複製代碼
新增機器
func (c *Consistent) Add(elt string) {
c.Lock()
defer c.Unlock()
// 增長互斥鎖,防止併發新增
c.add(elt)
}
func (c *Consistent) add(elt string) {
// 遍歷,增長虛節點cache
for i := 0; i < c.NumberOfReplicas; i++ {
c.circle[c.hashKey(c.eltKey(elt, i))] = elt
// output like: c.circle[1765504436] = cacheA
}
// 存儲鍵值
c.members[elt] = true
// 使hashkey有序
c.updateSortedHashes()
// 數量 + 1
c.count++
}
// 對key進行string化
func (c *Consistent) eltKey(elt string, idx int) string {
// return elt + "|" + strconv.Itoa(idx)
// if string == cacheA
/* output like 0cacheA 1cacheA 2cacheA */
return strconv.Itoa(idx) + elt
}
func (c *Consistent) hashKey(key string) uint32 {
// 若是傳進來的字符串小於64位,優化操做
if len(key) < 64 {
var scratch [64]byte
copy(scratch[:], key)
return crc32.ChecksumIEEE(scratch[:len(key)])
}
// 對於key進行crc32得出key的int值
return crc32.ChecksumIEEE([]byte(key))
}
複製代碼
查找數據接口
func (c *Consistent) Get(name string) (string, error) {
// 加讀鎖
c.RLock()
defer c.RUnlock()
// 若是c.circle沒數據,返回error
if len(c.circle) == 0 {
return "", ErrEmptyCircle
}
// 把key hash化
key := c.hashKey(name)
// 搜索key
i := c.search(key)
return c.circle[c.sortedHashes[i]], nil
}
// 查找過程
func (c *Consistent) search(key uint32) (i int) {
f := func(x int) bool {
return c.sortedHashes[x] > key
}
// sort.Search其實是個基於f()函數進行search,找到c.sortedHashes[x] > key的位置而後進行返回
i = sort.Search(len(c.sortedHashes), f)
// 若是i>數組的長度,則默認i在0號位置上
if i >= len(c.sortedHashes) {
i = 0
}
return
}
複製代碼
從一致性hash
內移除數據(機器)
func (c *Consistent) Remove(elt string) {
c.Lock()
defer c.Unlock()
// 加鎖,防止併發刪除
c.remove(elt)
}
// 移除數據
func (c *Consistent) remove(elt string) {
for i := 0; i < c.NumberOfReplicas; i++ {
// 從map裏面刪除這個元素
delete(c.circle, c.hashKey(c.eltKey(elt, i)))
}
delete(c.members, elt)
// 從新排序
c.updateSortedHashes()
// 數量 - 1
c.count--
}
複製代碼
一致性hash
在這幾款數據庫都有應用
refrence