面灵AI→

映客直播 后端二面

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

《面试题目》

一、实习项目

  1. Roaring Bitmap 和一般的 Bitmap 区别在哪里?
  2. 你主要做了数据结构与缓存治理这块的改动是吗?
  3. 整体流程里大概用了哪些中间件?这些中间件具体是干嘛用的、什么场景用的?
  4. 你们是怎么用到 TiDB 的?为什么会用到 TiDB?
  5. 广告推荐的这些数据都是在线数据吗?既然都是在线数据、又不能做到实时,那为什么叫在线数据?
  6. 在线数据和离线数据之间是怎么做处理的?

二、消息队列(Kafka vs Pulsar)

  1. 你都用过哪些消息队列?
  2. Kafka 和 Pulsar 这两个的区别在哪里?
  3. 什么业务场景需要用到这两个消息队列?

三、线上故障应急与运维排查

  1. 假如某个接口上线之后错误率突然升高,你会怎么做?
  2. 假如不能回滚呢?系统不允许回滚时该怎么止损?
  3. 具体怎么降级?这个接口依赖什么东西、业务影响是什么、功能影响是什么?结合业务功能该怎么做服务降级?

四、系统设计题(社交关注关系系统)

  1. 设计一个关注系统(A 关注 B、B 关注 A):表结构和字段怎么设计?
  2. 缓存架构怎么设计?
  3. 接口逻辑怎么设计?
  4. 在粉丝表/关注表中,雪花算法生成的 ID 起到什么作用?

五、研发工作法与 AI 编程工具

  1. 如果给你一个完全没有接触过的业务功能模块,你会怎么上手?
  2. 现在有 AI 了,你觉得在上手新业务模块的过程中 AI 能帮你做些什么?
  3. 你现在写代码都用什么工具?
  4. 你平时的代码生成中,AI 的参与度有多高?

《参考解析》

Roaring Bitmap 与普通 Bitmap 的区别在于「怎么存稀疏」和「怎么算得快」。普通 Bitmap 用一个 bit 表示一个整数,做集合运算(AND/OR/ANDNOT)只要按字长并行处理,速度极快;但它的空间是「最大元素 / 8」字节,当元素稀疏且取值范围很大(比如几亿的用户 id)时会浪费巨量内存。Roaring Bitmap 的做法是把 32 位整数按高 16 位分桶,每个桶内的低 16 位用三种容器之一存:元素少且稀疏用 Array Container(有序 short 数组,二分查找)、元素稠密用 Bitmap Container(固定 8KB 的位图,此时比数组省)、连续区间用 Run Container(存 [start, length] 序列,对连续数据压缩率极高)。这样空间随数据分布自适应,而三种容器之间可以高效地做按桶的集合运算(同类型直接按位运算或归并,跨类型按定义转换后再算)。典型用途是标签过滤、人群圈选、多维 AND/OR 快速下钻——工程上要提的一点是它算得快是因为「延迟计算 + 桶级并行」,容器只在需要时才转成位图,避免了对整段范围的预分配。

「在线数据为什么不能做到实时」这类追问要拆清「在线」的定义。在线数据通常指「服务于在线请求、存在可低延迟读取的存储里」,与之对应的是离线数仓/数据湖;它描述的是存放位置和读取路径,不是数据新鲜度。所以一份每天同步一次的宽表落在 HBase/Redis 里,对在线服务而言仍然是「在线数据」,只是新鲜度是 T+1。真正决定实时性的指标是端到端延迟和更新频率:要做到秒级就得走 binlog/CDC 或消息流,把变更实时投递到在线存储;要做到分钟级可以用微批;T+1 就是离线链路导。这也是「在线与离线之间怎么处理」的答案:离线出全量基准、在线走增量变更,两条链路的产物需要在同一份存储里做合并与版本管理,常见的做法是 Lambda 架构(批层 + 速度层,查询时合并)或 Kappa 架构(只保留流,靠重放历史实现重算),并配合对账任务核对两边的一致性。

