文章目录
- sync.Map相关
- [1. sync.Map的底层原理](#1. sync.Map的底层原理)
- [2. read map 和 dirty map 之间的关联?](#2. read map 和 dirty map 之间的关联?)
- [3. 为什么要设计 nil 和 expunged 状态?](#3. 为什么要设计 nil 和 expunged 状态?)
- [4. sync.Map 适用的场景?](#4. sync.Map 适用的场景?)
- [5. 你认为 sync.Map 有啥不足吗?](#5. 你认为 sync.Map 有啥不足吗?)
- [6. 补充知识------分段锁 map 是什么](#6. 补充知识——分段锁 map 是什么)
sync.Map相关
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-entry 对只存在 read map 中
回答:
-
nil 态是软删除态,可以让删除操作的流量在 read map 层挡住,防止加锁,去删除 dirty map 中的数据
-
expunged 态是硬删除态,也是逻辑上 k-v 删除了,但是 k-entry 对只存在 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 中
dirty map 和 read map 中的 key-entry 是相同的,但只有在 read map 中是有效(即没有被删除)时,才会被拷贝到 dirty map 中。删除状态(软删除和硬删除)会导致 entry 在 dirty map 中被过滤掉。
read map只会把非nil的逻辑上存在的共享给dirty map,并且把read map中为nil的entry改为expunged
dirtyLocked 这个流程是加锁的,如果在 sync.map 数据量比较大情况下,会引发性能抖动问题,因为这个时候其他 goroutine 想要访问 dirty map 拿锁就只能阻塞起来,存在很大的隐患
Godis 并发安全 map 实现------https://github.com/hdt3213/godis/blob/master/datastruct/dict/concurrent.go
回答:
sync.Map 不适用于写多的场景,因为写操作足够多的话,sync.Map 就相当于一把 sync.Mutex + Map
而且 sync.Map 中存在一个将 read map 数据流转到 dirty map 的过程,这个过程是线性时间复杂度,当 map 中 k-v 数量较多的时候,容易导致程序性能抖动,比如想要访问 sync.Map 拿锁操作的 goroutine 一直等待这个线性时间复杂度的过程完成
如何设计无锁map,面试应该怎么回答
无锁的话,只能用CAS来不断重试阻挡了


6. 补充知识------分段锁 map 是什么
保证 map 的并发安全,最简单的做法就是直接用锁来进行保护,比如加读写锁保护,但是这样锁的粒度比较大,加锁直接锁住了整个 map,性能很差
分段锁的核心思想:
-
数据分片:将整个 Map 划分为多个段,每个段包含独立的子 Map 和锁
-
锁粒度细化:操作时仅锁定目标数据所在的段,其他段仍可并发访问,减少锁竞争
适用写多或 Key 分布均匀的场景 ,在选择 sync.Map 和分段锁 map,优先考虑的就是应用场景下读写流量的比例,像 sync.Map 只适用了读多写少的场景,如果读写流量中写流量占比较大 或者 无法在使用之初确定读写流量比例,那就可以直接选择使用分段锁 map
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!