文章目录

Go语言sync.Map的read与dirty双map结构设计

发布于 2026-04-20 16:26:17 · 浏览 3 次 · 评论 0 条

Go语言sync.Map的read与dirty双map结构设计

在Go语言中,并发安全的数据结构一直是开发者关注的问题。sync.Map作为标准库提供的并发安全map实现,其核心创新在于read与dirty双map结构设计。这种设计巧妙地平衡了读多写少场景下的性能问题。

1. 背景与问题

在Go 1.9之前,开发者若需要在并发环境中使用map,通常有以下选择:

  1. 使用 sync.RWMutex 保护普通map,所有读写操作都需要获取锁
  2. 选择 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 读写操作实现

读取操作

  1. 首先访问 read map
  2. 如果键不存在检查是否在miss计数中
  3. 如果miss次数过多触发双map切换
  4. 在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
}

写入操作

  1. 首先尝试 无锁写入read map
  2. 如果read map不存在该键检查是否在dirty map中
  3. 如果不在直接写入 dirty map
  4. 如果在标记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 数据迁移策略

满足以下条件时,执行数据迁移:

  1. miss计数超过dirty map大小
  2. read map标记为amended(表示dirty中有read没有的数据)

迁移过程

  1. 创建新的readOnly结构
  2. 合并read和dirty中的数据,但排除read中标记为"已删除"的键
  3. 替换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. 最佳实践

根据实际场景选择合适的数据结构:

  1. 如果读操作远多于写操作选择sync.Map
  2. 如果写操作频繁考虑使用sync.RWMutex保护的map
  3. 如果键值对很少直接使用sync.RWMutex可能更简单高效
  4. 避免sync.Map中存储不可比较的键类型

注意sync.Map的局限性:

  1. 不支持泛型约束,键类型需要可比较
  2. 不支持获取map的长度
  3. 不支持自定义并发控制策略

权衡性能和复杂度:

  1. 如果性能要求极高考虑使用更专业的并发数据结构
  2. 如果代码可维护性更重要选择简单的mutex保护方案

通过理解sync.Map的read与dirty双map结构设计,开发者可以更好地在实际项目中应用这种并发安全的数据结构,充分发挥其优势,避免其局限性。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文