TiDB 的选用理由要落到「分库分表的痛点」上。TiDB 是计算存储分离的分布式数据库:TiDB 层(无状态 SQL 层)+ PD(调度与时间戳分配)+ TiKV(Raft 多副本的 KV 存储,按 Region 自动分裂与调度)。它带来的价值是:水平扩展不用改应用(不像 MySQL 分库分表那样要引入中间件、改路由、解决跨片 JOIN 与分布式事务)、支持分布式事务(Percolator 模型,默认乐观事务,也有悲观模式减少冲突重试)、有强一致的副本(Raft 多数派提交)、兼容 MySQL 协议所以迁移成本低。代价是:多一次网络跳转和 Raft 复制导致写延迟高于单机 MySQL、热点 Region 需要打散、大事务和全表扫描要谨慎、运维需要理解 Region 调度。所以合理的选用场景是「单表数据量已经很大、写入持续增长、又要保留 SQL 能力和事务语义」,如果只是读多写少的小表,用 MySQL + 从库更划算。

Kafka 与 Pulsar 的区别要抓住三个本质差异。① 架构:Kafka 是「Broker 同时管计算和存储」,分区和副本绑在 Broker 的本地磁盘上,扩缩容要搬数据;Pulsar 是计算存储分离——Broker 无状态负责协议与分发,存储交给 BookKeeper(Bookie 集群),所以扩容 Broker 不影响存储、存储可以独立扩、Broker 故障时客户端能快速切到另一个 Broker。② 多租户与 topic 数:Pulsar 原生支持 tenant/namespace 多租户和大量 topic(因为它给每个 topic 分配的是 Bookie 上的 segment,不依赖本地目录结构),Kafka 在 topic 数量很多(几千以上)时元数据管理和 rebalance 压力明显。③ 消费模型:Kafka 是分区独占式的消费者组(一个分区只能被组内一个消费者消费,扩消费者不能超过分区数);Pulsar 的订阅模式更丰富,支持 Exclusive、Failover、Shared(多消费者共享同一订阅并行消费同一 topic,相当于队列语义)和 Key_Shared(按 key 保序),这对「同一个 topic 既要广播又要队列」的场景更友好。此外 Pulsar 原生支持延迟消息、死信队列和分层存储(冷数据自动下沉到对象存储)。场景选择:日志采集与流式计算首选 Kafka(生态最成熟、Flink/Spark 集成最好、工具链丰富);多租户平台、topic 数量极多、需要队列语义与延迟消息的场景 Pulsar 更合适;已有业务只需事务消息和延迟消息且团队在阿里系技术栈里,RocketMQ 是最省事的。

接口错误率飙升的应急要按「先止血、后定位、再修复」的时间顺序答,这是这道题的核心。第一步是判断影响面:看监控确定是全量还是部分流量、是全部接口还是单个接口、错误码集中在 5xx 还是 4xx、影响哪些用户和地区,同时看这个接口的下游依赖(数据库、缓存、第三方)。第二步止血,优先级排序是:① 有开关就关(功能降级/熔断,先保住主链路);② 能回滚就回滚(配置回滚通常比代码回滚快得多);③ 如果是流量问题就限流(按用户/接口/来源分级限流,保核心用户);④ 如果是下游故障就走降级路径。第三步定位:对比变更时间线(最近的发版、配置变更、DBA 操作)、看错误堆栈与日志的突增模式、看资源水位(连接池、线程池、GC、CPU)、看依赖的 RT 与错误率,必要时用灰度实例或本机复现。

「不能回滚怎么止损」和「具体怎么降级」要给出可操作的动作。不能回滚时:① 关掉新功能入口(前端隐藏入口、后端开关拦截),让代码留在但流量不进;② 改配置绕过(关掉新引入的依赖调用、把新逻辑的开关关掉);③ 限流 + 排队,把并发度降到系统能承受的水平;④ 准备热修(小改动直接改线上配置或用脚本补数据/回滚数据);⑤ 必要时切流量到备用集群或旧版本实例。降级必须按业务功能分级,答题时给一个分级清单:核心不可降级(涉及资金、下单、登录),可降级为非实时(推荐、排行榜、搜索联想改成缓存或静态结果),可降级为不可用(活动页、次要推荐位、数据看板),可关闭(埋点上报、非关键异步任务)。设计上要有:降级开关的集中配置与秒级生效、降级的自动化触发条件(错误率/RT 阈值)、降级期间的用户提示、以及恢复要能自动或一键完成。最后补一句「降级方案要提前演练,否则真出事时开关没人敢按」。

