Go语言sync.Map的read与dirty双map结构设计
在Go语言中,并发安全的数据结构一直是开发者关注的问题。sync.Map作为标准库提供的并发安全map实现,其核心创新在于read与dirty双map结构设计。这种设计巧妙地平衡了读多写少场景下的性能问题。
1. 背景与问题
在Go 1.9之前,开发者若需要在并发环境中使用map,通常有以下选择:
- 使用
sync.RWMutex保护普通map,所有读写操作都需要获取锁 - 选择
sync.Map,但仅适用于特定场景
传统的map实现中,每次读写都需要加锁,这在读多写少的场景下效率低下。想象一个高并发的读取场景,每次读取都需要获取读锁,即使大多数时间不会发生写操作。
sync.Map的read与dirty双map结构就是为了解决这个问题而设计的。
2. read与dirty双map结构原理
sync.Map内部维护了两个map:
- read map:用于大多数读取操作,只读,无锁
- dirty map:用于存储新写入的数据或被标记为删除的数据,需要加锁
2.1 两个map的分工
read map:
- 存储大多数热点数据
- 结构为
readOnly,包含一个map类型的m字段 - 对读取操作完全无锁,极大提升读性能
- 只存储"已存在"的键值对
dirty map:
- 存储新写入的数据或被标记为删除的数据
- 与read map中的数据不完全一致
- 对写操作加保护,保证并发安全
2.2 读写操作实现
读取操作:
- 首先访问 read map
- 如果键不存在,检查是否在miss计数中
- 如果miss次数过多,触发双map切换
- 在dirty map中查找,如果找到则更新read map
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok {
e, ok = m.dirty[key]
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.load()
}
return nil, false
}
写入操作:
- 首先尝试 无锁写入read map
- 如果read map不存在该键,检查是否在dirty map中
- 如果不在,直接写入 dirty map
- 如果在,标记read map中该键为"已删除",然后更新dirty map
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.tryStore(value) {
return
}
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
m.dirty[key] = e
}
e.storeLocked(value)
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(value)
} else {
if !read.amended {
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] newEntry(value)
}
m.mu.Unlock()
}
3. 双map之间的数据迁移机制
sync.Map通过以下机制管理read和dirty之间的数据迁移:
3.1 miss计数机制
每当在read map中找不到键时,增加miss计数:
- 如果miss计数超过dirty map大小,触发数据迁移
- 将dirty map中"未删除"的键值对迁移到read map
- 重置miss计数,并设置amended标志为false
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty, amended: false})
m.dirty = nil
m.misses = 0
}
3.2 数据迁移策略
当满足以下条件时,执行数据迁移:
- miss计数超过dirty map大小
- read map标记为amended(表示dirty中有read没有的数据)
迁移过程:
- 创建新的readOnly结构
- 合并read和dirty中的数据,但排除read中标记为"已删除"的键
- 替换read map,并重置dirty和miss计数
4. 性能考虑
sync.Map的read与dirty双map设计在不同场景下有不同的表现:
4.1 读多写少场景
在这种场景下:
- 大多数读取可以直接访问无锁的read map
- 写操作相对较少,对dirty map的修改不会频繁触发数据迁移
- 性能接近无锁map的读取性能
4.2 写多读少场景
在这种场景下:
- 频繁的写操作会导致miss计数不断重置
- 数据迁移频繁发生,性能下降
- 建议使用
sync.RWMutex保护的普通map
4.3 读写均衡场景
在这种场景下:
- 性能取决于具体的读写比例
- 通常优于传统mutex保护的map
- 但不如专门的读写锁高效
5. 实际应用示例
创建一个sync.Map实例:
var m sync.Map
存储键值对:
m.Store("key1", "value1")
m.Store("key2", "value2")
加载值:
value, ok := m.Load("key1")
删除键:
m.Delete("key2")
遍历map:
m.Range(func(key, value interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", key, value)
return true
})
6. 最佳实践
根据实际场景选择合适的数据结构:
- 如果读操作远多于写操作,选择
sync.Map - 如果写操作频繁,考虑使用
sync.RWMutex保护的map - 如果键值对很少,直接使用
sync.RWMutex可能更简单高效 - 避免在
sync.Map中存储不可比较的键类型
注意sync.Map的局限性:
- 不支持泛型约束,键类型需要可比较
- 不支持获取map的长度
- 不支持自定义并发控制策略
权衡性能和复杂度:
- 如果性能要求极高,考虑使用更专业的并发数据结构
- 如果代码可维护性更重要,选择简单的mutex保护方案
通过理解sync.Map的read与dirty双map结构设计,开发者可以更好地在实际项目中应用这种并发安全的数据结构,充分发挥其优势,避免其局限性。

暂无评论,快来抢沙发吧!