Skip to content

🐯 [并发] Scan 持读锁期间调用 fn 可能嵌套获取同一锁 · fsm.go #141

Description

@github-actions

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

⚠️ [重要 · 并发] Scan 持读锁期间调用 fn 可能嵌套获取同一锁 service/fsm.go:189

问题根因ScanRange 全程持 m.mu.RLock()(memtable.go:188),而回调 fn(即 KVServer.Scan 的闭包)中调用了 pred.Eval——这是一个纯函数,在当前 PR 中不嵌套锁。但未来若调用方在 fn 中做任何需要写锁的操作(如在一个遍历中删改数据),将因同一 goroutine 尝试获取 m.mu.Lock() 而死锁(Go 的 RWMutex 不可递归)。根因是「锁范围隐含地覆盖了回调执行期间」,但 API 签名没有声明 fn 内禁止写操作。

为什么低级解法不够:加注释说明「回调内不得写锁」是被动防御;改为用 TryRLock 兜底也不解决设计问题。通用评审者可能看不出这个隐患。

架构级方案:两个方向:① 将扫描路径改为快照迭代器模式——在 ScanRange 开始时创建 active/dirty 跳表的不可变快照(atomic load 指针 + copy-on-write 或引用计数),然后释放读锁,在无锁快照上遍历。这样 fn 可安全执行任何操作,锁仅用于保护快照创建。② 在 ScanRange 的文档注释中明确标注「fn 内禁止获取 m.mu 写锁」,并在未来引入 lint 或 sync.RWMutexRLocker() 代理检查。建议采用方案①,这不仅消除死锁隐患,还能大幅缩短锁持有时间。

代价/收益:代价:快照方案需要跳表支持 O(1) 的原子快照(拷贝头指针数组),或引入引用计数机制防止遍历中节点被释放;实现成本中等。收益:锁持有时间从「整段扫描时长」降为「O(1) 快照创建」,写入并发几乎不受扫描影响,回调函数无锁使用约束。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions