map基本的结构及查找、写入、删除K/V值的具体分析见 上一节
扩容总流程
- 在
mapassign
函数写入K/V值后进行扩容条件判断:
(1)负载因子大于6.5,也就是元素总数 / 总桶数 > 6.5时,触发翻倍扩容
(2)溢出桶过多,一般发生于持续写入数据又全部删除时,触发等量扩容 hashGrow
函数负责创建新桶,分配内存空间,注意此时并没有进行旧桶的数据迁移,而是把迁移操作分散到每一次写入和删除中,也就是渐进式rehash,防止迁移大量数据时导致的查询服务停止,保证哈希表性能的稳定性。- 在扩容状态中,每一次
mapassign
函数写入和mapdelete
函数删除操作时都会触发两次旧桶的数据迁移
(1)当前写入或删除key值对应的旧桶
(2)nevacuate计数值对应的旧桶,每次数据迁移后nevacuate+1,如果和上面的旧桶相同则更新迁移进度,nevacuate == 旧桶总数时结束扩容状态。 evacuate
函数负责对旧桶数据进行迁移,翻倍扩容时旧桶数据会分流到新桶中不同的编号桶中。
map基本结构及hash值查找流程

bmap基本结构如下:
hash值定位K/V值位置如下:

扩容触发条件
在mapassign
函数每次写入K/V数据后都会进行触发扩容条件的检测:
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h) //进行扩容操作,创建新桶并让h.buckets指向新桶内存地址
goto again // Growing the table invalidates everything, so try again
}
包括以下两个方面的检测:
- 负载因子大于6.5,也就是元素总数 / 总桶数 > 6.5时,触发翻倍扩容
- 溢出桶过多,一般发生于持续写入数据又全部删除时,触发等量扩容
overLoadFactor
函数检测负载因子:
loadFactorNum = 13
loadFactorDen = 2
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits //等价于8
func bucketShift(b uint8) uintptr {
return uintptr(1) << (b & (sys.PtrSize*8 - 1)) //就是返回2的b次方数值
}
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
//等价于:count > 8 && count > 13*(2^B / 2)
//把位置换下,count/2^B > 6.5 6.5也就是哈希表的负载因子
}
tooManyOverflowBuckets
函数检测溢出桶数量
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B > 15 {
B = 15
}
//判断溢出桶是否太多,当桶总数 < 2 ^ 15 时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多。
//当桶总数 >= 2 ^ 15 时,直接与 2 ^ 15 比较,当溢出桶总数 >= 2 ^ 15 时,即认为溢出桶太多了。
return noverflow >= uint16(1)<<(B&15)
}
新桶创建(hashGrow)
hashGrow
函数负责扩容操作,该函数创建新桶并让h.buckets指向新桶内存地址,注意此时并没有进行数据迁移的工作。
func hashGrow(t *maptype, h *hmap) {
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
//溢出桶太多造成扩容的情况
bigger = 0 //等量扩容,下面h.B += 0
h.flags |= sameSizeGrow //等量扩容标志位
}
oldbuckets := h.buckets //oldbuckets保存当前桶地址
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil) //为新桶申请一段连续的内存空间
flags := h.flags