面灵AI→

美团 后端开发 笔试加一面二面 30 题

轮次
笔试+一面+二面
时间
2026-09
来源
牛客网

《面试题目》

笔试

  1. 实现一个购物车合并接口,同一用户多设备加购要合并,写出思路和边界。
  2. 给定订单列表,按城市汇总金额,输出金额最高的 K 个城市。
  3. 一道 AI Coding 题,题干给了一堆需求,你第一步干嘛?
  4. 字符串里找出第一个不重复字符,怎么做到一遍扫描?
  5. 限流怎么做,令牌桶和漏桶你选哪个?
  6. 缓存设计题,热点 key 怎么防被打爆?

一面深挖

  1. 介绍下你最熟的项目,重点说技术选型。
  2. 项目里缓存和数据库双写,有几种保一致的办法,线上你敢用哪种?
  3. 分库分表后跨分片查询怎么办?
  4. 你遇到过最难的线上 bug 是什么,怎么定位的?
  5. 接口耗时突然翻倍,你的排查链路是什么?
  6. 消息队列你用来解耦还是削峰,消费重复怎么处理?
  7. 分布式锁用 Redis 还是 ZK,过期了业务没跑完怎么办?
  8. 你的服务怎么保证幂等?
  9. 限流熔断降级,你们线上怎么配的?
  10. 你做过压测吗,QPS 上不去一般卡在哪?

八股

  1. MySQL 索引为什么用 B+ 树,不用跳表?
  2. 聚簇索引和非聚簇索引区别,回表是什么?
  3. 事务隔离级别,幻读怎么解决?
  4. Redis 数据结构你用过哪些,各自什么场景?
  5. TCP 三次握手,为啥不是两次?
  6. HTTPS 握手过程,对称和非对称分别用在哪?
  7. 进程间通信有哪些方式?
  8. 你了解的操作系统调度策略?

手撕

  1. 【手撕】手写一个固定容量的 LRU 缓存,要求 get / put 都是 O(1),并讲清头尾指针边界。

二面与架构

  1. 项目如果流量翻十倍,架构要改哪几处?
  2. 服务拆分你按什么维度切,怎么定边界?
  3. 你怎么做容量评估和预案?
  4. 你带过人吗,需求排期冲突怎么协调?
  5. 让你从零搭一个高并发网关,你会怎么设计?

《参考解析》

购物车合并的边界才是考点:主体逻辑是「按 userId 归并、以 skuId 为键、数量累加、取最新时间戳」,但边界必须一条条说出来:同一 sku 不同规格(颜色/尺码)要当两行,键得是 skuId 而不是 itemId;取消态的条目不能参与累加;同一 sku 在 A 设备删除、B 设备新增(并发写)要有确定规则,一般以「最后一次修改时间戳」为准,时间戳相同则取删除;数量要夹在业务上下限内(比如单品最多 99 件);合并过程要幂等——用 userId + 设备id 做去重键,或者让客户端带一个合并请求 id,重试不会把数量加两遍。实现上把合并放进一次 Redis 原子操作或数据库唯一键 upsert,别用「先查再算再写」。

Top K 城市汇总:哈希聚合是第一步(O(n)),第二步取前 K 用大小为 K 的最小堆(O(n log K)),比全排序 O(n log n) 好,K 远小于城市数时优势明显。边界有两个:金额可能为负(退款),如果题目要的是「净额最高的 K 个城市」就别先过滤负数,如果过滤了要说明口径;并列金额的排序要有稳定兜底(按城市名再排一次),否则结果不可复现。数据量超过内存时先按城市哈希分片落盘,再对每个分片分别聚合。

第一个不重复字符:哈希表记频次,第一遍扫字符计数,第二遍按原顺序找第一个计数为 1 的,时间 O(n)、空间 O(Σ)(字符集大小,全 ASCII 可以用 256 长度的数组,比哈希表更快)。要做到「一遍扫描」需要额外结构:一边计数一边维护候选队列,遇到计数从 1 变 2 就把候选丢掉,这样摊还下来也是 O(n),但代码复杂度和常数都更差,面试里说清取舍比硬写一遍扫描更好。

缓存与数据库双写一致性:常见方案有四种——先更新 DB 再删缓存(Cache Aside,最常用)、先删缓存再更新 DB(并发下有旧值回填风险)、延迟双删(更新后再延迟几百毫秒删一次,覆盖读请求把旧值写回缓存的窗口)、订阅 binlog 异步删缓存(Canal,业务代码无侵入,是生产上最稳的一种)。线上敢用的组合是 Cache Aside + 缓存过期时间兜底,对一致性要求高的走 binlog 方案;强一致场景(比如余额)干脆不要让缓存承载真值,缓存只做热点加速、读时校验版本。要明确说出「缓存和 DB 无法做到强一致,只能把不一致窗口压到可接受」这句话。

B+ 树为什么而不是跳表:B+ 树的节点大小对齐磁盘页(InnoDB 默认 16KB),一次 IO 读一整页、页内有序数组二分,三到四层就能覆盖千万级数据,且叶子节点用双向链表连起来,范围查询顺着链表扫描即可。跳表是内存结构(Redis 的 zset 用它),靠多层索引把查找降到 O(log n),但没有磁盘页的概念、单次访问要跳很多节点,每个节点还带多层指针,磁盘 IO 次数远高于 B+ 树。另外 B+ 树所有数据在叶子层、非叶子只存键,扇出更高,树更矮。选型的本质是「磁盘 IO 次数」。

接口耗时翻倍怎么排查:先分层定位,别一上来就翻代码。第一步看监控大盘确认是单接口还是全线,是某个机房还是全局,是 P99 还是均值;第二步看依赖,把接口耗时拆成 DB、缓存、RPC、外部 HTTP 几段,哪段涨了一眼就能看到(有全链路 trace 就更快);第三步看变化面——最近有没有发版、上下线、配置变更、流量变化、数据量增长,先怀疑变更;第四步定位到具体依赖后查它自己的指标:慢 SQL 有没有新增(执行计划变没变、索引失效、统计信息过期)、Redis 有没有大 key 或热 key、连接池是否被打满(等待时间上升但执行时间不变是典型信号)、下游是否触发了限流。最后复现和验证:压测环境重放同一批请求,或者用火焰图看 CPU 热点。

LRU 的 O(1) 实现:哈希表 + 双向链表(哨兵头尾节点),哈希表存 key -> 节点,链表按访问时间排序,头部是最近使用、尾部是最久未用。get 命中后把节点摘下来插到头部;put 命中则更新值并移到头部,未命中则新建节点插头部,若超容量就删尾节点并从哈希表移除。用头尾两个哨兵节点可以避免一堆空指针判断,所有指针操作只动 prev/next 两个方向,边界只有「链表为空」和「删的是尾节点」两种情况。Java 里 LinkedHashMap 开 accessOrder=true 并重写 removeEldestEntry 就是现成的,但要先能手写再提库。

get(k):  node = map[k]
         if node == null: return -1
         moveToHead(node); return node.val
put(k,v): if map has k: node.val = v; moveToHead(node)
          else: node = new Node(k,v); map[k]=node; addToHead(node)
                if size > cap: tail = removeTail(); map.remove(tail.key)

流量翻十倍的改造顺序:先加缓存和读副本把读流量挡掉(收益最大、改动最小),再做限流和降级保住核心链路,然后才是拆服务和无状态化水平扩容;写路径上看瓶颈在 DB 还是队列,DB 走分库分表、队列走分区扩容和消费并发;最后补容量压测和预案。顺序很重要,一上来就拆微服务是把复杂度加上去却没解决单点瓶颈。