大厂 Java 面试实录:严肃面试官 vs 搞笑水货程序员谢飞机(音视频弹幕场景)
场景:某互联网大厂「音视频 + 内容社区」业务线。核心链路:直播间弹幕/评论 → Kafka → 弹幕服务 → Redis 热缓存 → MySQL 落库 → 实时风控 → Prometheus 监控告警。
角色:
- 面试官(严肃):问得很深,还会循循善诱。
- 谢飞机(搞笑水货):简单题能答,复杂题开始“云里雾里”,但嘴很硬。
第一轮:直播弹幕链路的基础设计(从能跑到能稳)
Q1:面试官
直播间弹幕量很大,你会怎么设计弹幕服务的入口?Spring MVC 还是 WebFlux?
A1:谢飞机
我一般用Spring MVC,因为我熟……咳,因为它成熟稳定。要是并发特别高,我也可以“考虑”WebFlux,毕竟它……更丝滑?
面试官(引导)
回答方向对。MVC 是阻塞模型,线程数是瓶颈;WebFlux 适合高并发 IO 场景,但要端到端响应式才有效。那弹幕入口你怎么做限流?
Q2:面试官
给我一个你在 Spring Boot 里做限流(Resilience4j)的例子,按直播间维度限流。
A2:谢飞机
限流我会!就是……把请求挡住。Resilience4j 我用过注解:@RateLimiter。然后超了就返回“你太快了”。
面试官(点头)
能用就行,但大厂要讲“按 key 限流”。继续:弹幕要异步落库,为什么你会选 Kafka?
Q3:面试官
Kafka 落地弹幕消息,你怎么保证“不会丢”?至少说清楚 producer 和 consumer 的关键参数。
A3:谢飞机
Kafka 不会丢的吧?它不是很可靠嘛。
producer 我会开acks=all,consumer 我会手动提交 offset……嗯,然后就不丢了!
面试官(表扬 + 追问)
关键点抓到了:acks=all和手动提交。那你怎么做“幂等”,避免重复消费导致重复入库?
Q4:面试官
弹幕消费落库怎么做幂等?给个 MySQL/Redis 方案都行。
A4:谢飞机
幂等嘛,就是“只执行一次”。我可以用 Redis setnx,或者数据库加唯一索引,比如 messageId 唯一,这样插入重复就失败。
当然失败了我就……忽略。
面试官(认可)
这就对了:业务幂等是大厂基本功。
第二轮:性能与一致性(从能稳到能扛)
Q1:面试官
弹幕接口要返回很快,但又要展示“最近 50 条”。你怎么设计缓存?用 Redis 还是 Caffeine?
A1:谢飞机
我选 Redis!因为它在“远端”,大家都能用。Caffeine 是本地缓存,适合单机……不过我也可以两级缓存:Caffeine 顶一下热点,Redis 做共享。
面试官(引导)
思路正确:二级缓存常见。那你怎么避免缓存击穿/穿透?
Q2:面试官
说说缓存穿透、击穿、雪崩,你分别怎么应对?
A2:谢飞机
穿透:查不到也一直查数据库,我就加布隆过滤器(虽然我没手写过)。
击穿:热点 key 过期,一堆人打 DB,我就加互斥锁或者逻辑过期。
雪崩:很多 key 同时过期,我就随机过期时间、加限流降级。
面试官(赞许)
虽然“没手写过”很诚实,但思路完整。接下来:落库用 MyBatis 还是 JPA?连接池你选啥?
Q3:面试官
MyBatis/JPA 你怎么选?连接池 HikariCP 为什么是默认首选?
A3:谢飞机
我选 MyBatis,SQL 更可控,性能调优也直观。JPA 适合简单 CRUD。
HikariCP 快,而且……Spring Boot 默认就是它,我也就默认了。
面试官(补刀)
“默认就用”是水货回答。说一个 HikariCP 的关键点:它做了更少的锁竞争、连接获取路径更短。好,继续:弹幕消息要跨服务调用风控,你用 Feign 还是 gRPC?
Q4:面试官
风控服务是高 QPS、低延迟,你选 OpenFeign 还是 gRPC?为什么?
A4:谢飞机
我一般用 Feign,很方便,写接口就行。
但如果追求性能……gRPC 用 protobuf,传输更小,延迟更低,也更适合内部高频调用。
面试官(认可)
这次回答像个人了。
第三轮:可观测性与事故处理(从能扛到能救火)
Q1:面试官
弹幕延迟突然升高,你怎么定位?你会看哪些指标?Micrometer 怎么接 Prometheus?
A1:谢飞机
我会先看 Grafana 面板:QPS、RT、错误率。
Micrometer 接 Prometheus 就是加依赖,然后暴露/actuator/prometheus。
面试官(继续逼问)
那你怎么把 Kafka 消费延迟也纳入监控?以及链路追踪怎么做?
Q2:面试官
Kafka 的 lag 怎么监控?链路追踪用 Jaeger/Zipkin,你在 Spring Cloud 体系里怎么串起来?
A2:谢飞机
lag 可以看 consumer group 的 offset 差值……用 exporter 或者自己采集。
追踪我知道用 traceId,把它传来传去,然后 Jaeger 就能看调用链。
面试官(点评)
方向对,但描述含糊:大厂要“可操作”。例如用Kafka Exporter+ Prometheus;链路用OpenTelemetry/(或 Sleuth)自动注入 traceId。最后一题:系统降级怎么做?
Q3:面试官
弹幕服务依赖风控服务,风控挂了你怎么办?给个 Resilience4j 的降级例子。
A3:谢飞机
那就降级呗:风控调用失败就默认放行……或者默认拦截?
我觉得默认放行用户体验好。(面试官眼神危险)
代码可以用@CircuitBreaker,fallback 返回一个“通过”的结果。
面试官(严肃)
安全和体验要权衡:通常风控失败要“分级处理”,至少对高风险流量要默认拦截。
行了,今天到这。回去等通知,有消息 HR 联系你。
文章最后:把上面问题的“标准答案”一次讲透(带代码、场景、可落地)
下面按面试链路把关键技术点补齐,让小白也能跟下来。
1)弹幕入口:Spring MVC vs WebFlux 怎么选?
业务场景:直播弹幕接口是典型“高并发 IO”,请求短、响应快,主要瓶颈在网络、序列化、下游依赖(Redis/Kafka/风控)。
- Spring MVC(Servlet):一个请求占用一个线程,吞吐受线程池限制;优点是生态成熟、调试简单。
- Spring WebFlux(Reactive):事件循环 + 非阻塞 IO,适合高并发、长连接/IO 密集;但必须“端到端响应式”(Redis/Kafka 客户端、下游调用也要配套),否则收益有限。
建议:
- 入口是短请求、依赖链路多且大部分组件仍是阻塞 →MVC + 线程池治理更稳。
- 若是长连接(如 WebSocket 弹幕推送)或整体响应式链路 →WebFlux 更合适。
2)限流:Resilience4j 在 Spring Boot 的用法(按接口限流 + 降级)
Maven 依赖
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version> </dependency>配置(application.yml)
resilience4j.ratelimiter: instances: danmakuPost: limitForPeriod: 200 limitRefreshPeriod: 1s timeoutDuration: 0代码(Spring MVC)
@RestController @RequestMapping("/api/live") class DanmakuController { @PostMapping("/{roomId}/danmaku") @io.github.resilience4j.ratelimiter.annotation.RateLimiter( name = "danmakuPost", fallbackMethod = "rateLimited" ) public Map<String, Object> post(@PathVariable long roomId, @RequestBody DanmakuReq req) { // 这里只演示:真实场景会写 Kafka、写 Redis、异步落库 return Map.of("ok", true); } private Map<String, Object> rateLimited(long roomId, DanmakuReq req, Throwable t) { return Map.of("ok", false, "msg", "发送过快,请稍后再试"); } } record DanmakuReq(String content, String msgId, long uid) {}说明:严格“按直播间 roomId 限流”通常要用网关层(Spring Cloud Gateway/Nginx)或自定义 key 维度限流组件;注解限流更像“方法级保护”。
3)Kafka:如何尽量不丢消息 + 不重复入库(至少一次 + 幂等)
Producer:关键参数
acks=all:ISR 全部确认才算成功enable.idempotence=true:幂等生产(避免重试导致重复写入)retries配合幂等min.insync.replicas(broker 侧)要合理配置
Consumer:手动提交 offset(处理成功再提交)
Spring Kafka(示意):
@KafkaListener(topics = "danmaku", groupId = "danmaku-consumer") public void onMessage(ConsumerRecord<String, DanmakuEvent> record, Acknowledgment ack) { DanmakuEvent e = record.value(); // 1) 幂等校验(例如用 DB 唯一键 or Redis SETNX) // 2) 落库成功 // 3) ack.acknowledge() 手动提交 ack.acknowledge(); } record DanmakuEvent(String msgId, long roomId, long uid, String content, long ts) {}幂等落库:MySQL 唯一键(推荐、简单、可靠)
表设计增加唯一索引:
CREATE TABLE danmaku ( id BIGINT PRIMARY KEY AUTO_INCREMENT, msg_id VARCHAR(64) NOT NULL, room_id BIGINT NOT NULL, uid BIGINT NOT NULL, content VARCHAR(255) NOT NULL, ts BIGINT NOT NULL, UNIQUE KEY uk_msg_id(msg_id) );插入时用“插入忽略/冲突不报错”的语义(MySQL 示例):
INSERT INTO danmaku(msg_id, room_id, uid, content, ts) VALUES(?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE msg_id = msg_id;4)缓存:Redis + Caffeine 二级缓存,防击穿/穿透/雪崩
业务目标:读最近 50 条弹幕要快;写弹幕要尽量不阻塞。
- Redis:共享、跨实例
- Caffeine:单机热点缓存,降低 Redis 压力
常用治理
- 穿透:布隆过滤器 / 空值缓存(短 TTL)
- 击穿:热点 key 互斥锁 / 逻辑过期(后台重建)
- 雪崩:TTL 加随机值 + 限流降级 + 多级缓存
5)连接池:为什么 HikariCP 常用
面试可说的关键点(别说“默认就用”):
- 连接获取路径短、优化了锁竞争
- 对超时、泄漏检测等治理能力更完整
- Spring Boot 深度集成,配置简单、性能表现稳定
6)服务间调用:Feign vs gRPC
弹幕 → 风控一般是内部高频调用:
- Feign(HTTP/JSON):开发效率高,调试方便,适合通用业务接口
- gRPC(HTTP/2 + Protobuf):序列化更小、性能更好、适合高 QPS 低延迟、强约束接口
7)可观测性:Micrometer + Prometheus + Grafana + Trace
Prometheus 指标暴露
Spring Boot(Actuator)常见配置:
- 依赖:
spring-boot-starter-actuator、micrometer-registry-prometheus - 暴露端点:
/actuator/prometheus
Kafka lag 监控
落地做法(可操作):
- 部署Kafka Exporter或使用平台自带监控
- Prometheus 抓取 lag 指标(consumer group 的 current offset vs log end offset)
- Grafana 面板告警:lag 持续上升 + 消费耗时增大
链路追踪
- 使用 OpenTelemetry(或 Spring Cloud Sleuth 老体系)自动注入 traceId/spanId
- 上报到 Jaeger/Zipkin,定位慢点在 Redis、DB、风控还是 Kafka
8)降级:风控挂了怎么办(Resilience4j 熔断 + fallback)
典型策略
- 风控不可用时:按风险等级处理
- 高风险(新号、异常 IP、命中历史黑名单)→ 默认拦截
- 低风险(老用户、低频)→ 可降级放行但要打点审计
代码示例(熔断 + fallback)
@Service class RiskClient { @io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker( name = "risk", fallbackMethod = "fallback" ) public RiskResult check(DanmakuEvent e) { // 这里假设调用远程风控服务(Feign/gRPC) return new RiskResult(true, "pass"); } private RiskResult fallback(DanmakuEvent e, Throwable t) { // 示例:按 uid/roomId 做简单分级 boolean allow = e.uid() > 100000; // 演示用:真实应查画像/黑名单/阈值 return new RiskResult(allow, allow ? "degraded-pass" : "degraded-block"); } } record RiskResult(boolean allow, String reason) {}小结:这条链路的“面试官想听什么”?
- 入口模型:MVC/WebFlux 选型要结合端到端阻塞/非阻塞与连接形态(HTTP vs WebSocket)。
- 限流/降级:不仅会用注解,更要讲清“key 维度”“兜底策略”。
- Kafka 不丢不重:
acks=all、幂等生产、手动提交、业务幂等。 - 缓存治理:二级缓存 + 三大问题(穿透/击穿/雪崩)的工程化解法。
- 可观测性:指标 + 日志 + Trace;Kafka lag 要能落到 exporter + 告警规则。
如果你希望下一篇继续这个系列,我可以把场景升级到:
- WebSocket 弹幕推送(背压、连接治理)
- Flink 实时风控(Kafka → Flink → Redis/ES)
- Kubernetes 灰度发布 + 指标回滚策略