面灵AI→

北京新氧后端开发实习岗一面面经

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 上一段实习的离职原因是什么?
  2. 能否来北京实习?
  3. Go 里的 panic 是怎么一回事?
  4. MySQL 的数据是怎么存储的?
  5. 说一下索引?
  6. 讲一下 MVCC?
  7. Redis 有哪些数据结构类型?
  8. Redis 的持久化方式有哪些?
  9. 说一下你对消息队列的理解?
  10. MQ 怎么保证消息不丢失?

《参考解析》

Go 的 panic 与 recover。 panic 是运行时异常,触发后会沿调用栈向上逐层执行已注册的 defer,直到某一层在 defer 里调用 recover 把它拦下,否则整个进程退出。几个容易答漏的点:recover 只有在 defer 函数中直接调用才有效,包一层函数再调用就失效;panic 之后被 recover 的函数会正常返回,但同一函数里 panic 点之后的代码不会执行;defer 中若发生 panic 会覆盖原来的 panic 值。工程约定上,库和业务函数用 error 返回失败,panic 只留给不可恢复的初始化错误或编程错误;每个自己起的 goroutine 外层要兜 defer + recover 并打日志,因为未捕获的 goroutine panic 会直接杀掉整个进程,HTTP 框架的恢复中间件做的就是这件事。

InnoDB 的数据是怎么存的。 自顶向下是:表空间(.ibd)→ 段 → 区(extent,默认 1MB,64 个连续页)→ 页(16KB,读写的最小单位)。行记录在页内按主键顺序排列,页与页之间用双向链表串起来,页内通过页目录的槽位做二分查找。聚簇索引的叶子节点存的是整行数据,所以按主键查一次就能拿到全部字段;二级索引的叶子只存索引列加主键值,查非索引列要走「二级索引找到主键 → 回表聚簇索引」两步。行溢出(长 varchar、blob、text)时多余部分放到溢出页,主记录里只留前缀和指针。

索引:为什么 B+ 树,以及怎么用。 B+ 树的非叶子节点只存键值和指针、不存数据,扇出非常大,千万行数据树高通常只有三到四层,一次查询的磁盘 IO 次数就等于树高;叶子节点之间是链表,所以范围扫描和排序很友好,这也是它比 B 树更适合数据库的原因。使用时要注意的几件事:联合索引遵循最左前缀,条件里跳过了最左列就用不上索引;范围条件之后的列在索引里无法继续用于定位,只能靠索引下推在引擎层过滤;能覆盖查询所需全部列时走覆盖索引,省掉回表;索引并非越多越好,每个索引都要在写入时维护,还会占空间,所以要根据区分度和实际查询模式取舍。

MVCC 是怎么实现的。 靠「隐藏列 + undo log 版本链 + ReadView」三件套。每行记录有两个隐藏列:最近修改它的事务 id(trx_id)和指向 undo log 中旧版本的回滚指针(roll_pointer);每次修改都把旧值写进 undo log,串成一条版本链。快照读时按 ReadView 判断版本可见性,ReadView 里记录四样东西:当前活跃事务 id 集合、最小活跃事务 id、下一个要分配的事务 id、以及创建者自身的事务 id。从版本链最新的版本往回找,找到第一个「已提交且早于 ReadView 创建」的版本就是可见版本。隔离级别直接决定 ReadView 的生成时机:读已提交(RC)每次快照读都新建一个 ReadView,所以同一事务两次查询可能看到不同结果;可重复读(RR)只在事务内第一次快照读时生成并一直复用,因此能保证重复读一致。另外,select ... for update、update、delete 走的是当前读,读最新版本并加锁,不受 ReadView 影响。

Redis 的数据结构与持久化。 常用类型是 string、list、hash、set、zset,此外还有 bitmap、HyperLogLog、GEO、stream 这些按场景提供的结构。底层并不只是一层:string 用 SDS 保存长度与二进制安全,小对象用 listpack/ziplist 这类紧凑编码省内存,list 用 quicklist(多个 listpack 串成的双向链表)平衡内存与操作复杂度,zset 用跳表加哈希表实现按分值范围查询与按成员查分值的双需求。持久化有两条路:RDB 是某一时刻的全量快照,fork 出子进程配合写时复制生成,文件紧凑、恢复快,代价是两次快照之间的数据会丢;AOF 追加记写命令,appendfsync 有 always/everysec/no 三档,everysec 是性能与可靠性最常用的折中,同时还有重写(rewrite)机制压缩体积,恢复时要逐条重放所以比 RDB 慢。生产上通常是两者同时开启,再加主从与哨兵或集群解决可用性。

MQ 怎么保证消息不丢(以及为什么不重是另一回事)。 要按消息的三段旅程分别答。生产端:发送后必须拿到 Broker 的确认,失败要重试并把最终失败落库或告警;要求严格时用事务消息或本地消息表,把「业务落库」和「消息投递」绑在一个可靠流程里。Broker 端:把消息真正落到磁盘并保住副本——RocketMQ 可以选择同步刷盘加同步双写,Kafka 这类要设 acks=all 并保证 min.insync.replicas 大于 1,否则主节点挂掉时还没同步的消息就没了。消费端:关闭自动提交,业务处理成功后再提交位点,处理失败就重投或进死信队列人工介入。最后一定要补一句:「不丢」和「不重」是两个问题,上面的做法保证的是至少一次投递,必然带来重复,所以消费端必须做幂等(业务唯一键、去重表、状态机前置校验),「至少一次 + 幂等」合起来才是效果上的恰好一次。