米哈游后端开发岗面经合集:分布式锁、层级评论与深分页
- 轮次
- 多轮面试合集
- 时间
- 2026-06
- 来源
- 牛客网
《面试题目》
面经 01(6 月 19 日)
- 消息发送失败怎么办?
- 分布式锁的原理是什么?可能会出现哪些异常,怎么解决?
- 可重入锁是怎么实现的?
- 设计一个层级的评论系统。
- 联查的话会出现什么问题?
- 深分页采用书签方式,多条件怎么办?写下 SQL 语句。
select * from t where a = 1 and b > 1 and c = 1 order by d怎么建索引?- 手撕:最长回文子串。
- 对实习生有什么要求?
面经 02
- 代码阅读:给出几段 Go WaitGroup 的使用场景,分别有什么问题?
- 代码阅读:给出一段与实习项目逻辑类似的业务代码,有什么问题?
《参考解析》
1. 消息发送失败怎么办。 面试官在意的不是「重试三次」这种答案,而是消息从产生到落库这条链路上,哪一步失败会导致状态不一致。可靠做法是本地消息表(事务性 Outbox):业务写库和写消息表在同一个本地事务里提交,再由独立的投递器扫表发送,发送成功才标记;这样「业务成功但消息丢了」和「消息发了但业务回滚」两种情况都不会出现。消费端必须幂等——用业务唯一键(订单号 + 事件类型)做去重表或 Redis SETNX,重复投递只会被丢弃。发送侧再配指数退避重试和死信队列,超过次数进人工兜底;顺序敏感的场景要保证同一业务键路由到同一分区,否则重试会打乱顺序。
2. 分布式锁的原理与异常。 本质是「在共享存储上抢占一个带过期时间的唯一键」。要覆盖的异常有四类:① 锁过期业务没做完——看门狗续期或租约(fencing token)兜底,写入时带上单调递增的 token,存储侧校验旧 token 拒绝;② 主从切换丢锁——Redis 主从异步复制,主节点宕机时锁可能还没同步,RedLock 也只是降低概率,强一致场景要用 etcd/ZooKeeper 或数据库唯一约束;③ 释放了别人的锁——解锁必须比对持有者标识,用 Lua 把「比对 + 删除」做成原子操作;④ 客户端长时间 GC 或网络分区导致自己以为还持锁——同样靠 fencing token 和业务侧幂等兜底。一句话:分布式锁只能提供「大概率互斥」,最终正确性必须落在存储层的条件更新上。
3. 可重入锁怎么实现。 核心是记录「谁持有」以及「持有几次」。Redis 场景用 Hash:field 是客户端唯一标识(uuid + 线程 id),value 是重入计数,加锁时 HINCRBY 并把过期时间刷新,解锁时递减,减到 0 才删 key,整个过程用 Lua 保证原子。JVM 里的 ReentrantLock 则是 AQS 的 state 加 exclusiveOwnerThread,同一线程再次 lock() 只是 state 自增。要注意的是重入计数必须和过期时间一起维护,否则重入多次后一次 unlock 就把锁删掉,或者计数还在锁已经过期。
4. 层级评论系统的设计。 先明确读多写少、层级通常只有两三层(根评论 + 回复),不要一上来就做无限树。表结构用邻接表:comment(id, root_id, parent_id, path, author_id, content, created_at, like_count),root_id 指向楼层根,path 存物化路径(如 1/8/23)。拉某个楼层的全部回复只需 where root_id = ? 一次查出再在内存里按 parent 组装,避免递归查询;按 path 前缀还能快速取子树。删除用软删 + 占位(「该评论已删除」),保证子回复不悬空。热点楼层把前 N 条回复缓存进 Redis,计数用 INCR 异步回写;审核状态单独一列,列表查询走 (root_id, status, created_at) 联合索引。
5. 联查会出现什么问题。 一是笛卡尔积/行数放大:一对多 join 后主表字段重复,分页 limit 会截断在错误的位置,正确姿势是先分页查主表 id 再回表关联,或按一对多子查询聚合。二是 N+1:ORM 懒加载在列表页每条记录再查一次子表。三是索引失效:join 字段类型或字符集不一致会退化成全表扫。四是锁范围扩大:联查里对多张表加锁容易死锁,事务要尽量短。层级评论这种场景,更好的做法是「一次查楼层、一次查回复、内存拼装」,用两次单表查询换掉一次复杂 join。
6. 书签式深分页 + 多条件。 书签分页(keyset pagination)的前提是排序键唯一且稳定,所以排序键要写成 (sort_col, id),翻页条件用「上一页最后一条的值」做严格大于:
SELECT id, content, created_at
FROM comment
WHERE root_id = 1001
AND status = 1
AND (created_at, id) < ('2026-06-19 12:00:00', 88231)
ORDER BY created_at DESC, id DESC
LIMIT 20;
多条件时把所有过滤条件放进 WHERE,把排序键放进 ORDER BY 并建对应联合索引 (root_id, status, created_at, id);(created_at, id) < (...) 是行值比较,MySQL 8 能直接走索引。若要支持「跳到第 N 页」,书签方式做不到,得额外维护序号或改用搜索引擎的 search_after。
7. 这条 SQL 怎么建索引。 条件里 a = 1、c = 1 是等值,b > 1 是范围,order by d 想走索引排序。按最左前缀,等值列放前面、范围列放最后,排序键要接在等值列之后才能避免 filesort。可选方案:(a, c, d) 能让过滤和排序都走索引,但 b > 1 只能回表后过滤;(a, c, b, d) 能过滤三列,但 b 是范围,d 无法继续用于排序,会 filesort。所以常见答案是 (a, c, d, b)(覆盖过滤 + 排序,b 做索引条件下推)或 (a, c, b) 配合业务上接受排序开销,具体看 b 的选择性和结果集大小——用 EXPLAIN 看 type、key、Extra 里的 Using filesort 再定。
8. 最长回文子串。 中心扩展是面试首选:枚举 2n-1 个中心(单字符与双字符间隙),向两侧扩展到不相等为止,时间 O(n²)、空间 O(1),代码短且不易写错。DP 版本 dp[i][j] = s[i]==s[j] && (j-i<2 || dp[i+1][j-1]),同样 O(n²) 但要多一块表。马拉车(Manacher)能到 O(n),需要构造带分隔符的新串并维护回文半径与最右边界,写对不容易,面试里作为加分项提一句即可。
9. 代码阅读题的共同陷阱。 WaitGroup 那几段通常考三件事:Add 必须在 go 之前调用(否则 goroutine 可能先 Done,计数变负 panic);Wait 之后不能复用同一个 WaitGroup 继续 Add(与 Wait 并发是未定义行为);循环变量捕获问题(Go 1.22 之前 for 变量共用地址,闭包里拿到的都是最后一个值)。业务代码 review 则看资源泄漏(连接、文件、goroutine 未释放)、错误被吞、并发读写 map、事务边界过大、以及「先查后改」的竞态——正确写法是条件更新或加锁,而不是 select 完再 update。