BIGO 后台开发 秋招一面面经
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 请先做一下自我介绍
- 如果一个 Java 服务需要同时对接多个外部 API,你会如何设计调用层?
- 如果让你设计一个面向直播业务的实时互动服务,你会如何拆分系统?
- 当系统需要在多个模型或多个服务之间进行切换时,除了切换接口地址,还需要考虑什么?
- 长文本请求超过上下文限制时,如何在不明显损失业务信息的情况下完成处理?
- 在一个高并发接口中,如何区分吞吐量、并发数、响应时间和处理能力?
- Java 服务中的 Token、字节数和字符数有什么区别?如何避免容量计算错误?
- Java 中如何定位一次疑似内存泄漏?哪些对象最容易被长期持有?
- volatile 能保证哪些事情?为什么它不能替代锁?
- happens-before 规则在并发问题中有什么作用?
- Java 中如何排查锁竞争严重的问题?
- 线程池参数应该如何设置?为什么不能盲目把线程数调大?
《参考解析》
- 多外部 API 的统一调用层:不要在业务代码里散落 HTTP 调用,而是每个外部服务一个 Adapter,实现统一接口,把连接池管理、超时控制、重试、认证、序列化和异常转换都收在适配层。响应结构差异在适配层转成内部领域对象,这样外部 API 改字段不会直接穿透到业务层。异常要分类——连接超时、读取超时、服务端 5xx、业务错误码、客户端参数错误,重试策略必须按类型区分:连接失败和 5xx 可以重试,参数错误重试一百次也是错的,业务错误要看幂等性。
- 直播实时互动服务怎么拆:先按「连接」和「业务」分开——长连接接入层负责协议解析、心跳、鉴权和连接管理,可以无状态横向扩容;业务逻辑层处理房间、礼物、弹幕、榜单;推送层负责把消息按房间维度扇出。中间用消息队列解耦,热点房间单独做隔离(独立 Topic 或独立消费组),避免一个大房间把全站消费能力吃满。状态上要区分「必须强一致的」(余额、礼物扣减)和「可以最终一致的」(在线人数、热度值),前者走数据库 + 分布式锁或事务消息,后者走内存 + 定期落盘。
- 多模型 / 多服务切换要考虑什么:协议、响应结构、超时策略、错误码、上下文兼容性、能力差异这六项缺一不可。参数结构不同就不能只改 URL 复用原请求,必须在适配层做参数转换和响应归一化。切换场景包括主服务故障降级、按请求类型路由到专用服务、负载过高时分流、新旧版本灰度,以及按任务复杂度选不同服务。最关键的一条是:降级之后要明确哪些能力丢失了,并在响应里如实体现——比如主服务支持复杂排序、备用服务只支持基础查询,那就返回受限结果,而不是伪装成完整成功。
- 超长文本怎么进上下文:不能简单截断头尾。做法是先识别任务目标,再按相关性、时间和结构分层压缩:第一层抽结构化信息(实体、时间、金额、状态、约束条件),第二层对普通文本做摘要,第三层保留与当前问题直接相关的原文片段,最后重组成模型能吃的上下文。关键字段必须结构化保存,不能只留在自然语言摘要里——摘要一旦丢信息就再也找不回来了。多来源文本要带来源 ID 和版本号,否则结论的依据事后无法核对。
- 吞吐量、并发数、响应时间、处理能力:并发数是同一时刻正在处理的请求数;吞吐量是单位时间完成的请求数;响应时间是单个请求的耗时;处理能力是在满足稳定性和质量要求下能持续承载的最大吞吐量。四者不是线性关系,Little 定律给出
L = λW(平均并发数 = 吞吐量 × 平均响应时间)。如果响应时间因为锁竞争或下游阻塞上升,并发数会堆积但吞吐量未必增加——这正是系统开始劣化的信号。压测要看 P95 / P99,只看平均值会把长尾藏起来。 - Token / 字节数 / 字符数:字符数是语言层面的(
String.length(),注意它数的是 UTF-16 code unit,emoji 会算成两个);字节数取决于编码,UTF-8 下一个中文通常 3 字节;Token 是模型或协议层面的切分单位,同一段文本在不同 tokenizer 下数量不同。三者不能互相换算。算网络传输要显式指定StandardCharsets.UTF_8;算模型输入限制必须用对应模型的 tokenizer 跑一遍,不能拿字符串长度估算;算数据库字段或 MQ 消息大小时按实际编码后的字节数判断。 - 内存泄漏定位:先区分真泄漏和正常堆增长——正常流量上涨会让堆占用升高,但 Full GC 之后应该回落;如果 Full GC 后占用持续上升,才更像对象被错误持有。排查手段是 GC 日志看老年代增长趋势,
jmap -dump或-XX:+HeapDumpOnOutOfMemoryError拿堆转储,再用 MAT 看 dominator tree 和对象引用链。最容易被长期持有的对象:不断追加的静态集合、没remove()的 ThreadLocal、线程池任务里捕获的大对象、没有容量和过期限制的本地缓存、注册后没注销的监听器、以及被旧线程持有的类加载器。 - volatile 的语义边界:volatile 保证可见性(写立刻对其他线程可见,基于内存屏障和缓存一致性协议)和一定程度的有序性(禁止特定重排序),但不保证复合操作的原子性——
count++是读改写三步,多线程同时执行照样丢更新。它适合「一个线程写、多个线程读」的状态标记(比如停止标志、双重检查锁的实例引用)。要保证复合状态一致性,用synchronized、显式锁或原子类。 - happens-before 的作用:它定义的是「前一个操作的结果对后一个操作一定可见,且执行顺序在前」。主要规则包括:同一个线程内的程序顺序;对同一把锁的解锁 happens-before 后续加锁;对 volatile 变量的写 happens-before 后续读;线程 start 前的操作 happens-before 新线程内的操作;线程内所有操作 happens-before 其他线程成功从 join 返回;以及传递性。排查并发问题时它比「代码看起来是这个顺序」可靠得多——没有建立 happens-before 关系的两段代码,编译器和 CPU 都可以重排。
- 锁竞争怎么排查:先用
jstack抓线程 dump 看有多少线程长期处于BLOCKED,再结合 JFR 或 Async Profiler 看锁等待时间和热点锁。要重点检查:同步范围是不是过大(把网络请求或日志写在锁里)、多个无关业务是不是共用同一把锁、有没有锁嵌套或锁升级、被保护的数据结构能不能换成无锁或分段结构(LongAdder、ConcurrentHashMap)。改法是缩小临界区——先做不依赖共享状态的远程调用,再用小同步块更新本地状态,但改完必须重新验证一致性,别为了性能破坏原子性。 - 线程池参数怎么定:看任务类型。CPU 密集型线程数接近核数(过多只会增加上下文切换);IO 密集型可以按
核数 × (1 + 等待时间/计算时间)估算并实测调整,但真正的上限往往在上下游——数据库连接数、下游接口 QPS 限额。除了核心 / 最大线程数,队列容量、空闲存活时间、拒绝策略、任务超时、线程命名和监控指标都要一起设。盲目调大线程数的后果是:下游被打挂、上下文切换开销上升、任务积压反而更严重。队列必须是有界的,无界队列会把 OOM 风险藏起来。