来自 #139 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
⚠️ [重要 · 存储] ScanRange 全程持读锁,写入与刷盘被阻塞 storage/zstorage/memtable.go:188
问题根因:ScanRange 在 m.mu.RLock() 保护下完成整个遍历过程(可能涉及数千条记录的 JSON 解析 + 谓词评估)。期间所有写入操作(Put/Delete)以及刷盘操作(StartFlush 内部将 active→dirty)都会因为尝试获取写锁而阻塞。且当前的 firstGTE 遍历跳表的 Next[0] 链表在没有写锁保护的情况下原本是并发不安全的(见下一个 finding)。
为什么低级解法不够:泛解法「加注释说此操作应快」不解决问题的本质。把锁换成 sync.RWMutex 已是最低保障,但读锁持有时间不可控。
架构级方案:此问题与上一个 finding 的快照迭代器方案共享同一解法:创建快照后立即释放读锁。两个问题可一并解决。此外,对扫描这类端到端延迟容忍(几百 ms)但吞吐敏感的操作,可采用无锁遍历 + 版本号校验:遍历前记录跳表版本号(每次写入递增),遍历中不持任何锁,遍历结束后校验版本号是否变化,若变化则重试。在低更新频率的热窗口中,重试概率极低,而吞吐提升显著。
代价/收益:无锁遍历+版本号方案代价在于:重试逻辑增加了代码复杂度、偶发的时间消耗;快照方案代价是内存和实现复杂度。收益是写路径完全不阻塞,适合高写入负载的边缘场景。
storage/zstorage/memtable.go:188问题根因:
ScanRange在m.mu.RLock()保护下完成整个遍历过程(可能涉及数千条记录的 JSON 解析 + 谓词评估)。期间所有写入操作(Put/Delete)以及刷盘操作(StartFlush内部将 active→dirty)都会因为尝试获取写锁而阻塞。且当前的firstGTE遍历跳表的Next[0]链表在没有写锁保护的情况下原本是并发不安全的(见下一个 finding)。为什么低级解法不够:泛解法「加注释说此操作应快」不解决问题的本质。把锁换成
sync.RWMutex已是最低保障,但读锁持有时间不可控。架构级方案:此问题与上一个 finding 的快照迭代器方案共享同一解法:创建快照后立即释放读锁。两个问题可一并解决。此外,对扫描这类端到端延迟容忍(几百 ms)但吞吐敏感的操作,可采用无锁遍历 + 版本号校验:遍历前记录跳表版本号(每次写入递增),遍历中不持任何锁,遍历结束后校验版本号是否变化,若变化则重试。在低更新频率的热窗口中,重试概率极低,而吞吐提升显著。
代价/收益:无锁遍历+版本号方案代价在于:重试逻辑增加了代码复杂度、偶发的时间消耗;快照方案代价是内存和实现复杂度。收益是写路径完全不阻塞,适合高写入负载的边缘场景。