一、按照指定順序遍歷map
map按key順序獲取value
package main import ( "fmt" "sort" ) func main() { m := make(map[string]int, 5) fmt.Printf("%T[%p](%d): %v\n", m, &m, len(m), m) m["wang"] = 1 m["wangq"] = 2 m["wangh"] = 3 m["hua"] = 4 m["huaq"] = 5 fmt.Printf("%T[%p](%d): %v\n", m, &m, len(m), m) m["huaw"] = 6 m["qing"] = 7 m["qingw"] = 8 fmt.Printf("%T[%p](%d): %v\n", m, &m, len(m), m) mlen := len(m) ss := make([]string, 0, mlen) for k := range m { ss = append(ss, k) } sort.Strings(ss) sort.Sort(sort.Reverse(sort.StringSlice(ss))) // 反序 fmt.Println(ss) for _, v := range ss { fmt.Printf("%s:%d ", v, m[v]) } } ////// map[string]int[0xc000136018](0): map[] map[string]int[0xc000136018](5): map[hua:4 huaq:5 wang:1 wangh:3 wangq:2] map[string]int[0xc000136018](8): map[hua:4 huaq:5 huaw:6 qing:7 qingw:8 wang:1 wangh:3 wangq:2] [wangq wangh wang qingw qing huaw huaq hua] wangq:2 wangh:3 wang:1 qingw:8 qing:7 huaw:6 huaq:5 hua:4
注:map() make時大小無關,An empty map is allocated with enough space to hold the specified number of elements. The size may be omitted, in which case a small starting size is allocated.
二、map實現原理
map的源碼位於 src/runtime/map.go中 筆者go的版本是1.12在go中,map同樣也是數組存儲的的,每個數組下標處存儲的是一個bucket,這個bucket的類型見下面代碼,每個bucket中可以存儲8個kv鍵值對,當每個bucket存儲的kv對到達8個之后,會通過overflow指針指向一個新的bucket,從而形成一個鏈表,看bmap的結構,我想大家應該很納悶,沒看見kv的結構和overflow指針啊,事實上,這兩個結構體並沒有顯示定義,是通過指針運算進行訪問的。
//bucket結構體定義 b就是bucket type bmap{ // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. //翻譯:top hash通常包含該bucket中每個鍵的hash值的高八位。 // 如果tophash[0]小於mintophash,則tophash[0]為桶疏散狀態 //bucketCnt 的初始值是8 tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt values. // NOTE: packing all the keys together and then all the values together makes the // code a bit more complicated than alternating key/value/key/value/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer. //翻譯:接下來是bucketcnt鍵,然后是bucketcnt值。 //注意:將所有鍵打包在一起,然后將所有值打包在一起, //使得代碼比交替鍵/值/鍵/值/更復雜。但它允許//我們消除可能需要的填充, //例如map[int64]int8./后面跟一個溢出指針 }
看上面代碼以及注釋,我們能得到bucket中存儲的kv是這樣的,tophash用來快速查找key值是否在該bucket中,而不同每次都通過真值進行比較;還有kv的存放,為什么不是k1v1,k2v2..... 而是k1k2...v1v2...,我們看上面的注釋說的 map[int64]int8,key是int64(8個字節),value是int8(一個字節),kv的長度不同,如果按照kv格式存放,則考慮內存對齊v也會占用int64,而按照后者存儲時,8個v剛好占用一個int64,從這個就可以看出go的map設計之巧妙。
最后我們分析一下go的整體內存結構,閱讀一下map存儲的源碼,如下圖所示,當往map中存儲一個kv對時,通過k獲取hash值,hash值的低八位和bucket數組長度取余,定位到在數組中的那個下標,hash值的高八位存儲在bucket中的tophash中,用來快速判斷key是否存在,key和value的具體值則通過指針運算存儲,當一個bucket滿時,通過overfolw指針鏈接到下一個bucket。
三、一致性hash
參考:動手寫分布式緩存 - GeeCache第四天 一致性哈希(hash)
3.1 算法原理
一致性哈希算法將 key 映射到 2^32 的空間中,將這個數字首尾相連,形成一個環。
- 計算節點/機器(通常使用節點的名稱、編號和 IP 地址)的哈希值,放置在環上。
- 計算 key 的哈希值,放置在環上,順時針尋找到的第一個節點,就是應選取的節點/機器。
環上有 peer2,peer4,peer6 三個節點,key11
,key2
,key27
均映射到 peer2,key23
映射到 peer4。此時,如果新增節點/機器 peer8,假設它新增位置如圖所示,那么只有 key27
從 peer2 調整到 peer8,其余的映射均沒有發生改變。
也就是說,一致性哈希算法,在新增/刪除節點時,只需要重新定位該節點附近的一小部分數據,而不需要重新定位所有的節點,這就解決了上述的問題。
3.2 數據傾斜問題
如果服務器的節點過少,容易引起 key 的傾斜。例如上面例子中的 peer2,peer4,peer6 分布在環的上半部分,下半部分是空的。那么映射到環下半部分的 key 都會被分配給 peer2,key 過度向 peer2 傾斜,緩存節點間負載不均。
為了解決這個問題,引入了虛擬節點的概念,一個真實節點對應多個虛擬節點。
假設 1 個真實節點對應 3 個虛擬節點,那么 peer1 對應的虛擬節點是 peer1-1、 peer1-2、 peer1-3(通常以添加編號的方式實現),其余節點也以相同的方式操作。
- 第一步,計算虛擬節點的 Hash 值,放置在環上。
- 第二步,計算 key 的 Hash 值,在環上順時針尋找到應選取的虛擬節點,例如是 peer2-1,那么就對應真實節點 peer2。
虛擬節點擴充了節點的數量,解決了節點較少的情況下數據容易傾斜的問題。而且代價非常小,只需要增加一個字典(map)維護真實節點與虛擬節點的映射關系即可。
package consistenthash import ( "hash/crc32" "sort" "strconv" ) // Hash maps bytes to uint32 type Hash func(data []byte) uint32 // Map constains all hashed keys type Map struct { hash Hash replicas int keys []int // Sorted hashMap map[int]string } // New creates a Map instance func New(replicas int, fn Hash) *Map { m := &Map{ replicas: replicas, hash: fn, hashMap: make(map[int]string), } if m.hash == nil { m.hash = crc32.ChecksumIEEE } return m } // Add adds some keys to the hash. func (m *Map) Add(keys ...string) { for _, key := range keys { for i := 0; i < m.replicas; i++ { hash := int(m.hash([]byte(strconv.Itoa(i) + key))) m.keys = append(m.keys, hash) m.hashMap[hash] = key } } sort.Ints(m.keys) } // Get gets the closest item in the hash to the provided key. func (m *Map) Get(key string) string { if len(m.keys) == 0 { return "" } hash := int(m.hash([]byte(key))) // Binary search for appropriate replica. idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash }) return m.hashMap[m.keys[idx%len(m.keys)]] }
參考:
1. map實現原理 topgoer