GO中sync.Map的底层原理

Published on in Technology with 0 views and 0 comments

数据结构

image.png

双层存储架构(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 的状态非常关键,它有三种可能:

  1. 指向有效值

状态p 指向一个真实的、有效的 Key-Value Pair。

含义:这是最常见的情况,Key 存在且有值。

  1. nil (软删除/删除标记)

状态p 的值是 nil

含义表示该 Key 已经被删除了 (Delete)

出现场景:当你调用 Delete 方法时,系统会尝试将 dirtyread 中的对应 entry 的指针设置为 nil

回到你的问题:你最开始问的场景就是这个:read 中 Key="A" 的值是 nil。这意味着 Key "A" 已经执行了删除操作,所以 Load 发现它是 nil,就会返回 false(找不到)。

  1. expunged (彻底清除/标记位)

状态p 指向一个特殊的内部标记值(不是 nil)。

含义:**表示该 Key 不仅被删除,而且它在 **dirty Map 中也不存在(已被彻底清理)。这个标记只存在于 read Map 中。

💡 小趣闻: expunge 在英文里是“清除、擦除”的意思,这个命名非常形象地说明了它的作用。

Store

image.png

Load

image.png

动态流转机制

这个机制就是解决一个核心问题:read Map 什么时候会变得“过时”需要更新?

核心概念:读缺失 (Miss)

sync.Map 通过一个内部的 misses 计数器来判断 read Map 是否已经不再高效。

什么是 Miss?

当一个 Key 通过 Load 查找时,如果它:

没有read Map 中找到,

但在 dirty Map 中找到了(需要加锁的慢路径),

这就构成了一次 Miss,misses 计数器就会增加 1。

触发晋升(Promotion)

当 misses 计数器累积到一个阈值时,就会触发一次大规模的 Map 替换操作,我们将它称为**“晋升”**:

misses≥len(dirty)misseslen**(dirty)**

一旦满足这个条件,系统就会用当前完整的 dirty Map 来替换掉过时的 read Map。这会把所有的新数据和更新的数据都放进快速、无锁的 read 路径中。

Delete

image.png


标题:GO中sync.Map的底层原理
作者:wangzhaoo
地址:http://www.bangnimang.top/go-syncmap