来自 #139 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
⚠️ [重要 · 锁] ScanRange 全程持读锁阻塞写入与刷盘 storage/zstorage/memtable.go:188
问题根因:ScanRange 在 m.mu.RLock() 期间遍历整个 active+dirty 链表并执行 fn 回调。虽然注释说明「热窗口范围扫描有界且短——fn 内若需……应自行拷贝」,但存在两个问题:(1) ScanRange 执行期间的 fn 回调(含谓词 Eval + append 拷贝)可能在用户数据量大、谓词评估慢时显著延长持锁时间;(2) 写路径(Put/Delete)与 flush 需要写锁 m.mu.Lock(),扫描长持读锁会阻塞写入,热窗口写入是最高频路径,不能容忍写等待。
为什么低级解法不够:警告用户「fn 应快速返回」不能从架构上消除问题。全局读锁对写入的阻塞是硬延迟,且随扫描范围和数据量增大而恶化。
架构级方案:快照语义 + 无锁遍历:将 Scan 与写入解耦——在 Scan 开始时获取当前 active/dirty 链表指针的快照拷贝(不拷贝数据,只拷贝顶层指针和 level 元信息),然后立刻释放读锁。遍历在快照上进行(底层节点若被 flush 回收则存在悬空指针风险,所以需要 RCU 或引用计数)。更轻量的中间方案:缩短临界区——先快照链表指针(只需在加锁期间拷贝 head/Next[0] 引用),释放锁后再遍历和谓词评估。这样写入不等待整个扫描过程,仅等待指针拷贝的微秒级窗口。
代价/收益:快照方案复杂度上升,需要确保链表节点在快照存活期间不被释放(引用计数或 epoch-based reclamation);短期可以先用「指针拷贝 + 提前放锁」缩短临界区,收益明显且实现简单。
storage/zstorage/memtable.go:188问题根因:
ScanRange在m.mu.RLock()期间遍历整个 active+dirty 链表并执行 fn 回调。虽然注释说明「热窗口范围扫描有界且短——fn 内若需……应自行拷贝」,但存在两个问题:(1)ScanRange执行期间的 fn 回调(含谓词 Eval + append 拷贝)可能在用户数据量大、谓词评估慢时显著延长持锁时间;(2) 写路径(Put/Delete)与 flush 需要写锁m.mu.Lock(),扫描长持读锁会阻塞写入,热窗口写入是最高频路径,不能容忍写等待。为什么低级解法不够:警告用户「fn 应快速返回」不能从架构上消除问题。全局读锁对写入的阻塞是硬延迟,且随扫描范围和数据量增大而恶化。
架构级方案:快照语义 + 无锁遍历:将 Scan 与写入解耦——在 Scan 开始时获取当前 active/dirty 链表指针的快照拷贝(不拷贝数据,只拷贝顶层指针和 level 元信息),然后立刻释放读锁。遍历在快照上进行(底层节点若被 flush 回收则存在悬空指针风险,所以需要 RCU 或引用计数)。更轻量的中间方案:缩短临界区——先快照链表指针(只需在加锁期间拷贝
head/Next[0]引用),释放锁后再遍历和谓词评估。这样写入不等待整个扫描过程,仅等待指针拷贝的微秒级窗口。代价/收益:快照方案复杂度上升,需要确保链表节点在快照存活期间不被释放(引用计数或 epoch-based reclamation);短期可以先用「指针拷贝 + 提前放锁」缩短临界区,收益明显且实现简单。