Go语言学习——sync.map源码剖析

tianqy · · 347 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。
1.简介</br> &nbsp;&nbsp;&nbsp;&nbsp;最近看了下Sync包,详读了sync.map源码,感觉源码实现还是比较巧妙的,有不少可以学习的地方;在讲源码前,先看下sync.map的"历史",从网上搜资料,sync.map是Go语言在1.9版本才引入的并发安全的map,对此,有些同学心中可能会有个疑问,如果是支持并发,为什么不采取锁map的方式,为啥还要在单独搞个sync.map结构呢?我们先看下锁map存在的问题:</br> &nbsp;&nbsp;&nbsp;&nbsp;1)mutex + map</br> &nbsp;&nbsp;&nbsp;&nbsp;最简单的方案就是在map上加个锁,针对map的所有操作都要提前加锁,其存在问题也很明显,锁竞争会非常频繁;</br> &nbsp;&nbsp;&nbsp;&nbsp;2)rwmutex + map</br> &nbsp;&nbsp;&nbsp;&nbsp;优化一点,依据场景,如果是读操作多于写操作,可以把mutex换成rwmutex,相比方案一,有一定优化、至少读读之间不会存在互斥,不过,读写之间还会存在阻塞;</br> &nbsp;&nbsp;&nbsp;&nbsp;根据锁map的优化迭代方案可知,在读读场景下,rwmutex + map可以并发、不存在阻塞,但是,读写还是存在阻塞,而sync.map要做的事情就是能进一步优化:对于map的各种操作,尽可能不阻塞;为此,sync.map采用了两级缓存实现,一级缓存做无锁并发,二级缓存做有锁并发,如:</br> &nbsp;&nbsp;&nbsp;&nbsp;![image.png](https://static.studygolang.com/200922/dedee94450d66e2e1bde468d9a258af4.png)</br> &nbsp;&nbsp;&nbsp;&nbsp;对上图说明两点:</br> &nbsp;&nbsp;&nbsp;&nbsp;1)针对sync.map的各种操作,都先经过一级缓存,一级缓存采用无锁的方式,只要不出现击穿,即key都在一级缓存中可以找到,则就不会访问到二级缓存;</br> &nbsp;&nbsp;&nbsp;&nbsp;2)一级缓存和二级缓存之间存在数据同步,二级缓存数据相对更全一些,所以当一级缓存数据比较久时,可以将二级缓存数据同步一下,该情况是在读击穿时处理;在不击穿的前提下,一级缓存中可能有数据删除,数据移除情况也要同步给二级缓存,清除废弃数据、减少空间占用,该情况是在写击穿并且是一、二级缓存都不存在键的情况处理,总之,同步的原则是:一级缓存数据尽可能新;一级缓存数据只能是二级缓存的子集;</br> 2.实现</br> &nbsp;&nbsp;&nbsp;&nbsp;sync.map的优势是理想情况下以无锁代替有锁、提高性能,但存在击穿后不得不加锁的问题,一旦击穿进入二级缓存,就要进行锁操作了,所以sync.map不太适用于写多读少以及频繁创建新键的情况;因为要考虑击穿问题,所以sync.map的实现也是围绕击穿考虑的。 2.1读操作</br> &nbsp;&nbsp;&nbsp;&nbsp;读操作比较简单,步骤是:</br> &nbsp;&nbsp;&nbsp;&nbsp;1)查看一级缓存中是否有key,有就返回对应value;</br> &nbsp;&nbsp;&nbsp;&nbsp;2)如果没有则进入读击穿,加锁后,在复看一级缓存中是否有key(复看是因为存在二级缓存向一级缓存同步数据的情况),有就返回对应value;</br> &nbsp;&nbsp;&nbsp;&nbsp;3)如果没有则看二级缓存中有没有,有就返回对应value,此时出现读击穿,会进入读击穿保护机制——击穿达到一定次数,会将二级缓存数据同步到一级缓存;</br> &nbsp;&nbsp;&nbsp;&nbsp;需要注意的是,在返回value时要检测value的有效性,如果已经废弃(expunged状态),则不用返回。</br> 2.2写操作</br> &nbsp;&nbsp;&nbsp;&nbsp;写操作相对复杂,根据key是否存在的情况,可以分为create和update,步骤是:</br> &nbsp;&nbsp;&nbsp;&nbsp;1)查看一级缓存中是否有key,有就尝试更新,之所以是尝试是因为还要检查数据是否已经废弃,如果已经废弃,即使key在一级缓存中存在,也是击穿效果,因为二级缓存中没有;</br> &nbsp;&nbsp;&nbsp;&nbsp;2)如果一级缓存操作失败,加锁后,在复看一级缓存,如果有key,则更新value,并检测value是否为废弃状态,如果是,则将key、value写入二级缓存;</br> &nbsp;&nbsp;&nbsp;&nbsp;3)如果一级缓存中一直没有key,但二级缓存中有,则直接更新数据;</br> &nbsp;&nbsp;&nbsp;&nbsp;4)如果一级缓存和二级缓存都没有key,则将key、value写入二级缓存,此时会尝试将一级缓存数据同步给二级缓存,用于删除废弃数据(将一级缓存中的删除数据设置为expunged状态),因为只有该情况下,一级缓存数据可能是二级缓存数据的子集,所以当插入全新的key时,才会尝试更新缓存数据、移除废弃数据;</br> 2.3删除操作</br> &nbsp;&nbsp;&nbsp;&nbsp;删除采取的是延迟删除操作,对于待删除数据,其value先设置为nil,优先从一级缓存删除,如果一级缓存没有,再去二级缓存中删除。</br> 2.4源码</br> &nbsp;&nbsp;&nbsp;&nbsp;以1.14.4版本为例,处理源码是: ```go func (m *Map) Load(key interface{}) (value interface{}, ok bool) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // 一级缓存没有查到key,加锁、复查,amended用于判断一级缓存和二级缓存是否一致 if !ok && read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 一级缓存还是没有查到key,则击穿进入二级缓存 if !ok && read.amended { e, ok = m.dirty[key] m.missLocked() // 读击穿保护,根据击穿次数决定是否要同步数据 } m.mu.Unlock() } if !ok { return nil, false } return e.load() } 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 { // 一级缓存中有key,则更新value,同时,还要查看value是否已经废弃,如果废弃还要将数据写入二级缓存,确保下次同步前,二级缓存数据的完整性,因为操作到二级缓存,所以需要放在锁操作下;这也是为什么tryStore只是尝试存储 if e.unexpungeLocked() { m.dirty[key] = e } e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { e.storeLocked(&value) } else { // 对于key完全不存在的情况,尝试数据同步,从一级缓存到二级缓存 if !read.amended { m.dirtyLocked() // 数据同步时,废弃数据不会同步,废弃数据会设置为expunged状态 m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } m.mu.Unlock() } // Delete部分相对简单,主要是将value设置为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 } } } ``` 3.总结</br> &nbsp;&nbsp;&nbsp;&nbsp;sync.map是以无锁操作一级缓存的方式支持并发、提高性能,而根据其实现可知,sync.map适用于读多、更新多、新建少的场景(新建情况下,可能会带来较大的开销,比如:读击穿、数据刚从二级缓存同步到一级缓存后,又要新建key,数据又要反向同步一次)。</br>

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:1006366459

347 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传