Golang 里面 map 不是并发安全的,这一点是众所周知的,而且官方文档也很早就给了解释:Why are map operations not defined to be atomic?. 也正如这个解释说的一样,要实现一个并发安全的 map 其实非常简单。
并发安全
sync.RWMutex
type syncMap struct {
items map[string]interface{}
sync.RWMutex
}
GetSetDelete
但是这种设计会有些性能隐患。主要是两个方面:
- 读写锁的粒度太大了,保护了整个 map 的访问。写操作是阻塞的,此时其他任何读操作都无法进行。
- 如果内部的 map 存储了很多 key,GC 的时候就需要扫描很久。
「分表」
syncMapsyncMap
type SyncMap struct {
shardCount uint8
shards []*syncMap
}
*syncMapshardCountshardsmap[string]*syncMap
syncMapsyncMap
那么数据如何被分配到指定的分块呢?一种很通用也很简单的方法就是 hash. 字符串的哈希算法有很多,byvoid 大神实现和比较了多种字符串 hash 函数(各种字符串Hash函数比较),得出结论是:“BKDRHash无论是在实际效果还是编码实现中,效果都是最突出的”。这里采用了 BKDRHash 来实现:
const seed uint32 = 131 // 31 131 1313 13131 131313 etc.. func bkdrHash(str string) uint32 { var h uint32 for _, c := range str { h = h*seed + uint32(c) } return h } // Find the specific shard with the given key func (m *SyncMap) locate(key string) *syncMap { return m.shards[bkdrHash(key)&uint32((m.shardCount-1))] }
locatebkdrHashkeyindexsyncMap
GetSetDeleterange
还有一点:
如果业务场景能保证我们绝不会同时读写一个key的话也不用加锁,一个小例子试验下。。
会有冲突的情况:
package main
func main() {
Map := make(map[int]int)
for i := 0; i < 100; i++ {
go writeMap(Map, i, i)
go readMap(Map, i)
}
}
func readMap(Map map[int]int, key int) int {
return Map[key]
}
func writeMap(Map map[int]int, key int, value int) {
Map[key] = value
}
go run -race main.go 结果如下:
那么稍微改下呢。。
package main
func main() {
Map := make(map[int]int)
for i := 0; i < 2; i++ {
go writeMap(Map, i, i)
go readMap(Map, (i+1) % 2)
}
}
func readMap(Map map[int]int, key int) int {
return Map[key]
}
func writeMap(Map map[int]int, key int, value int) {
Map[key] = value
}
出现的是不冲突,但是没啥意义,因为几乎没有这样的应用场景吧,在这做这个小测试只是想说明golang 的map会出现读写冲突是因为map是引用类型,同时读写共享资源会使得共享资源崩溃,