背景
json·from 写入前不清空目标子树,只写新成员并覆盖 marker。旧成员的 key 仍物理存在。
实测(8 组覆盖写,第一次写 A、第二次同 root 写 B):
| 场景 |
A → B |
读回 |
obj-to-scalar |
{"a":{"x":1}} → {"a":1} |
{"a":{"x":1}} ❌ 读到旧结构 |
shrink-obj |
{"a":1,"b":2,"c":3} → {"a":9} |
{"a":9} ✅ 逻辑正确 |
shrink-array |
{"a":[1..5]} → {"a":[1]} |
{"a":[1]} ✅ 逻辑正确 |
两类后果:
(1) 脏读(正确性)。 obj-to-scalar 时新值写在 root·a(标量),但旧的 root·a· marker 与 root·a·x 仍在;readValue 先探 marker,命中旧 objindex 就走 readObj,标量新值被完全遮蔽。
(2) 孤儿键(存储泄漏)。 逻辑正确的场景下物理垃圾照样留存:
$ redis-cli --scan --pattern '*ow-shrink-obj*'
/cx/ow-shrink-obj· ← marker,成员表只剩 a
/cx/ow-shrink-obj·a
/cx/ow-shrink-obj·b ← 孤儿
/cx/ow-shrink-obj·c ← 孤儿
$ redis-cli --scan --pattern '*ow-shrink-array*'
/cx/ow-shrink-array·a·[0]
/cx/ow-shrink-array·a·[1..4] ← 孤儿(marker 已是 map[1]{1})
list 走 marker body,所以逻辑视图干净,垃圾只在物理层累积——反复导入同一 root 会单调增长。
需求描述
json·from(src) -> root 的语义应是「root 子树等于 src」,而非「把 src 合并进 root」。
预期结果
- 写入后 root 子树与源 JSON 精确对应,无残留 key。
obj-to-scalar 读回 {"a":1}。
- 重复导入同一 root 的物理 key 数不随次数增长。
方案简述
写入前对 root 执行子树删除(kvspace 侧已有 deltree 语义,需确认 dispatch ABI 是否暴露);若不暴露,则在 writeValue 里对每个被覆盖的节点按旧 marker 成员表递归删除。
需要在方案文档里明确:json·from 是覆盖语义还是合并语义。二者都可实现,但必须择一并写死,不能靠调用方猜。
背景
json·from写入前不清空目标子树,只写新成员并覆盖 marker。旧成员的 key 仍物理存在。实测(8 组覆盖写,第一次写 A、第二次同 root 写 B):
obj-to-scalar{"a":{"x":1}}→{"a":1}{"a":{"x":1}}❌ 读到旧结构shrink-obj{"a":1,"b":2,"c":3}→{"a":9}{"a":9}✅ 逻辑正确shrink-array{"a":[1..5]}→{"a":[1]}{"a":[1]}✅ 逻辑正确两类后果:
(1) 脏读(正确性)。
obj-to-scalar时新值写在root·a(标量),但旧的root·a·marker 与root·a·x仍在;readValue先探 marker,命中旧 objindex 就走readObj,标量新值被完全遮蔽。(2) 孤儿键(存储泄漏)。 逻辑正确的场景下物理垃圾照样留存:
list走 marker body,所以逻辑视图干净,垃圾只在物理层累积——反复导入同一 root 会单调增长。需求描述
json·from(src) -> root的语义应是「root 子树等于 src」,而非「把 src 合并进 root」。预期结果
obj-to-scalar读回{"a":1}。方案简述
写入前对 root 执行子树删除(kvspace 侧已有 deltree 语义,需确认 dispatch ABI 是否暴露);若不暴露,则在
writeValue里对每个被覆盖的节点按旧 marker 成员表递归删除。需要在方案文档里明确:
json·from是覆盖语义还是合并语义。二者都可实现,但必须择一并写死,不能靠调用方猜。