sync.Map 适用场景与误用¶
30 秒版(开场)¶
sync.Map主要优化两类负载:key 只写一次后读很多次,或不同 goroutine 访问彼此不相交的 key 集合。它不是“所有读多写少都更快”,也不提供一致快照;与map+Mutex/RWMutex必须按真实负载 benchmark。
3 分钟版(精讲深度)¶
- 是什么:并发安全的
map[any]any;API:Load/Store/LoadOrStore/Delete/Range。 - 为什么:普通 map+Mutex 读也互斥;sync.Map 无锁读路径(命中 read-only)。
- 怎么做:miss 时加锁晋升 dirty;删除用 tombstone;Range 快照不一致迭代。
10 分钟版(原理 + 图示)¶
flowchart LR
Load[Load key] --> Hit{read 命中?}
Hit -->|是| Fast[无锁返回]
Hit -->|否| Slow[锁 dirty 查找]
Store -->|新 key 多| DirtyGrow[dirty 膨胀]
适合
- 配置/元数据缓存,写入一次读多次
- 每个 key 只写一次(如连接表
connID -> Conn) - 多 goroutine 并发读写不相交的 key,减少锁竞争
不适合
- 高频
Store同一 key(如计数) - 需要
len、强一致快照 - key 类型需自行约束(any + 断言)
替代:map[K]V + RWMutex;或 分片 map(shard = hash(key)%N)降低锁竞争。
生产场景¶
- WebSocket 连接表:连接建立 Store,断开 Delete,广播 Range(注意 Range 回调勿阻塞)。
- 误用:QPS 统计每请求 Store → 性能劣于 Mutex+map 或 atomic。
- 可观测:mutex profile 热点在
sync.(*Map).Store。
排查与工具¶
- benchmark 对比
ShardedMapvssync.Map -race若混用普通 map 仍会报
架构取舍¶
| 方案 | 条件 |
|---|---|
| sync.Map | 读极多、写少、key 集合稳定 |
| RWMutex+map | 需遍历一致性、中等竞争 |
| 分片锁 map | 高并发读写、key 均匀 |
| 外部缓存 Redis | 跨实例 |
深挖问答¶
- Range 回调能修改同一个 Map 吗? → 可以,文档明确允许回调调用任意 Map 方法;但 Range 不是一致快照,当前 key 可能反映遍历期间任一时刻的映射。
- 类型安全? → 否,用泛型封装或
xsync.MapOf(第三方)。 - 与 map+sync.RWMutex 性能? → 因负载而异,必须 benchmark。
- nil value? → 可存;Load 需区分不存在与零值(用 ok)。
- 能用 LoadOrStore 代替 singleflight 吗? → 不能完整替代。它只原子决定哪个 value 被保存;多个调用者仍可能提前重复执行昂贵初始化,也不负责共享 error/结果生命周期。
反模式与事故¶
- 把 sync.Map 当 通用并发 map 全局替换。
- Range 回调里做 RPC → 当前遍历长时间不返回;虽不阻塞其他 Map 方法,仍会拖住调用方并放大资源占用。
- value 存指针,外部无拷贝修改 → 数据竞态。
代码示例¶
var sessions sync.Map // key: sessionID string, val: *Session
func GetSession(id string) (*Session, bool) {
v, ok := sessions.Load(id)
if !ok {
return nil, false
}
return v.(*Session), true
}
并发原语基础见 basis/sync/main.go。