以为你是独一无二的,谁知竟然偷偷又多了一个
golang中的哈希数据结构是map,在项目中用到map存储k(模型ID)v(发送数据令牌桶)信息,当从上游获取到一条数据,根据模型ID找到对应的令牌桶获取令牌,这就是读的过程,当模型配置修改的时候,需要清除map中存储的模型信息等到下一个该模型的数据到来再添加,这就是写的过程。
golang中的map是不带锁的,支持并发读,却不支持读写并发,如果同时进行读与写,会报错panic(fatal error: concurrent map writes),那么如何解决呢?一种方法是采用的是加锁,可以加mutex或者读写锁,第二种就是sync.map,它支持了并发读写操作。
数据结构
如下所示,map中有read(读)和dirty(写)两种,操作dirty的时候需要加锁,misses用来记录从dirty读的次数,当超过一定次数后dirty中的数据会同步到read中。read中有amended字段来标识同一条记录read中没有dirty中有的情况。expunge代表dirty中被删掉的情况。
type Map struct {
mu Mutex // 操作dirty时候上的锁
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int // 从dirty读加1,超过一定次数dirty同步到read
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
m map[interface{}]*entry
amended bool // 如果dirty中有的read中没有的,标记为true
}
// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
var expunged = unsafe.Pointer(new(interface{}))
type entry struct {
// If p == nil, the entry has been deleted and m.dirty == nil.
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
p unsafe.Pointer // *interface{}
}
来张图:
三个源码解读(场景需要连起来考虑)
Load
简单来说,load因此会从read和dirty两个中拿,如果read有且dirty中没有修改过,判断该条记录的状态如果是nil或者expunged(dirty中被删掉)则返回获取不到,其他状态返回read中的值;如果read中没有且dirty中修改,就加锁并从dirty中拿,并把missed字段+1,同步dirty到read,dirty=nil.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// 可能锁等待的时候read中已经有该key了,所以做二次检查
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked() //misses计数+1,且如果misses小于dirty的长度,dirty同步到read,dirty=nil
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
Store
如果read中存在key且尝试更新成功(p不是expaunged状态,即在dirty中被删掉),返回即可 否则加锁,并再次检查read中有没有该key,有可能加锁的时候read中已经被同步了该key了。 如果read中有key而之前没有,说明从dirty中同步过来一波,如果p是expunged,就把p设置成nil并且dirty中加入entry 如果read中没有dirty中有,直接存储 如果read中没有dirty中也没有,如果read和dirty中是一样的没有修改过,如果dirty为nil,同步read到dirty,存储到dirty,并且修改read的属性字段amend设置成修改过true
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() { //如果entry.p是expaunged,就设置成nil
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
} else {
if !read.amended {
m.dirtyLocked() // 如果dirty为nil,read同步到dirty
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// 尝试存储,如果entry.p不是expunged就存储,是的话返回false
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
// dirty为nil
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() { // 如果p不为expunged
m.dirty[k] = e
}
}
}
Delete
如果read中不存在key且read和dirty中不一致,上锁,再次检查read,如果还是如此,删除dirty中的key 如果read中存在key,如果entry.p为nil或者expunged,返回false,如果不是,entry.p设置成nil,标记删除
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
总结
sync.Map中有read与dirty,操作dirty需要锁
每次判断完read有无key之后进行进行加锁操作后还需要再次判断read有无key,防止在此时间内进行了从dirty同步到read操作,然后操作dirty
load场景中主要从read读,没有再从dirty读,同时会计数,如果数量超过一定量,数据从dirty同步到read
其中有两处read和dirty互相同步的地方,在store场景主要存到dirty, 如果dirty中有直接更新,当dirty为nil的时候会把read中不是expunged状态的同步到dirty;dirty什么时候为nil呢,在load的时候从dirty中读的次数太多的时候会把dirty同步到read,并且dirty=nil
删除为标记删除,把entry.p=nil来标记
适用场景
读多写少,读多都从read读不需要加锁,并且如果从dirty读有概率会要从dirty拷贝数据到read中