经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
golang sync.Map之如何设计一个并发安全的读写分离结构?
来源:cnblogs  作者:蓝胖子的编程梦  时间:2024/3/20 9:11:48  对本文有异议

在 golang中,想要并发安全的操作map,可以使用sync.Map结构,sync.Map 是一个适合读多写少的数据结构,今天我们来看看它的设计思想,来看看为什么说它适合读多写少的场景。

如下,是golang 中sync.Map的数据结构,其中 属性read 是 只读的 map,dirty 是负责写入的map,sync.Map中的键值对value值本质上都是entry指针类型,entry中的p才指向了实际存储的value值

  1. // sync.Map的核心数据结构
  2. type Map struct {
  3. mu Mutex // 对 dirty 加锁保护,线程安全
  4. read atomic.Value // read 只读的 map,充当缓存层
  5. dirty map[interface{}]*entry // 负责写操作的 map,当misses = len(dirty)时,将其赋值给read
  6. misses int // 未命中 read 时的累加计数,每次+1
  7. }
  8. // 上面read字段的数据结构
  9. type readOnly struct {
  10. m map[interface{}]*entry //
  11. amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
  12. }
  13. // 上面m字段中的entry类型
  14. type entry struct {
  15. // value是个指针类型
  16. p unsafe.Pointer // *interface{}
  17. }

我们从一个sync.Map的数据写入和数据查询 两个过程来分析这两个map中数据的变化。

我将不展示具体的代码,仅仅讲述数据的流动,相信懂了这个以后再去看代码应该不难。

步骤一: 首先是一个初始的sync.Map 结构,我们往其中写入数据,数据会写到dirty中,同时,由于sync.Map 刚刚创建,所以read map还不存在,所以这里会先初始化一个read map 。amended 是read map中的一个属性,为true代表 dirty 和read中数据不一致。

image.png

步骤二: 接着,如果后续再继续写入新数据,
在read map没有从dirty 同步数据之前,即amended 变为false之前,再写入新键值对都只会往dirty里写。

image.png

步骤三: 如果有读操作,sync.Map 都会尽可能的让其先读read map,read map读取不到并且amended 为true,即read 和dirty 数据不一致时,会去读dirty,读dirty的过程是上锁的。

image.png

步骤四: 当读取read map中miss次数大于等于dirty数组的长度时,会触发dirty map整体更新为readOnly map,并且这个过程是阻塞的。更新完成后,原先dirty会被置为空,amended 为false,代表read map同步了之前所有的数据。如下图所示,

image.png

整体更新的逻辑是直接替换变量的值,并非挨个复制,

  1. func (m *Map) missLocked() {
  2. m.misses++
  3. if m.misses < len(m.dirty) {
  4. return
  5. }
  6. // 将dirty置给read,因为穿透概率太大了(原子操作,耗时很小)
  7. m.read.Store(readOnly{m: m.dirty})
  8. m.dirty = nil
  9. m.misses = 0
  10. }

步骤五: 如果后续sync.Map 不再插入新数据,那么读取时就可以一直读取read map中的数据了,直接读取read map 中的key是十分高效的,只需要用atomic.Load 操作 取到readOnly map结构体,然后从中取出特定的key就行。

如果读miss了,因为没有插入新数据,read.amended=false 代表read 是保存了所有的k,v键值对,读miss后,也不会再去读取dirty了,也就不会有读dirty加锁的过程。

  1. // 上面read字段的数据结构
  2. type readOnly struct {
  3. m map[interface{}]*entry //
  4. amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
  5. }
  6. func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
  7. // 因read只读,线程安全,优先读取
  8. read, _ := m.read.Load().(readOnly)
  9. e, ok := read.m[key]
  10. // 如果read没有,并且dirty有新数据,那么去dirty中查找(read.amended=true:dirty和read数据不一致)
  11. // 暂时省略 后续代码
  12. .......
  13. }

上面的获取key对应的value过程甚至比RWMutex 读锁下获取map中的value还要高效,毕竟RWmutex 读取时还需要加上读锁,其底层是用atomic.AddInt32 操作,而sync.Map 则是用 atomic.load 获取map,atomic.AddInt32 的开销比atomic.load 的开销要大。

??????,所以,为什么我们说golang的sync.Map 在大量读的情况下性能极佳,因为在整个读取过程中没有锁开销,atomic.load 原子操作消耗极低。

但是如果后续又写入了新的键值对数据,那么 dirty map中就会又插入到新的键值对,dirty和read的数据又不一致了,read 的amended 将改为true。

并且由于之前dirty整体更新为read后,dirty字段置为nil了,所以,在更改amended时,也会将read中的所有未被删除的key同步到 dirty中

image.png

??????注意,为什么在dirty整体更新一次read map后,再写入新的键值对时,需要将read map中的数据全部同步到dirty,因为随着dirty的慢慢写入,后续读操作又会造成读miss的增加,最终会再次触发dirty map整体更新为readOnly map,amended 改为false,代表read map中又有所有键值对数据了,也就是会回到步骤三的操作,重复步骤三到步骤五的过程。

image.png

只有将read map中的数据全部同步到dirty ,才能保证后续的整体更新,不会造成丢失数据。

看到这里应该能够明白sync.Map的适合场景了,我来总结下,

sync.Map 适合读多写少的场景,大量的读操作可以通过只读取read map 拥有极好的性能。

而如果写操作增加,首先会造成read map中读取miss增加,会回源到dirty中读取,且dirty可能会频繁整体更新为read,回源读取,整体更新的步骤都是阻塞上锁的。

其次,写操作也会带来dirty和 read中数据频繁的不一致,导致read中的数据需要同步到dirty中,这个过程在键值对比较多时,性能损耗较大且整个过程是阻塞的。

所以sync.Map 并不适合大量写操作。

原文链接:https://www.cnblogs.com/hobbybear/p/18083224

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号