Golang 里面 map 不是并发安全的,这一点是众所周知的,而且官方文档也很早就给了解释:Why are map operations not defined to be atomic?. 也正如这个解释说的一样,要实现一个并发安全的 map 其实非常简单。

并发安全

sync.RWMutex
type syncMap struct {
    items map[string]interface{}
    sync.RWMutex
}
GetSetDelete

但是这种设计会有些性能隐患。主要是两个方面:

  1. 读写锁的粒度太大了,保护了整个 map 的访问。写操作是阻塞的,此时其他任何读操作都无法进行。
  2. 如果内部的 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是引用类型,同时读写共享资源会使得共享资源崩溃,