北京新氧后端开发实习岗一面面经
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 上一段实习的离职原因是什么?
- 能否来北京实习?
- Go 里的 panic 是怎么一回事?
- MySQL 的数据是怎么存储的?
- 说一下索引?
- 讲一下 MVCC?
- Redis 有哪些数据结构类型?
- Redis 的持久化方式有哪些?
- 说一下你对消息队列的理解?
- 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,否则主节点挂掉时还没同步的消息就没了。消费端:关闭自动提交,业务处理成功后再提交位点,处理失败就重投或进死信队列人工介入。最后一定要补一句:「不丢」和「不重」是两个问题,上面的做法保证的是至少一次投递,必然带来重复,所以消费端必须做幂等(业务唯一键、去重表、状态机前置校验),「至少一次 + 幂等」合起来才是效果上的恰好一次。