题目重点:当每个值 8MB、put 可达一万次且不能淘汰数据时,设计索引、持久化、恢复和读写性能。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 若一万次 put 都写不同 key 且每个 value 为 8MB,原始值约 80GB;实际 key 数、覆盖写、保留版本和压缩率未给。
- “所有数据可访问”解释为当前有效 key 的 value 不得仅因内存压力而被丢弃;remove 是显式删除语义,历史版本是否保留另定。
- 假设本地或共享持久存储可提供足够容量,系统愿意用较高读延迟换内存节省;需要先问 p95/p99 目标与掉电持久性要求。
解题路线
- 把数据面和索引分开:value 写入追加式数据段,内存哈希表记录 key→(segment,offset,length,version,checksum)。存储容量按独立 key 数和版本保留量估,而不是只按调用次数估。
- put 先写 value 与可恢复元数据,再原子切换 key 的最新索引;成功返回的含义是内存可见、写入 OS page cache,还是已 fsync,须签明。重复 key 更新形成旧数据垃圾,由后台 compaction 回收。
- get 先查索引,再按 offset 读取;热门 value 可加可淘汰的内存数据缓存,但淘汰仅影响速度,底层持久副本仍可读。remove 写 tombstone,再从可见索引移除,压缩时清理旧块。
- 启动恢复可从 checkpoint+尾部日志重建索引;checksum 和长度检查防部分写入被当完整对象。并发 put 同一 key 用 version/CAS 或单分区串行化定最后写入。
- 容量和瓶颈估算:80GB 原始值再加复制、日志及临时 compaction 空间;随机 8MB 读取的存储带宽与尾延迟通常比哈希查找成本更关键。
手动走一遍
key A 写入 8MB 时,先把对象落到 segment 7 的 offset X,核对完整后把索引 A 指向 (7,X,8MB,v1)。A 再次 put 新值写在 offset Y,索引原子切到 v2;旧 X 在没有读者引用时由压缩清理。将 A 的 value 从内存缓存驱逐后仍能从 Y 读回。
规模与取舍
哈希索引查找平均 O(1),读/写数据仍需传输 O(value bytes);内存索引 O(K) 随当前 key 数增长,数据存储 O(总有效值+尚未压缩旧版)。若 K 太大无法装内存,需磁盘索引/B-tree 或分片,读写代价随之改变。
易错点与追问
容易失分的边界
- 用 LRU/LFU 直接丢掉唯一的 value 副本,违背全量可访问。
- 把 8MB×10⁴ 简化成固定 80GB 而未问是否重复 key。
- 只保存内存指针,进程重启后无法定位磁盘对象。
- 索引先更新、对象后写且无事务记录,崩溃后读到不完整 value。
- 宣传‘get O(1)’却忽略磁盘 IO 和 8MB 传输时间。
继续深挖
- 若写入持续大于磁盘带宽如何背压?
- 多进程/多机共享该 cache 时谁维护索引权威版本?
- 如何避免 compaction 删除仍被并发 get 读取的段?
- 若要求严格掉电持久与极低延迟,该如何取舍 fsync 批量化?
