Skip to content

🐯 [并发] firstGTE 无锁遍历跳表,并发写入时出现越界 · memtable.go #143

Description

@github-actions

来自 #139 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。

🛑 [阻塞 · 并发] firstGTE 无锁遍历跳表,并发写入时出现越界 storage/zstorage/memtable.go:148

问题根因firstGTE(memtable.go:163-175)在持读锁时遍历跳表的 Next[i] 指针(sl.level-1 到 0 逐层下降)。读锁保护了 m.active/m.dirty 引用的切换,但跳表内部节点的 Next[i] 字段在并发写入时可被修改SkipList.insert 会写 p.Next[i]newNode.Next[i],这些在 put 路径上受 m.mu.Lock() 写锁保护)。在持 RLock 时读 Next[i],写入路径持写锁,这本身是安全的(Go sync.RWMutex 保证读锁与写锁互斥)。但关键在于:firstGTE 的遍历读取 p.Next[i] 时,同一跳表的插入操作可能在另一个 goroutine 等待写锁。由于 RWMutex 的读锁会阻塞写锁,所以不会出现直接的 data race。然而此处存在一个更微妙的问题:firstGTE 在持读锁期间访问 sl.headp.Next[i],若跳表在同一时刻被 sl.insert 修改(通过写锁保护),读锁确保不会并发发生。所以实际上这不是 data race——但前提是 m.mu 在 ScanRange 全程持有,且写操作(put/delete)都在 m.mu 保护下修改跳表。需要确认 put/delete 确实都在 m.mu 写锁保护下执行。

为什么低级解法不够:以目前代码来看,此问题属于误报——读锁确实保护了遍历。但若后续有人把某些跳表操作移出 m.mu 保护(如为了性能将读路径从 RLock 改为原子加载),这个遍历就会崩溃。

架构级方案:此处无需改动。但应加一条明确的架构层约定注释firstGTE 必须在 m.mu(或等效的写保护锁)持有期间调用,该约束应通过类型系统或文档强制执行。另建议:将 firstGTE 改为在快照副本上操作(见上方案)以彻底解耦。

代价/收益:代价:无需额外改动。收益:消除未来重构时的隐患。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions