Golang中实现并发控制更新map的更好方法
Golang中实现并发控制更新map的更好方法
你好,如果有一个键值对类似 (IP string, info struct{data string}) 的映射,我需要频繁地更新每个 IP 对应的 data,并且时不时地还需要添加/删除 IP。那么,除了读写锁之外,对于这个映射的更新操作,有什么更好的并发控制方式吗?因为使用读写锁的话,每次更新单个 IP 的数据都会锁住整个映射,谢谢!
或许可以使用 sync.Map (sync package - sync - Go Packages)。
sync.Map 会为你处理所有那些锁定(或原子操作)——因此无需手动加锁。
这可能有些大材小用,但你可以使用一个内存缓存解决方案,例如:
GitHub - allegro/bigcache: Efficient cache for gigabytes of data written in…
Efficient cache for gigabytes of data written in Go.
你也可以从他们关于构建它的博客文章中获取灵感:
Writing a very fast cache service with millions of entries in Go
Recently our team has been tasked to write a very fast cache service. The goal was pretty clear but possible to achieve in many ways. Finally we decided to try something new and implement the service in Go. We have described how we did it and what…
根据文档:
sync.Map类型是特化的。大多数代码应该使用普通的 Go map,并配合独立的锁或协调机制,以获得更好的类型安全性,并更容易在维护 map 内容的同时维护其他不变量。
sync.Map类型针对两种常见用例进行了优化:(1) 当给定键的条目只写入一次但读取多次时,例如只增长的缓存;或者 (2) 当多个 goroutine 读取、写入和覆盖互不相交的键集合的条目时。在这两种情况下,与配合独立的sync.Mutex或sync.RWMutex使用的 Go map 相比,使用sync.Map可以显著减少锁竞争。
因此,在大多数情况下,使用配合 RWMutex 的 map 可能是最直接、最简单的解决方案。只有当这种方式的性能无法满足你的需求时,你才应该寻找适合你特殊情况的解决方案(甚至可能是一个内存数据库)。
在Golang中处理并发map更新,除了sync.RWMutex外,推荐使用sync.Map或分片锁(sharded locking)。这里重点展示sync.Map,它针对并发读写场景进行了优化,特别适合你的使用模式(频繁更新、偶尔增删)。
sync.Map提供了几个关键方法:
Load(key): 读取Store(key, value): 写入/更新LoadOrStore(key, value): 读取或写入Delete(key): 删除Range(func(key, value)): 遍历
下面是一个示例,展示如何使用sync.Map来管理IP到info的映射:
package main
import (
"fmt"
"sync"
"time"
)
type Info struct {
Data string
}
func main() {
var ipMap sync.Map
// 并发写入/更新
for i := 0; i < 10; i++ {
go func(id int) {
ip := fmt.Sprintf("192.168.1.%d", id)
info := &Info{Data: fmt.Sprintf("data-%d", id)}
ipMap.Store(ip, info)
}(i)
}
// 并发读取
for i := 0; i < 10; i++ {
go func(id int) {
ip := fmt.Sprintf("192.168.1.%d", id)
if val, ok := ipMap.Load(ip); ok {
info := val.(*Info)
fmt.Printf("Loaded: %s -> %s\n", ip, info.Data)
}
}(i)
}
// 更新特定IP的数据
go func() {
ip := "192.168.1.1"
newInfo := &Info{Data: "updated-data"}
ipMap.Store(ip, newInfo)
}()
// 删除特定IP
go func() {
ip := "192.168.1.2"
ipMap.Delete(ip)
}()
// 使用LoadOrStore(存在则读取,不存在则写入)
go func() {
ip := "192.168.1.3"
existing, loaded := ipMap.LoadOrStore(ip, &Info{Data: "default-data"})
if loaded {
fmt.Printf("Already existed: %v\n", existing)
} else {
fmt.Printf("Stored new: %v\n", existing)
}
}()
// 遍历所有条目
go func() {
ipMap.Range(func(key, value interface{}) bool {
fmt.Printf("Range: %v -> %v\n", key, value.(*Info).Data)
return true // 返回true继续遍历,false停止
})
}()
time.Sleep(1 * time.Second)
}
如果你的场景中键(IP)的数量非常大,且更新非常频繁,也可以考虑分片锁的方案。这里提供一个简单的分片map实现:
package main
import (
"fmt"
"hash/fnv"
"sync"
)
type ShardedMap struct {
shards []*shard
count int
}
type shard struct {
data map[string]*Info
mu sync.RWMutex
}
func NewShardedMap(shardCount int) *ShardedMap {
shards := make([]*shard, shardCount)
for i := 0; i < shardCount; i++ {
shards[i] = &shard{
data: make(map[string]*Info),
}
}
return &ShardedMap{
shards: shards,
count: shardCount,
}
}
func (m *ShardedMap) getShard(key string) *shard {
h := fnv.New32a()
h.Write([]byte(key))
return m.shards[h.Sum32()%uint32(m.count)]
}
func (m *ShardedMap) Store(key string, value *Info) {
shard := m.getShard(key)
shard.mu.Lock()
shard.data[key] = value
shard.mu.Unlock()
}
func (m *ShardedMap) Load(key string) (*Info, bool) {
shard := m.getShard(key)
shard.mu.RLock()
val, ok := shard.data[key]
shard.mu.RUnlock()
return val, ok
}
func (m *ShardedMap) Delete(key string) {
shard := m.getShard(key)
shard.mu.Lock()
delete(shard.data, key)
shard.mu.Unlock()
}
// 使用示例
func main() {
sm := NewShardedMap(16) // 16个分片
// 并发操作
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("192.168.1.%d", id)
sm.Store(key, &Info{Data: fmt.Sprintf("data-%d", id)})
}(i)
}
wg.Wait()
// 读取
if val, ok := sm.Load("192.168.1.1"); ok {
fmt.Printf("Loaded: %v\n", val.Data)
}
}
sync.Map更适合读多写少、键值对数量相对稳定的场景;分片锁在写非常频繁、键值对数量大的情况下性能更好。根据你的具体需求选择即可。

