数据结构

双层存储架构(read vs dirty)
我们要掌握的第一个核心概念就是:sync.Map 为什么会有两个 Map?
在标准的 Go map 中,为了防止并发冲突,我们在读写时都需要加锁(Mutex)。但在高并发场景下,如果读操作很多,锁的竞争就会导致性能下降。
sync.Map 解决这个问题的策略是**“读写分离”**。它内部维护了两个字段:
read (只读/快路径)
特点:访问不需要加锁(使用原子操作 atomic)。
内容:存放的是“稳定”的数据,通常是那些已经存在且主要被用于读取的 key。
比喻:就像你办公桌上的便利贴。你需要找电话号码时,第一眼先看桌上(read),因为这最快,不用起身。
dirty (脏/慢路径)
特点:访问需要加锁(mu Mutex)。
内容:存放所有的数据,包括最新写入且还没同步到 read 中的 key。
比喻:就像档案室里的文件柜。如果桌上(read)找不到,你就不得不拿钥匙(加锁),走到档案室(dirty)去翻找,这比较慢。
Entry 的三种状态(nil, expunged, 指针)
为了高效地处理读写操作,sync.Map 在底层存储的不是值本身,而是一个指向 entry 结构体的指针。这个 entry 内部有一个原子指针 p,它指向了实际存储的值。
这个指针 p 的状态非常关键,它有三种可能:
状态:p 指向一个真实的、有效的 Key-Value Pair。
含义:这是最常见的情况,Key 存在且有值。
nil (软删除/删除标记)状态:p 的值是 nil。
含义:表示该 Key 已经被删除了 (Delete)。
出现场景:当你调用 Delete 方法时,系统会尝试将 dirty 或 read 中的对应 entry 的指针设置为 nil。
回到你的问题:你最开始问的场景就是这个:read 中 Key="A" 的值是 nil。这意味着 Key "A" 已经执行了删除操作,所以 Load 发现它是 nil,就会返回 false(找不到)。
expunged (彻底清除/标记位)状态:p 指向一个特殊的内部标记值(不是 nil)。
含义:**表示该 Key 不仅被删除,而且它在 **dirty Map 中也不存在(已被彻底清理)。这个标记只存在于 read Map 中。
💡 小趣闻: expunge 在英文里是“清除、擦除”的意思,这个命名非常形象地说明了它的作用。
Store

Load

动态流转机制
这个机制就是解决一个核心问题:read Map 什么时候会变得“过时”需要更新?
核心概念:读缺失 (Miss)
sync.Map 通过一个内部的 misses 计数器来判断 read Map 是否已经不再高效。
什么是 Miss?
当一个 Key 通过 Load 查找时,如果它:
没有在 read Map 中找到,
但在 dirty Map 中找到了(需要加锁的慢路径),
这就构成了一次 Miss,misses 计数器就会增加 1。
触发晋升(Promotion)
当 misses 计数器累积到一个阈值时,就会触发一次大规模的 Map 替换操作,我们将它称为**“晋升”**:
misses≥len(dirty)misses≥len**(dirty)**
一旦满足这个条件,系统就会用当前完整的 dirty Map 来替换掉过时的 read Map。这会把所有的新数据和更新的数据都放进快速、无锁的 read 路径中。
Delete