社交关注关系的系统设计分三块答。表结构:常见两种——follow(follower_id, followee_id, created_at) 单表存关系(双向查询靠两个索引:(follower_id, followee_id) 查我关注了谁、(followee_id, follower_id) 查谁关注了我),或者拆成 following 和 follower 两张表(写入时双写,读更快但一致性要维护)。大 V 关注关系量极大时需要按 follower_id 分片,粉丝列表用「写扩散到缓存」而不是实时查库。缓存架构:关注列表和粉丝列表都是典型的热点读、低频写场景——用 Redis 的 Set 或 ZSet(ZSet 用关注时间的 score 可以天然支持按时间排序分页)存关系,大 V 的粉丝列表可能超大,要用分页缓存或只缓存最近 N 页;计数(关注数/粉丝数)单独用计数器维护并定期与数据库对账修正(因为并发增减不可避免会漂移)。接口逻辑:关注要幂等(重复关注不报错也不重复计数,靠唯一索引或 Redis Set 天然去重),关注和取消关注要保证「关系表 + 双端计数 + 缓存」三个写入的一致性——推荐做法是以数据库唯一索引为准,缓存只做加速并允许短暂不一致,计数用异步补偿或定时对账。另外要处理的黑名单/互相关注状态、隐私设置(对方隐藏关注列表)、以及大量小请求的批量接口(一次查多个人的关注状态)。

雪花算法 ID 在这里的作用:关注关系表的自增主键在分库分表下会冲突、也难以按用户维度路由,雪花 ID 提供「全局唯一 + 趋势递增 + 本地生成不依赖数据库」的主键,适合分布式写入;趋势递增(高位是时间戳)这一点对 B+ 树索引也友好,能避免像 UUID 那样随机插入造成的页分裂。同时它还能从 ID 反解出生成时间,在排查问题时有用。要注意的坑:依赖机器时钟,时钟回拨会导致 ID 重复,需要做检测和等待/降级处理;机器号位数有限,大规模集群要分配好 workerId;ID 会暴露业务量级和生成时间(百度之类的公司会用号段模式或做混淆)。如果业务只需要唯一性、不需要按时间排序,用号段模式(一次从数据库取一批 ID 缓存本地)也是常见且更简单的方案。

AI 编程工具这一组问题要答得具体,避免空泛。「上手陌生模块」的标准路径是:先看文档和 README 建立概念模型 → 用日志/断点跟一条真实请求走完全链路 → 画出依赖图和核心数据结构 → 找最熟悉这块的人问边界问题 → 从最小的改动开始试。AI 在这里能帮上什么:快速解释陌生的一段代码或配置的作用、按自然语言把调用关系梳理成流程、生成某个函数的调用方/被调用方的检索脚本、把报错信息翻译成可能的原因清单、以及写一些一次性的验证脚本和小工具。边界要说清:AI 给的代码仓库理解经常是「看起来对但落点错了」,尤其是跨模块的隐式约定(谁写的、谁读的、什么时候清理)它看不到,所以它的输出必须回到代码和运行结果里验证。关于「AI 参与度」,诚实的答法是分层:样板代码、CRUD、单测草稿、正则和脚本可以高比例交给 AI;涉及并发、事务、权限、资金和线上配置的部分自己写自己审;不管参与度多高,最终责任在自己,所以「AI 写的代码怎么 review、怎么证明测试没有迎合代码」这类追问要能答上来——答案是把验证建立在独立的事实上(对着接口契约写测试、跑真实链路探针、做故障注入),而不是只让 AI 自己证明自己。