美团后端开发一面复盘:从深分页到秒杀扣库存
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍,讲清后端技术栈。
- 手撕:把链表重排成 L0→Ln→L1→Ln−1 的交错顺序。(追问:奇偶长度分别怎么处理?)
- MySQL 深分页为什么慢?limit 越往后越慢要满足什么条件?
- 深分页在真实项目里怎么优化?延迟游标怎么写?
- 缓存击穿怎么防?互斥锁和逻辑过期怎么取舍?
- 更新库和删缓存的顺序怎么定?为什么顺序错了会脏读?
- 分库分表按什么维度切?哈希还是范围?
- 分库之后跨片查询和分布式事务怎么处理?
- AI Coding 题:电商搜索排序,NDCG@10 怎么算?
- 你项目里 RAG 怎么搭的?切块和索引怎么选?
- 项目里记忆和 prompt 管理怎么设计?
- 设计一个行程规划 Agent,输入「周末南京两天预算两千」怎么拆?
- 手撕 LRU。(追问:支持泛型和过期时间)
- BERT 相比 Transformer 编码器做了哪些优化?
- RQ-VAE 跟 RQ-KMeans 差在哪?
- 场景题:热点榜单实时高并发怎么保证?
- 三数之和用双指针怎么写?
- 你项目用 Rust 写部分模块,和 Go 或 Java 比有什么特点?
- 秒杀库存怎么扣才不会超卖?
- 消息队列怎么削峰填谷?消费幂等怎么保证?
- 消费失败怎么重试?死信队列怎么用?
- 分布式锁用 Redis 还是 ZK?过期和续期怎么处理?
- HTTPS 为什么安全?非对称加密用在哪一步?
- 联合索引最左前缀怎么理解?排序怎么走索引?
- 慢 SQL 怎么查?explain 看哪些字段?
- 限流令牌桶和漏桶差在哪?分布式限流怎么搞?
- 反问:「团队目前最紧急的问题是什么?」
- 服务雪崩怎么防?熔断、降级、限流怎么配合?
- CAP 怎么权衡?CP 和 AP 各自适合什么?
- 反问:薪资结构和加班情况。
《参考解析》
链表交错重排:三步走,不用额外空间。第一,用快慢指针找中点,把链表切成前后两半;第二,把后半段整体原地反转;第三,两条链交替穿针——前一半取一个节点,后一半取一个节点,接上。奇偶长度的处理是这题的得分点:长度为奇数时前半段会多一个节点,穿针时以前半段先走、并在后半段耗尽后把前半段剩余的尾巴直接接上;写代码时用「后半段是否为 null」作为循环边界最稳,不要在循环里判断前半段剩余数量。整体 O(n) 时间、O(1) 空间。它由「找中点 + 反转 + 归并」三个基础题拼成,所以面试官真正看的是这三步有没有写干净。
NDCG@10 怎么算:NDCG 是归一化折损累计增益,先把每个位置的增益按位置折损累加,得到一个 DCG,再除以理想排序下的 IDCG 做归一化。折损公式一般用 DCG@k = Σ_{i=1..k} (2^{rel_i} − 1) / log2(i + 1)(用 2^rel − 1 是为了放大高相关文档的差异;也有用 rel_i / log2(i+1) 的简化版,但评分主流的写法是前者)。IDCG 是把同一组文档按真实相关性从高到低排好算出的 DCG。所以 NDCG@10 = DCG@10 / IDCG@10,取值落在 0~1,1 表示排序完全正确。面试口述时要把三个点说清:只算前 10 位(后面的位置不计入,所以截断位置很关键);增益要按位置折损,位置越靠后权重越低(分母 log2(i+1));每个 query 单独算完再在所有 query 上取平均,不能把所有文档混在一起算。它比 Precision@k 更适合排序任务,因为它同时考虑相关性的高低和位置的前后。
秒杀库存怎么扣才不会超卖:核心是让「判断余量」和「扣减」成为一个原子操作,不要先查再扣。三条常见路径:一,Redis 用 Lua 脚本把 GET 余量与 DECR 合成一次原子执行,余量小于 0 直接返回失败,Lua 在 Redis 里是单线程串行执行的,天然互斥;二,数据库用 UPDATE stock SET count = count - 1 WHERE id = ? AND count > 0,靠行锁加条件更新,判断影响行数为 0 即失败;三,数据库乐观锁版本号,WHERE version = ?。工程上还要配套:库存预热进 Redis、请求先过令牌桶限流削掉绝大部分流量、同一用户用唯一键做幂等(避免重复下单多扣)、扣减成功再异步落订单,用 MQ 削峰。只靠数据库行锁能保证不超卖,但峰值会被打穿,所以主流是 Redis 原子扣减挡在前面、数据库做最终一致。
分库分表维度与跨片问题:切分维度要对着查询模式选。按范围切(时间、ID 区间)利于范围查询和冷热分离,但容易热点集中——新写入全落在最新那个分片;按哈希切(用户 ID 取模)数据分布均匀,但范围查询要广播到所有分片。所以真实系统常按业务主键哈希(如买家 ID),再冗余一份按卖家 ID 的异构索引来支持卖家侧查询。跨片查询的应对是:把查询路由到单一分片(设计分片键让最常用的查询落在单片)、对必须跨片的查询走「并行查各分片再内存归并」、对统计类需求走离线数仓或宽表,不要在主交易链路上做跨片 join。分布式事务看一致性要求选:强一致用 Seata 的 AT/TCC 模式(代价是性能与复杂度);绝大多数场景可以接受最终一致,用本地消息表或事务消息 + 幂等消费,把跨库写拆成「本地事务 + 可靠投递 + 对端幂等」。选型原则是不为了少数跨片场景牺牲整体吞吐。
服务雪崩与三件套:雪崩是某个下游变慢或不可用,调用方线程被大量阻塞在等待上,资源耗尽后故障沿着调用链一路上卷,最终整站不可用。三个手段各管一段:限流在入口按 QPS 或并发数把超额流量挡在外面(令牌桶允许突发、漏桶输出恒速),保护自己不被压垮;熔断在下游连续失败超阈值时快速失败、不再真实调用,隔一段时间半开试探,避免把线程浪费在注定超时的请求上;降级是熔断发生后返回兜底结果(缓存旧值、默认值、友好提示)而不是报错。三者必须组合使用:只有限流会在依赖故障时仍把线程耗光,只有熔断会在流量洪峰时不设防。配套要有超时设置(比熔断更基础,很多雪崩的起点就是没设超时)和隔离舱(按依赖划分线程池或信号量,别让一个下游占满所有线程)。
CAP 的权衡:分布式系统里一致性、可用性、分区容忍三者不可同时满足,而分区在真实网络中必然发生,所以工程上实际是在 C 和 A 之间选。CP 优先一致:分区时宁可拒绝服务也不返回不一致数据,适合账务、库存、配置中心这类「错了比不可用更糟」的场景,代表是 ZooKeeper、etcd、Redis Cluster 在强一致配置下的表现。AP 优先可用:分区时继续提供读写、事后收敛,适合社交动态、商品浏览这类「短暂不一致可接受」的场景,代表是 Cassandra、Eureka。补充一点:真实系统往往是按数据分级混用——同一份业务里,金额走 CP、列表页走 AP,而不是整个系统一刀切。
消息队列的幂等与重试:削峰填谷的要点是生产端异步化、消费端按自己的能力控制拉取速率,队列在中间做缓冲区;但队列本身不解决重复投递,所以消费侧必须幂等。幂等的做法是给每条消息一个业务唯一键(订单号、request_id),消费时先用它做一次「插入去重表」的原子操作或 Redis SETNX,成功才执行业务,失败直接 ACK。重试的策略是有限的指数退避,超过次数进死信队列,由人工或补偿任务处理——死信队列的意义就是把「重试到天荒地老」换成「看得见的失败」,否则一条毒消息会阻塞整个分区。还要注意消费顺序:需要保序时按业务键哈希到同一分区,并且不要并发消费同一分区。
Rust 与 Go/Java 的差别:Rust 没有 GC,靠所有权和借用检查在编译期保证内存安全,因此没有 STW 停顿、内存占用可预测,非常适合延迟敏感的底层模块(协议解析、编解码、高频路径);代价是学习曲线陡、编译慢、异步生态的 Send/生命周期约束写起来更啰嗦,开发效率明显低于 Go/Java。Go 的调度器与 goroutine 让并发编程最简单,生态成熟、编译快,适合写中间件和服务;Java 有最完整的生态与 JVM 的 JIT 优化,长稳运行的服务吞吐表现好,但 GC 调优和内存占用是长期负担。选型逻辑是:性能与资源敏感的模块用 Rust,业务迭代速度优先的服务用 Go/Java,两者通过 FFI 或独立进程组合——这也是很多团队的真实做法。