golang sync.Map之如何设计一个并发安全的读写分离结构?
在 golang中,想要并发安全的操作map,可以使用sync.Map结构,sync.Map 是一个适合读多写少的数据结构,今天我们来看看它的设计思想,来看看为什么说它适合读多写少的场景。
如下,是golang 中sync.Map的数据结构,其中 属性read 是 只读的 map,dirty 是负责写入的map,
sync.Map中的键值对value值本质上都是entry指针类型,entry中的p才指向了实际存储的value值
。
// sync.Map的核心数据结构
type Map struct {
mu Mutex // 对 dirty 加锁保护,线程安全
read atomic.Value // read 只读的 map,充当缓存层
dirty map[interface{}]*entry // 负责写操作的 map,当misses = len(dirty)时,将其赋值给read
misses int // 未命中 read 时的累加计数,每次+1
}
// 上面read字段的数据结构
type readOnly struct {
m map[interface{}]*entry //
amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
}
// 上面m字段中的entry类型
type entry struct {
// value是个指针类型
p unsafe.Pointer // *interface{}
}
我们从一个
sync.Map的数据写入和数据查询
两个过程来分析这两个map中数据的变化。
我将不展示具体的代码,仅仅讲述数据的流动,相信懂了这个以后再去看代码应该不难。
步骤一:
首先是一个初始的sync.Map 结构,我们往其中写入数据,数据会写到dirty中,同时,由于sync.Map 刚刚创建,所以read map还不存在,所以这里会先初始化一个read map 。
amended
是read map中的一个属性,为true代表 dirty 和read中数据不一致。
步骤二:
接着,如果后续再继续写入新数据,
在read map没有从dirty 同步数据之前,即
amended
变为false之前,再写入新键值对都只会往dirty里写。
步骤三:
如果有读操作,sync.Map 都会尽可能的让其先读read map,read map读取不到并且
amended
为true,即read 和dirty 数据不一致时,会去读dirty,读dirty的过程是上锁的。
步骤四:
当读取read map中miss次数大于等于dirty数组的长度时,会触发dirty map整体更新为readOnly map,并且这个过程是阻塞的。更新完成后,原先dirty会被置为空,
amended
为false,代表read map同步了之前所有的数据
。如下图所示,
整体更新的逻辑是直接替换变量的值,并非挨个复制,
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
// 将dirty置给read,因为穿透概率太大了(原子操作,耗时很小)
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
步骤五:
如果后续sync.Map 不再插入新数据
,那么读取时就可以一直读取read map中的数据了,直接读取read map 中的key是十分高效的,只需要用atomic.Load 操作 取到readOnly map结构体,然后从中取出特定的key就行。
如果读miss了,因为没有插入新数据,
read.amended=false
代表read 是保存了所有的k,v键值对,读miss后,也不会再去读取dirty了,也就不会有读dirty加锁的过程。
// 上面read字段的数据结构
type readOnly struct {
m map[interface{}]*entry //
amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
}
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 因read只读,线程安全,优先读取
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果read没有,并且dirty有新数据,那么去dirty中查找(read.amended=true:dirty和read数据不一致)
// 暂时省略 后续代码
.......
}
上面的获取key对应的value过程甚至比RWMutex 读锁下获取map中的value还要高效,毕竟RWmutex 读取时还需要加上读锁,其底层是用
atomic.AddInt32
操作,而sync.Map 则是用
atomic.load
获取map,
atomic.AddInt32
的开销比
atomic.load
的开销要大。