100G缓存系统设计:少读多写场景的工程实践
问题定义
设计一个100G级缓存系统,场景特点:少读多写,需持久化到磁盘。传统内存缓存(Redis/Memcached)不适用。
核心考点
- 写密集型场景的性能瓶颈理解
- LSM-Tree架构原理与应用
- 磁盘IO优化策略
- 多级缓存设计能力
为什么传统缓存不适用?
Redis的困境
- 频繁持久化开销:RDB全量快照阻塞主线程,AOF每秒刷盘也会拖累写性能
- 内存限制:100G数据需要大量内存,成本高昂
- 随机写问题:内存数据最终需落盘,随机写导致磁盘IO瓶颈
少读多写的本质矛盾
- 写操作需要快速响应,避免阻塞
- 读操作少,不需要极致的查询性能
- 数据量大,必须依赖磁盘存储
设计方案:基于LSM-Tree架构
核心组件
1. 内存缓冲层(MemTable)
- 数据结构:跳表(SkipList)或红黑树,保证有序
- 容量:通常64MB~256MB
- 作用:接收所有写请求,提供毫秒级写入响应
2. 预写日志(WAL - Write-Ahead Log)
- 格式:顺序追加的二进制日志
- 作用:
- 保证数据持久性(crash recovery)
- 宕机后可从WAL恢复MemTable
- 策略:每次写操作先写WAL,再写MemTable
3. 磁盘文件层(SSTable - Sorted String Table)
- 特点:
- 不可变文件(Immutable),写一次后不再修改
- 内部数据按key有序排列
- 配备索引块(Index Block)和布隆过滤器
- 分层结构:
- L0层:MemTable刷盘生成,可能存在key重叠
- L1~LN层:通过Compaction合并,保证key不重叠
4. 布隆过滤器(Bloom Filter)
- 作用:快速判断key是否可能存在于某个SSTable,减少无效磁盘IO
- 位置:每个SSTable文件头部或独立索引文件
读写流程
写入流程(Write Path)
1. 写请求到达
↓
2. 追加写入WAL(顺序IO,性能高)
↓
3. 插入MemTable(内存操作,O(log n))
↓
4. 返回成功(延迟<5ms)
↓
5. MemTable达到阈值时,冻结为Immutable MemTable
↓
6. 后台线程异步刷盘,生成L0层SSTable
↓
7. WAL文件可删除
为什么写入快?
- WAL采用顺序写,避免磁盘随机寻址
- MemTable纯内存操作
- 刷盘异步执行,不阻塞写入线程
读取流程(Read Path)
1. 读请求到达
↓
2. 查询MemTable(最新数据)
↓
3. 查询Immutable MemTable(正在刷盘的数据)
↓
4. 从L0到LN逐层查询SSTable:
- 先查布隆过滤器判断key是否存在
- 存在则读取SSTable文件
- 利用索引块定位数据块
↓
5. 返回结果
读取优化:
- 布隆过滤器过滤90%+的无效查询
- 块缓存(Block Cache)缓存热点数据块
- 索引缓存加速key定位
Compaction策略
为什么需要Compaction?
- L0层SSTable越来越多,查询效率下降
- 存在冗余数据(旧版本key)
- 空间放大问题
合并策略
Size-Tiered Compaction(大小分层):
- 选择大小相似的SSTable合并
- 写放大小,空间放大大
- RocksDB默认策略
Leveled Compaction(层级分层):
- 每层数据量限制为上层的10倍
- 空间放大小,写放大大
- LevelDB采用此策略
完整架构设计
三层架构
┌─────────────────────────────────────────────┐
│ Layer 1: Redis热点缓存层(1-10GB) │
│ - 存储热点数据(二八原则) │
│ - 抵消少量读请求 │
│ - 设置合理过期时间 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Layer 2: RocksDB/LevelDB主存储层(100GB) │
│ - LSM-Tree架构 │
│ - 写优化设计 │
│ - 持久化保证 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Layer 3: 后台服务 │
│ - Compaction线程 │
│ - 定期清理过期数据 │
│ - 监控与告警 │
└─────────────────────────────────────────────┘
性能指标
| 指标 | 目标值 |
|---|---|
| 写入QPS | 10万+ |
| 写入延迟 | <5ms (P99) |
| 读取QPS | 1万+ |
| 读取延迟 | <20ms (P99) |
| 数据量 | 100GB+ |
| 可用性 | 99.9% |
高可用保障
数据可靠性
- WAL机制:宕机后从WAL恢复未刷盘数据
- 定期快照:定时备份SSTable文件
- 副本同步:主从复制或Raft共识协议
故障恢复
启动流程:
1. 读取最新的Manifest文件(元数据清单)
2. 加载所有SSTable文件列表
3. 扫描WAL文件,重建MemTable
4. 恢复服务
分布式扩展
当单机无法满足时,可采用:
1. 分片(Sharding)
- 按key范围或哈希分片
- 每个分片独立RocksDB实例
- 路由层负责请求转发
2. 分布式KV存储
- TiKV:基于RocksDB + Raft,支持分布式事务
- Cassandra:原生支持LSM-Tree,AP模型
- HBase:HDFS存储,适合大数据场景
业务场景示例
1. 日志去重系统
- 海量日志实时写入
- 偶尔查询特定日志
- 需要持久化保证不丢失
2. 推荐系统特征存储
- 用户特征频繁更新
- 读取量相对较少
- 需要支持百亿级别特征
3. 时序数据存储
- 监控指标持续写入
- 查询集中在最近时间范围
- 历史数据定期归档
面试加分点
- 明确指出Redis不适合写密集场景:展示对技术选型的思考
- 深入LSM-Tree原理:MemTable → SSTable → Compaction完整链路
- 量化性能指标:具体的QPS、延迟、数据量数字
- 多级缓存设计:不是单一技术,而是组合方案
- 分布式扩展:从单机到分布式的演进路径
总结
少读多写场景的缓存系统设计核心在于:
- 写优化:顺序写WAL + 内存MemTable + 异步刷盘
- 空间优化:Compaction合并,减少冗余
- 读优化:布隆过滤器 + 块缓存 + 热点数据分层
- 可靠性:WAL保证持久化,定期备份保证数据安全
选择LSM-Tree架构而非B+Tree,本质是针对写多读少场景做的取舍,用读性能换取写性能和空间效率。

被折叠的 条评论
为什么被折叠?



