Golang sync.Map
sync.Map
是 Go 语言标准库 sync
包中提供的一个并发安全的 map。它并非为了替代 map + sync.RWMutex
这种通用模式,而是针对**“读多写少”**的特定场景进行了深度优化。
0.1 一、 核心设计思想
sync.Map 的设计哲学是读写分离和空间换时间,其最终目的是:
让读操作尽可能快,甚至做到在大部分情况下无锁(lock-free)。
为了实现这个目标,它牺牲了写操作的性能,并使用了比普通 map 更复杂的内部结构。
0.2 二、 核心数据结构
sync.Map
的内部实现主要围绕两个核心的 map 结构和一个锁:
-
read
(只读 map)-
这是一个
atomic.Pointer
,指向一个内部的readOnly
结构体。readOnly
结构体里包含一个普通的 Gomap
。 -
这个
read
map 存储了 map 中被认为是 “稳定” 的数据 。 -
对
read
map 的访问是原子操作,因此读取是并发安全的,并且不需要加锁。这是sync.Map
高性能读取的关键。
-
-
dirty
(可写 map)-
这是一个普通的 Go
map
(map[any]any
)。 -
它存储了最近新增或被修改的键值对。可以把它看作是新数据的“暂存区”或“缓存”。
-
对
dirty
map 的所有访问都必须由一个互斥锁mu
来保护。
-
-
mu
(互斥锁sync.Mutex
)- 这个锁只用于保护
dirty
map 的并发访问。它不保护read
map。
- 这个锁只用于保护
-
misses
(未命中计数器)- 用于记录
Load
操作在read
map 中未命中、不得不去查询dirty
map 的次数。当misses
的数量达到一定阈值(等于dirty
map 的长度)时,就会触发一次数据从dirty
到read
的迁移。
- 用于记录
0.3 三、 结构图
graph TD subgraph sm [sync.Map 实例] mu["mu (sync.Mutex)"] read_ptr["read (atomic.Pointer)"] dirty_map["dirty (map[any]any)"] misses["misses (int)"] end subgraph ro [readOnly 结构] m["m (map[any]*entry)"] amended["amended (bool)"] end subgraph dirty_map_protected["受 mu 锁保护"] dirty_map end mu --- dirty_map_protected read_ptr -- "原子加载/存储" --> ro style sm fill:#f9f,stroke:#333,stroke-width:2px style ro fill:#ccf,stroke:#333,stroke-width:2px
-
sync.Map 实例: 包含一个锁
mu
,一个原子指针read
,一个普通的 mapdirty
和一个计数器misses
。 -
mu (sync.Mutex): 它的保护范围仅限于
dirty
map。 -
read (atomic.Pointer): 指向一个
readOnly
结构。所有对read
的读写都通过原子操作完成,保证并发安全。 -
readOnly 结构: 包含一个 map
m
,这是读操作的快速路径。amended
标志位表示dirty
map 中是否包含read
map 中没有的数据。 -
dirty map: 是一个常规的 map,存储最新的写入,访问它必须先获取
mu
锁。
0.4 四、 关键操作流程
0.4.1 A. Load (读取操作) - 性能核心
这是 sync.Map
被优化的主要路径。
-
快速路径(无锁):
-
通过原子操作加载
read
指针,获取readOnly
map。 -
在
readOnly.m
中查找 key。 -
如果找到了,并且其值不是“已删除”状态,直接返回。这个过程完全无锁,非常快。
-
-
慢速路径(加锁):
-
如果在
read
中没找到,说明 key 可能是新写入的,存在于dirty
中。 -
加锁
mu
,以安全地访问dirty
map。 -
再次检查
read
map(因为在加锁的瞬间,dirty
可能已经被提升为了新的read
map),防止数据不一致。 -
在
dirty
map 中查找 key,如果找到则返回。 -
无论是否在
dirty
中找到,都将misses
计数器加一。 -
检查
misses
是否达到了dirty
map 的长度,如果是,则触发一次数据迁移。 -
解锁
mu
。
-
0.4.2 B. Store (写入操作)
写入操作通常是慢路径,因为它很可能需要加锁。
-
快速检查:
-
先不加锁,原子加载
read
map,并检查 key 是否存在。 -
如果 key 存在,尝试通过原子操作 (CAS) 直接更新
read
map 中该 key 对应的 entry 的值。如果成功,操作结束。
-
-
慢速路径(加锁):
-
如果上述快速更新不成功(例如 key 不在
read
中,或 CAS 操作失败),则加锁mu
。 -
再次检查
read
map,因为可能在加锁期间发生了变化。 -
如果 key 在
read
中,但被标记为“已删除”,则需要将其“复活”并存入dirty
。 -
如果 key 不在
read
中,则直接将新的键值对存入dirty
map。 -
如果
dirty
map 为nil
,则会根据read
map 的内容创建一个新的dirty
map,并将新数据存入。 -
解锁
mu
。
-
0.4.3 C. Delete (删除操作)
删除操作总是慢路径,需要加锁。它会将键值对从 dirty
map 中删除,并在 read
map 中将对应的条目标记为“已删除”(expunged),这是一个逻辑删除,而不是物理删除。
0.4.4 D. 数据迁移 (dirty
-> read
)
当 Load
操作在 read
中未命中,且 misses
计数器增长到等于 dirty
map 的大小时,会触发这个过程:
-
加锁
mu
。 -
将
dirty
map 中的所有数据(包括read
中已有的但被更新的数据)合并到一个新的 map 中,这个新 map 将成为新的readOnly.m
。 -
通过原子操作,将
sync.Map
的read
指针指向这个包含全新数据的readOnly
结构。 -
将
dirty
map 清空(或设为nil
),并将misses
计数器重置为0。 -
解锁
mu
。
0.5 五、 总结与对比
特性 | 手动加锁 (map + sync.RWMutex ) |
sync.Map |
---|---|---|
核心机制 | 单一锁(读写锁)保护整个 map | 两个 map(read /dirty ),读大多无锁,写加锁 |
最佳使用场景 | 写多读少,或读写均衡的场景;或map创建后不经常修改的场景 | 读多写少,特别是“一次写入,多次读取”的场景 (如缓存) |
性能特点 | 读和写操作都会有锁的开销,高并发读写时锁竞争激烈 | 键稳定存在时,读操作极快(接近原子操作);写操作相对较慢 |
锁竞争与扩展性 | 整个 map 共享一把锁,高并发下所有操作都可能互相阻塞,扩展性受限 | 读操作无锁竞争,写操作有锁,但读写分离,显著降低了读操作的竞争,在高并发读场景下扩展性好 |
API 与易用性 | 开发者需手动管理锁,但 API 就是普通 map 的 API | 封装好的 API (Load , Store , Delete , Range ),无需手动管锁,但 API 功能受限(如无法直接获取长度) |
类型安全 | 编译时类型安全(map[string]int ) |
运行时类型断言(key , value 都是 any 类型) |