面灵AI→

小米后端开发二面:推理网关、双写一致与手撕 LRU

轮次
二面
时间
2026-09
来源
牛客网

《面试题目》

  1. 你的模型推理网关是怎么做请求排队的?
  2. 缓存和数据库双写怎么保证一致性?
  3. 分库分表怎么设计?热点账户怎么处理?
  4. 手撕:实现一个 LRU 缓存
  5. 反问:团队的技术栈和方向是什么?

《参考解析》

推理网关的请求排队 推理请求的特点是耗时不可控(取决于生成长度)且成本高,所以排队层要做的不是「先进先出」而是「分级 + 限流 + 快速失败」。可落地的方案:① 优先级队列 + 协程池(Go 里可以用带权重的 channel 或有序堆配 worker pool),交互式请求高优先级、离线批处理低优先级,低优先级任务只在有富余容量时调度,防止高优被饿死;② 长任务异步化——网关只负责受理并返回任务 id,结果通过回调或轮询/SSE 返回,不要占着连接干等几十秒;③ 超时与熔断——给每个下游(模型服务、向量库)设独立超时,用熔断器在下游连续失败时快速失败,配合信号量隔离防止一个慢下游拖垮整个网关;④ 背压——队列必须有界,满了就返回 429 或降级,绝不能无界堆积(无界队列只是把雪崩推迟到 OOM)。

缓存与数据库双写一致性 主流答案是 Cache Aside,而且是先更新数据库、再删除缓存(不是更新缓存)。为什么删而不是写:并发写会导致两次「写缓存」的顺序与「写库」的顺序相反,留下永久脏数据;删除则把下一次读变成一次回源,天然收敛。极端并发下仍有「读线程在写线程删缓存之前读到旧值并回写」的窗口,兜底有两种:① 延迟双删——更新库后删一次,延迟几百毫秒(大于一次主从复制加一次回源的耗时)再删一次,把窗口期回写的脏值清掉;② 订阅 binlog(canal / Maxwell)异步删缓存,把「业务代码是否记得删」从人手里拿走,可靠性最高,代价是引入链路延迟。

读路径还要防三件事:击穿(热点 key 失效瞬间大量请求回源)用 singleflight(同 key 只放一个请求查库,其余等结果)或逻辑过期(value 里带过期时间、物理不过期,发现过期后异步刷新并先返回旧值);穿透(查不存在的数据)用空值缓存加短 TTL 或布隆过滤器;雪崩(大量 key 同时过期)给过期时间加随机抖动,并做多级缓存。

分库分表与热点账户 分片键选得好不好决定后面所有问题。按用户 id 取模(user_id % 1024)的优点是数据均匀、路由简单、同一用户的数据落在同库(避免跨库 join),缺点是扩容要重新分布,通常靠成倍扩容(1024 → 2048)加双写迁移解决,或者一开始就用预分片/一致性哈希,后期只搬分片不 rehash。跨分片查询和分页也要提前想清楚:ORDER BY ... LIMIT 需要归并排序,深分页应改成游标翻页;分布式事务优先用本地消息表或事务消息做最终一致,不要硬上 2PC。

热点账户(大商家、平台账户)是取模方案的死穴——按 id 取模后所有请求打向同一个分片,该分片被单个账户打爆。解法三类:① 加盐散列——把热点账户的余额拆成 N 份(account_{id}_{0..N-1})分散到不同分片,写入时随机选一份扣减、读取时聚合,代价是读要合并、事务要跨分片;② 单独路由——按日交易量阈值识别热点账户,走独立库表加单账户串行化(内存队列 + 定期落库),本质是给热点一条专用通道;③ 业务降级——余额变动改成异步流水加定期对账,放弃实时强一致。

LRU 缓存的手写要点 结构是「哈希表 + 双向链表」:哈希表提供 O(1) 定位节点,双向链表维护访问顺序(头部最新、尾部最旧)。实现时用两个哨兵节点(dummy head / tail)把插入和删除的边界统一掉,这是现场最省事也最不容易翻车的技巧,否则要写一堆 if node == head。get:命中则把节点移到头部并返回值,未命中返回 -1;put:key 已存在则更新值并移到头部,不存在则新建插到头部,若 size > capacity 就删掉尾部节点并从哈希表移除。

要主动说清复杂度(get/put 均摊 O(1))和并发版本(ConcurrentHashMap + 分段锁,或 LinkedHashMap(capacity, 0.75f, true) 直接实现)。面试官常追问「Java 里为什么不用 LinkedHashMap」,可以答「知道它能实现,但手写是为了展示对结构本身的理解,而且 LinkedHashMap 非线程安全、也无法扩展 TTL 和 LRU-K」。若继续问淘汰策略,对比 LFU(按频次,热点更稳但需维护计数堆)、LRU-K(避免偶发批量扫描污染缓存)和 W-TinyLFU(Caffeine 的方案,兼顾频率与新鲜度)。

这场面试的复盘 二面的主线是「项目深聊 + 为什么这么选」,面试官追的是取舍而不是定义。准备方法是给每个技术点准备三层:现象(我遇到什么问题)、方案对比(当时考虑过哪几种、各自代价)、结果与反方案(最后选了什么、线上指标怎么变、如果重选会怎么改)。这套讲法比背八股稳,因为八股答案人人一样,取舍只有真做过的人讲得出来。反问环节了解到的信息(Go 加一点 Rust 做高性能组件)也可以继续深挖,问清 Rust 用在哪个环节、为什么选它,比泛泛问「团队氛围」更有价值。