跳过正文
  1. 面试题库/

05|sync.Map 相关

·2059 字·5 分钟
目录
Golang面试题库 - 这篇文章属于一个选集。
§ 5: 本文

1. sync.Map 的底层原理
#

分析

对于sync.Map的底层原理,我们回答的核心点围绕,sync.Map如何保证并发安全,并减少锁操作的原理

回答

空间换时间、数据的动态流转、entry状态的设计

  • sync.Map采用 空间换取时间的取舍策略 以及 实时动态的数据流转策略,期望使用read map来尽量将读、更新、删除操作的流量用无锁化的操作挡下来,避免去加锁去访问拥有全量数据的dirty map
  • sync.Map对于k-v对里面的v,还设计了两种删除状态,一种是为nil的软删除态,一种是为expunged的硬删除态
    • nil态 可以拦截删除操作在read map 这一层
    • expunged态 可以正确标识dirty map中有没有对应的逻辑删除的key-entry

2. read map 和 dirty map 之间的关联?
#

分析:

  • read map和dirty map作为sync.Map中的两个最重要的结构,他们互帮互助,read map为dirty map尽量用轻便的原子操作挡住读、更新、删的流量,而dirty map也为read map提供最终的兜底手段
  • 同时 read map和dirty map数据有互相流转的过程

回答

  • read 可以当做 dirty的保护层map,尽量用轻便的原子操作将流量拦截在read,防止加锁访问dirty
  • dirty 当做read的兜底层map,如果在read 中没有完成的操作,最终需要加锁,然后尝试在dirty 完成兜底
  • 当因为miss read而访问dirty的次数等于dirty的长度时,需要将dirty map提升到read map,并置dirty为nil
  • 当dirty map为nil,会在Store里面触发dirtyLocked流程,这个流程会遍历read map,将所有非删除状态的k-entry对写入到新dirty 里面去

3. 为什么要设计 nil 和 expunged 状态?
#

分析:

  • dirty map用于最终数据兜底,如果每次我们删除操作,直接删除dirty中对应k-entey对,但后面又对这个k进行写操作,那就导致多次加锁操作
  • 设计nil状态来标记k-entry对已经被逻辑删除了,但是k-entry还存在于read map和dirty map中,如果想对一个删除的key,再进行写,那么也可以通过在read map中解决
  • 而设计expunged状态是为了正确标识出key-entry对是否存在于dirty map中
  • nil状态是软删除状态,代表逻辑上k-v被删除了,但是k-entry对还存在与read map和dirty map中
  • expunged态是硬删除态,也是逻辑上k-v删除了,但是k-entey对只存在read map中

回答:

  • nil 态是软删除态,可以让删除操作的流量在read map层挡住,防止加锁,去删除dirty map中的数据
  • expunged 态是硬删除态,也是逻辑上k-v删除了,但是k-entey对只存在read map中,能正确标识出key-entry对是否存在于dirty map中

4. sync.Map 适用的场景?
#

分析:

因为我们期望将更多的流量在read map这一层进行拦截,从而避免加锁访问dirty map

对于更新,删除,读取,read map可以尽量通过一些原子操作,让整个操作变得无锁化,这样就可以避免进一步加锁访问dirty map

倘若写操作过多,sync.Map 基本等价于一把互斥锁 + map,所以我们要尽可能避免写多的场景,场景应用贴合读多,更新多,删多

回答:

sync.Map 是适用于读多、更新多、删多、写少的场景

5. 你认为 sync.Map 有啥不足吗?
#

分析:

对于sync.map,在dirtyLocked流程中,需要遍历整个read map,完成两步工作

  • 更新read map中的删除状态,将软删除态(nil) 变成 硬删除态(expunged)
  • 将read map中非删除状态的key-entry对 写入到 dirty map中

dirtyLocked这个流程是加锁的,如果在sync.map数据量比较大的情况下,会引性能抖动问题,因为这个时候其他goroutine想要访问dirty map拿锁就只能阻塞起来,存在很大的隐患

回答:

  • sync.Map不适用于写多的场景,因为写操作足够多的话,sync.Map就相当于一把Mutex+Map
  • 而且sync.Map中存在一个将read map数据流转到 dirty map的过程,这个过程是线性时间复杂度,当map中k-v数量较多的时候,容易导致程序性能抖动,比如想要访问sync.Map拿锁操作的goroutine一直等待这个线性时间复杂度的过程完成

6. 补充知识——分段锁 map 是什么
#

保证map的并非安全,最简单的做法就是直接用锁来进行保护,比如读写锁保护,但是这样锁的粒度比较大,加锁直接锁住了整个map,性能很差

分段锁的核心思想:

  1. 数据分片:将整个Map划分为多个段,每个段包含独立的子Map和锁。
  2. 锁粒度细化:操作时仅锁定目标数据所在的段,其他段仍可并发访问,减少锁竞争

适用写多或Key分布均匀的场景,在选择sync.Map和分段锁map,优先考虑的就是应用场景下读写流量的比例,像sync.Map只适用于读多写少的场景,如果读写流量中写流量占比比较大 或者 无法在使用之初确定读写流量比例,那就可以直接选择使用分段锁map

Golang面试题库 - 这篇文章属于一个选集。
§ 5: 本文