在 Golang 中的 map 结构,在删除键值对的时候,并不会真正的删除,而是标记。那么随着键值对越来越多,会不会造成大量内存浪费?
首先答案是会的,很有可能导致 OOM,而且针对这个还有一个讨论:https://github.com/golang/go/issues/20135。大致的意思就是在很大的 map中,delete操作没有真正释放内存而可能导致内存 OOM。
所以一般的做法:就是 重建map。而 go-zero中内置了 safemap的容器组件。safemap在一定程度上可以避免这种情况发生。
那首先我们看看 go原生提供的 map是怎么删除的?
原生map删除
1packagemain
2
3funcmain{
4m := make( map[ int] string, 9)
5m[ 1] = "hello"
6m[ 2] = "world"
7m[ 3] = "go"
8
9v, ok := m[ 1]
10_, _ = fn(v, ok)
11
12delete(m, 1)
13}
14
15funcfn(v string, ok bool) ( string, bool) {
16returnv, ok
17}
测试代码如上,我们可以通过 go tool compile -S -N -l testmap.go | grep "CALL":
0x007100113(test/testmap.go:4)CALLruntime.makemap(SB)
0x009900153(test/testmap.go:5)CALLruntime.mapassign_fast64(SB)
0x00ea00234(test/testmap.go:6)CALLruntime.mapassign_fast64(SB)
0x013b00315(test/testmap.go:7)CALLruntime.mapassign_fast64(SB)
0x019400404(test/testmap.go:9)CALLruntime.mapaccess2_fast64(SB)
0x01f100497(test/testmap.go:10)CALL"".fn(SB)
0x021400532(test/testmap.go:12)CALLruntime.mapdelete_fast64(SB)
0x023000560(test/testmap.go:7)CALLruntime.gcWriteBarrier(SB)
0x024100577(test/testmap.go:6)CALLruntime.gcWriteBarrier(SB)
0x025200594(test/testmap.go:5)CALLruntime.gcWriteBarrier(SB)
0x025c00604(test/testmap.go:3)CALLruntime.morestack_noctxt(SB)
执行第12行的 delete,实际执行的是 runtime.mapdelete_fast64。
这些函数的参数类型是具体的 int64,mapdelete_fast64跟原始的 delete操作一样的,所以我们来看看 mapdelete。
mapdelete
长图预警!!!
大致代码分析如上,具体代码就留给大家去阅读了。其实大致过程:
- 写保护,防止并发写
- 查询要删除的 key 是否存在
- 存在则对其标志做删除标记
- count--
所以你在大面积删除 key,实际 map存储的 key是不会删除的,只是标记当前的key状态为 empty。
其实出发点,和 mysql的标记删除类似,防止后续会有相同的 key插入,省去了扩缩容的操作。
但是这个对有些场景是不妥的,如果开发者在未来时间内都不会再插入相同的 key,很可能会导致 OOM。
所以针对以上情况,go-zero开发了 safemap。下面我们看看 safemap是如何避免这个问题的?
safemap
直接从操作 safemap中分析为什么要这么设计:
- 预设一个 删除阈值 ,如果触发会放到一个新预设好的 newmap 中
- 两个 map 是一个整体,所以 key 只能留一份
所以为什么要设置两个 map就很清楚了:
- dirtyOld 作为存储主体,如果 delete 操作达到阈值,则会触发迁移。
- dirtyNew 作为暂存体,会在到达阈值时,存放部分 key/value
所以在迁移操作时,我们需要做的就是: 将原先的 dirtyOld 清空,存储的 key/value 通过 for-range 重新存储到 dirtyNew ,然后将 dirtyNew 指向 dirtyOld 。
> 可能会有疑问: 不是说 key/value 没有删除吗,只是标记了 tophash=empty > > 其实在 for-range过程中,会过滤掉 tophash <= emptyOne的 key
这样就实现了不需要的 key 不会被加入到 dirtyNew,进而不会影响 dirtyOld。
这其实也就是垃圾回收的年老代和新生代的概念。
更多实现细节,可以查看源码!
项目地址
https://github.com/tal-tech/go-zero
https://gitee.com/kevwan/go-zero
欢迎使用 go-zero 并 star支持我们!
END
如何有效表白