dirty 提升为 read 的确切触发条件是 misses >= len(dirty),且需 read.amended == true;此时执行原子更新 read、置 dirty = nil、重置 misses = 0。

dirty 提升为 read 的确切触发条件是什么
触发条件是 misses >= len(dirty),不是「超过」或「大于」,而是「大于等于」。这个等号很关键——当第 len(dirty) 次 miss 发生时,提升立即发生。
每次调用 Load 在 read.m 中没命中、转而查 dirty 时,misses 就加 1;但注意:只有在 read.amended == true(即 dirty 非空且有新数据)的前提下,这次查 dirty 才会计为一次 miss。
-
dirty为空时,read.amended为 false,misses不再递增 -
dirty刚被提升后清空,此时len(dirty) == 0,只要没新写入,misses >= 0永远成立——但不会触发提升,因为amended已是 false,逻辑短路了 - 提升动作本身包含三步:
read原子更新为新readOnly(含原dirty内容)、dirty = nil、misses = 0
为什么选 len(dirty) 而不是固定阈值或百分比
用 len(dirty) 是一种自适应策略:数据越少,越早同步;数据越多,容忍更多 miss,避免频繁拷贝。它隐含一个假设——如果已有 N 个新 key 在 dirty 中,那大概率后续读请求里有相当比例会命中它们,不如一次性推到 read 供无锁访问。
但这不是绝对最优解。比如 dirty 里全是冷 key(极少被读),而 read 里热 key 却因删除标记变多导致命中率下降,此时 misses 仍会稳步上涨并触发提升,把一堆冷数据又刷进 read,反而稀释了热 key 的缓存效率。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 提升后
read.m包含所有未被expunged的旧 entry + 全量dirtyentry,但已标记删除的 entry 不会复制过去 -
dirty中那些指向expunged的 entry,在提升时会被直接丢弃(不复制、不保留) - 所以提升不是“全量镜像”,而是“有效数据迁移”
容易被忽略的边界行为
提升不是原子的“切换指针”那么简单,中间有锁和状态重置,且对并发 Load/Store 有可观测影响。
- 提升过程中,任何新的
Store都会先检查dirty是否为 nil;如果是,就基于当前read.m初始化一份新dirty(复制所有非expunged的 key),再写入——这步开销不小 - 多个 goroutine 同时触发 miss 并接近阈值时,可能只有一个能成功执行提升,其余会在
mu.Lock()后发现amended == false而退出,但它们的misses++已发生,只是没被用于触发 -
Range方法内部会先尝试用read迭代,失败才 fallback 到加锁遍历dirty;若在Range执行中途发生提升,迭代结果仍基于旧read快照,不会包含刚提升进来的 key
如何验证当前是否发生了提升
标准库没暴露 misses 或 dirty 长度,无法直接观测。但可通过组合行为间接判断:
- 在大量写入后立刻密集
Load未写过的 key,观察首次命中的延迟是否明显降低(说明刚完成提升,read变大了) - 用
go tool trace抓取运行时,搜索sync.Map.Load中锁等待时间突降的区间,往往对应提升完成点 - 修改源码注入日志(仅调试):在
missLocked里加条件打印if m.misses >= len(m.dirty)
真正要注意的是:别依赖提升时机做业务逻辑,它只服务于性能优化,不是一致性保证机制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










