01佬滴滴混沌工程一面
项目#
- Feed 流具体内容讲一下。
- Feed 流里引入了多级缓存,Redis 多级缓存具体是什么?
- 缓存一致性能保证吗?
- 用户进来一个请求之后,整个淘汰策略有考虑吗?
- Redis 大 Key 怎么拆分?
- 比如一个 Key 是 String 类型,里面存的是 JSON,数据量很大,可能有 100MB,现在要做拆分,有什么思路?会怎么解决?
- 视频流系统某一天突然很卡,从用户角度看网络延迟很高,站在你的视角怎么排查?排查逻辑是什么?
算法#
- 手写 LRU。
参考答案(AI 生成)#
以下答案由 AI 生成,仅供面试复盘参考。
1. Feed 流具体内容怎么讲?#
答:可以按“发布、分发、读取、互动、缓存、降级”来讲。发布侧写视频元数据,使用 MQ 或 Outbox 把事件投递出去;分发侧按用户规模选择推模式、拉模式或推拉结合;读取侧按时间、热度或关注关系分页拉取;互动侧处理点赞、评论、收藏等计数和状态;缓存侧用 Redis 承接热点列表和视频详情;降级侧准备回源 DB、限流、兜底空页和异步补偿。
2. Redis 多级缓存具体是什么?#
答:常见可以设计成本地缓存 + Redis + DB。L1 本地缓存放极热数据,TTL 很短,减少 Redis 压力;L2 Redis 放热点详情、Feed 列表、排行榜和计数;L3 DB 作为最终数据源。读请求先查本地缓存,再查 Redis,最后回源 DB 并回填缓存。
3. 缓存一致性能保证吗?#
答:缓存一致性通常做到最终一致。常见写法是先更新数据库,再删除缓存,后续读请求回源 DB 并重新回填缓存;删除失败可以通过重试队列、MQ 或 binlog 订阅补偿;缓存再配合 TTL 做兜底。对一致性要求更高的接口,可以读主库或在写请求完成后同步刷新关键缓存。
4. 请求进来后的淘汰策略怎么考虑?#
答:淘汰策略要分缓存层看。本地缓存设置短 TTL 和容量上限,常用 LRU/LFU 控制热点;Redis 层设置合理 TTL,列表类缓存控制长度,比如只保留最近 N 条;数据库层保留全量数据。Feed 流还要区分热数据和冷数据,热数据放 Redis,冷数据走 DB 索引分页,避免缓存被低频老数据撑大。
5. Redis 大 Key 怎么拆分?#
答:核心思路是把一个大 Key 拆成多个小 Key,让读写、迁移、删除都变成小批量操作。集合类大 Key 可以按用户 ID、业务 ID、时间片或 hash 分片拆;列表类可以按页或时间窗口拆;对象类可以按字段模块拆。迁移时采用双写、分批搬迁、灰度切读、校验和异步清理。
6. 100MB String JSON 怎么拆分?#
答:先分析 JSON 结构和访问模式,再把大 JSON 拆成可独立读取的小对象。比如按业务模块拆成 obj:{id}:profile、obj:{id}:stats、obj:{id}:items:{shard};数组字段按页拆成 obj:{id}:items:page:{n};需要整体读取时用版本号或 manifest key 记录分片列表。迁移流程可以先写新旧两套 key,读路径优先读新结构,缺失时回源旧 key 并回填新 key,完成校验后清理旧 key。
7. 视频流系统突然很卡,怎么排查?#
答:按链路分层排查。先确认用户侧指标,比如接口耗时、错误率、超时率、首包时间和地域分布;再看网关和服务端的 QPS、P95/P99、CPU、内存、GC、goroutine、连接数;接着看 Redis 命中率、慢查询、大 Key、带宽、连接池和热点 Key;再看 MySQL 慢 SQL、锁等待、连接池、索引命中和主从延迟;最后看 MQ 积压、消费者速率、CDN/对象存储下载耗时和网络丢包重传。定位后按瓶颈处理,比如扩容、限流、降级、缓存预热、拆大 Key、优化 SQL 或调整超时。
8. 手写 LRU#
答:LRU 用哈希表 + 双向链表实现。哈希表负责 O(1) 定位节点,双向链表负责维护访问顺序;每次读写节点都移动到链表头部,容量满时淘汰链表尾部节点。
type LRUCache struct {
cap int
cache map[int]*node
head *node
tail *node
}
type node struct {
key int
val int
prev *node
next *node
}
func Constructor(capacity int) LRUCache {
head := &node{}
tail := &node{}
head.next = tail
tail.prev = head
return LRUCache{
cap: capacity,
cache: make(map[int]*node),
head: head,
tail: tail,
}
}
func (c *LRUCache) Get(key int) int {
n, ok := c.cache[key]
if ok {
c.moveToFront(n)
return n.val
}
return -1
}
func (c *LRUCache) Put(key int, value int) {
n, ok := c.cache[key]
if ok {
n.val = value
c.moveToFront(n)
return
}
n = &node{key: key, val: value}
c.cache[key] = n
c.addToFront(n)
if len(c.cache) > c.cap {
victim := c.tail.prev
c.remove(victim)
delete(c.cache, victim.key)
}
}
func (c *LRUCache) moveToFront(n *node) {
c.remove(n)
c.addToFront(n)
}
func (c *LRUCache) addToFront(n *node) {
n.prev = c.head
n.next = c.head.next
c.head.next.prev = n
c.head.next = n
}
func (c *LRUCache) remove(n *node) {
n.prev.next = n.next
n.next.prev = n.prev
}