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,性能很差
分段锁的核心思想:
- 数据分片:将整个Map划分为多个段,每个段包含独立的子Map和锁。
- 锁粒度细化:操作时仅锁定目标数据所在的段,其他段仍可并发访问,减少锁竞争
适用写多或Key分布均匀的场景,在选择sync.Map和分段锁map,优先考虑的就是应用场景下读写流量的比例,像sync.Map只适用于读多写少的场景,如果读写流量中写流量占比比较大 或者 无法在使用之初确定读写流量比例,那就可以直接选择使用分段锁map