谢飞机面试记:从音视频到电商,一场覆盖Spring Cloud、Redis、JVM与Kafka的Java大厂求生指南
谢飞机面试记:从音视频到电商,一场覆盖Spring Cloud、Redis、JVM与Kafka的Java大厂求生指南
清晨九点,某互联网大厂面试间。面试官老张板着脸,手里捏着一份简历,对面坐着头发微乱、背着双肩包的谢飞机。简历上写着“精通Java、熟悉微服务、做过电商项目”,实际经验全来自B站教程和网上开源项目。
“谢飞机是吧?先别紧张,我们随便聊聊。”老张推了推眼镜,“你简历上写熟悉Java SE 8/11/17,那你说说,Java 8的Stream和Java 17的Stream有什么不同?如果让你处理一段音视频文件的元数据解析,你会怎么用Stream?”
谢飞机眼睛一亮:“Stream嘛,就是流!可以用filter过滤,map转换,collect收集!音视频元数据……呃,比如提取所有视频的时长,先map到时长字段,再filter大于10秒的,最后collect到List!Java 17没太大区别吧,就是多了几个方法,比如toList(),反正我平时都用8。”
老张点点头:“基础还行。那如果这个音视频元数据是一个大文件,几百万条记录,你直接用Stream会不会内存爆掉?”
谢飞机挠头:“那……那就用parallelStream?并行处理!再不行就分批呗。哦对,可以用Files.lines(),那是个懒加载的流,不会一次性全读进来。哈哈,我学过!”
“不错。”老张难得露出一丝笑意,“那你用过多线程吧?JVM里堆和栈有什么区别?一个线程的栈上能放对象吗?”
谢飞机立刻挺起胸膛:“堆是对象共享的地方,栈是每个线程私有的,存局部变量和引用。栈上能放对象?……嗯,理论上不能吧?不对,JIT逃逸分析后对象可能分配在栈上!我背过,标量替换!但是具体怎么替换的……反正就那个意思。”
老张没追问,翻了翻简历:“行,我们进入正题。假设我们做一个内容社区UGC平台,用户上传短视频,后端要有审核、转码、评论、点赞。你会怎么设计服务拆分?用什么技术栈?”
谢飞机搓搓手:“微服务嘛!Spring Boot + Spring Cloud,注册中心用Consul,服务间调用用OpenFeign,负载均衡用Spring Cloud LoadBalancer,网关用Spring Cloud Gateway,链路追踪用Jaeger,监控用Micrometer + Prometheus + Grafana。视频处理可以单独拆一个服务,参考FFmpeg调用外部工具。评论和点赞用Redis,消息用Kafka异步解耦。”
“哦?”老张眯起眼,“那如果用户同时疯狂点赞一个热门视频,Redis缓存击穿或雪崩了,你怎么处理?”
谢飞机:“缓存击穿?就是热点key失效瞬间大量请求打到DB对吧?解决方法……加互斥锁!或者热点key不设置过期时间。雪崩就是大量key同时失效,那就随机过期时间!还可以用Redis集群高可用。”
“那如果Redis挂了怎么办?”
“挂了……那就挂了呗……不,可以本地缓存兜底,用Caffeine做多级缓存。不过数据一致性又不好保证……呃,我是说可以用分布式锁加双写策略,比如先更新数据库再删缓存,延迟双删!虽然可能删失败,但可以配合消息队列重试。”谢飞机越说越虚。
老张没评价,继续问:“说到消息队列,Kafka在视频上传场景怎么用?如果转码服务消费失败,你怎么保证不丢消息?”
谢飞机来劲了:“生产者发消息到Kafka,消费者订阅主题,开启手动commit,处理成功再提交offset,失败就重试!实在不行重新投递到死信队列。”
“那重试次数太多阻塞了后续消息怎么办?”
“那就用线程池异步处理?或者把消息延迟放到另一个topic?”谢飞机额头冒汗,“反正就是不能丢数据……Kafka可以做幂等生产者,副本机制,ISR同步,Leader选举,zero copy,顺序写……我知道的!但是具体配置我有点记不清了。”
“好,我们聊点别的。”老张拿起一杯水,“你简历上写了‘熟悉电商订单系统’,那你说说,一个电商下单流程涉及哪些表?事务怎么控制?”
谢飞机背过八股,张口就来:“订单表、订单明细表、库存表、支付流水表、优惠券表。用@Transactional,先扣库存,再创建订单,然后调用支付服务。库存不足就抛异常回滚——分布式事务就用Seata,AT模式或者TCC,或者用事务消息做最终一致性。”
“那如果下单时需要扣减Redis库存,同时写数据库,还要给用户发Kafka消息通知快递物流,你怎么保证一致性?”
谢飞机:“呃——先扣数据库库存,再删Redis?不行……应该先写本地消息表,再发Kafka?还是先发Kafka再更新数据库?我记得可以用本地消息表加上消息队列实现最终一致性:先update数据库,然后insert一条消息表,再定时任务扫消息表发送MQ。但是怎么保证update和insert同一个事务?那就用@Transactional,没问题!”
“那你觉得MySQL的隔离级别默认是什么?如果订单状态更新为‘已支付’,同时用户查询订单,会出现什么现象?”
谢飞机:“MySQL默认是可重复读。已支付是另一个事务提交后的状态,可重复读下同一事务多次查询结果一样,所以不会查到已支付?不对,如果查询的事务在更新之前开启,那确实看不到;如果更新事务提交后开启新查询就能看到。这其实不会读到脏数据,因为已提交。可重复读解决了脏读和不可重复读,但是可能幻读,InnoDB在可重复读下用间隙锁避免部分幻读。嘿嘿,我熟。”
老张又问了:“你用过MyBatis,那#{}和${}的区别是什么?如果要做动态排序,你能直接用${}吗?”
谢飞机:“#{}是预编译占位符,防止SQL注入;${}是字符串拼接,有风险。动态排序如果直接拼列名或字段,用${}可以做,但要白名单校验,不然会被注入。我一般用枚举映射!安全!”
“不错。”老张第一次露出赞许的表情,“那我们进入最后一个环节,你还会哪些框架?”
谢飞机开始滔滔不绝:“测试用JUnit5、Mockito、AssertJ;日志用Log4j2和SLF4J;模板引擎用Thymeleaf和FreeMarker;API文档用Swagger;序列化用Jackson和Protobuf;CI/CD用GitLab CI和Docker K8s;大数据用Spark Flink;爬过Elasticsearch;用过Redisson;还有Netty、WebSocket、R2DBC……我都听说过!”
老张揉了揉太阳穴:“那你如何在Spring WebFlux里调用一个阻塞的第三方视频审核接口?使用阻塞线程池有什么风险?”
谢飞机:“WebFlux是响应式编程,不能用阻塞调用。可以用limitRate加subscribeOn,把阻塞调度到专属线程池,用elastic调度器,但会占线程。风险就是线程池耗尽吧——那就增大线程池?或者用enableBlockingFirstOperator?不对,那是Mono。呃,实际项目我没用过,只写过hello world。”
“好吧。”老张合上笔记本,“我们今天的面试就到这里。你回去等通知吧,我们大概三到五个工作日会有结果。”
谢飞机一激灵:“谢谢面试官!请问您的通知是邮件还是电话?”
老张面无表情:“取决于你的简历是海投还是内推。下一位。”
完整技术答案解析
以下是本次面试中涉及的业务场景与技术点详解,适合小白系统学习。
第一轮:Java基础与音视频元数据处理
1. Stream API 在 Java 8 与 Java 17 中的区别
- Java 8 的
Stream支持filter、map、collect,但collect需要传入Collectors.toList()等收集器。 - Java 16+ 引入
Stream.toList(),直接返回不可变 List,代码更简短。 - Java 17 还增强了
Stream.toList()的不可变性(不允许添加、删除元素)。 - 对于大文件(如几百万条音视频元数据),应使用
Files.lines(Path)获取行流,它是一个惰性求值的流,按需读入内存,而不是一次性把所有数据加载到 List。配合filter和limit可以尽早短路,避免 OOM。 - 注意
parallelStream()并行流对于 I/O 密集型操作不一定是好事,默认使用公共 ForkJoinPool,可能会和其他任务互相干扰;CPU 密集计算时可适当使用,但线程数不好控制。生产环境推荐自定义线程池。
2. JVM 堆与栈,以及对象能否在栈上分配
- 堆(Heap)是所有线程共享的内存区域,用来存放对象实例和数组。GC 管理的主要区域。
- 栈(Java Virtual Machine Stack)是线程私有的,每个方法调用都会创建一个栈帧,里面存放局部变量表(基本类型变量、引用)、操作数栈、方法返回地址等。
- 局部变量如果是
new出来的对象,变量本身在栈上,对象本体在堆上。 - 通过 JIT 编译器进行逃逸分析,如果对象只在方法内部使用,且没有逃逸出方法,可能被优化为栈上分配或标量替换(把对象拆成多个局部变量),减少堆分配和 GC 压力。但这只是 JVM 的优化手段,并非 Java 语言规范强制。
- 栈能放对象(逃逸分析后),但严格说不是“放整个对象”,而是通过标量替换把对象内部字段直接展开为局部变量。
第二轮:内容社区 UGC 平台与缓存、消息队列
1. 微服务拆分与注册中心、调用链
- 内容社区包括:用户服务、内容发布服务、审核服务、转码服务、评论服务、点赞服务、推荐服务。
- 技术选型:
- 注册中心:Consul 或 Nacos(更常用)。
- 服务调用:OpenFeign + Resilience4j 做熔断限流降级。
- 网关:Spring Cloud Gateway(而不是 Zuul,Zuul 已停产)。
- 链路追踪:Jaeger / Zipkin,配合 Micrometer 上报指标到 Prometheus,Grafana 展示。
- 视频上传流程:客户端上传视频到 OSS,后端返回上传 ID;后端发送“视频上传完成”消息到 Kafka;转码服务消费消息,调用 FFmpeg 转码,更新转码状态;审核服务异步审核。
2. Redis 缓存击穿与雪崩
- 缓存击穿:某一个热点 key 失效,瞬间大量请求直接打到数据库。
- 解决:互斥锁(SETNX 锁住后重建缓存);热点 key 不设置过期时间(逻辑过期);用
cache-aside模式但加锁回源。
- 解决:互斥锁(SETNX 锁住后重建缓存);热点 key 不设置过期时间(逻辑过期);用
- 缓存雪崩:大量 key 在同一时间过期,或者 Redis 宕机。
- 解决:缓存过期时间加上随机值(比如 300~600 秒随机);Redis 主从 + 哨兵 / 集群;多级缓存(Caffeine 本地缓存 + Redis 远程缓存)。
- 数据一致性:更新数据库后删除缓存,如果删除失败,可以用延迟双删(先删缓存,更新 DB,再延迟 500ms 删一次);更可靠的是监听 binlog(Canal)或使用事务消息后异步删缓存。
3. Kafka 消息可靠性与消费失败处理
- 生产者可靠性:设置
acks=all,启用幂等enable.idempotence=true,重试次数和重试退避合理配置。 - 消费者可靠性:使用手动提交 offset
enable.auto.commit=false,在处理完业务成功后调用commitSync。 - 消费失败处理:在消费者中 catch 异常,记录重试次数。可以重试 N 次后投递到死信队列(DLQ),由单独程序消费告警或人工处理。
- 避免阻塞:如果单条消息处理慢,会影响分区消费顺序。方案:保存
pending状态,异步处理,或用独立线程池消费,但注意无序问题;对于转码任务通常不需要严格顺序,可以多线程并发。 - Kafka 性能原理:顺序写磁盘、零拷贝(sendfile)、分区并行、ISR 副本机制。
第三轮:电商订单事务、隔离级别与 WebFlux 阻塞陷阱
1. 下单流程与分布式事务
- 表设计:
order(订单主表)、order_item(订单明细)、stock(库存)、payment_transaction(支付流水)、user_coupon(用户优惠券)。 - 本地事务:用
@Transactional保证创建订单 + 扣减库存 + 标记优惠券在同一数据库事务中。但跨服务(支付、物流)无法用本地事务。 - 分布式事务方案:
- Seata AT 模式:通过全局事务协调,对业务侵入小。
- TCC:Try-Confirm-Cancel,适合强一致性,但实现复杂。
- 本地消息表 + 消息队列:在本地事务中写入业务表和消息表(同库),然后定时任务把消息发送到 MQ,消费者成功后删除消息。适合最终一致性场景(如通知物流)。
- 先扣库存后创建订单:如果订单创建失败,需要回滚库存。如果库存扣减在 Redis,还要同步数据库库存,使用 Lua 脚本保证原子性。
2. MySQL 事务隔离级别与幻读
- MySQL InnoDB 默认隔离级别:
REPEATABLE READ(可重复读)。 - 四种隔离级别:
- READ UNCOMMITTED(脏读)
- READ COMMITTED(不可重复读,可能读到其他事务已提交的新数据)
- REPEATABLE READ(可重复读,InnoDB 通过 MVCC 和间隙锁解决大部分幻读)
- SERIALIZABLE(串行化,性能差)
- 在可重复读下,同一事务内多次读取同样记录的结果一致,但可能出现“幻读”(两次读取结果行数不同)。InnoDB 使用当前读 + 间隙锁 / 临键锁来防止幻读,普通快照读不会出现幻读。
- 场景:用户查询“已支付”订单,如果查询事务在更新事务之前开启快照,则读不到已提交的新状态;如果要求读到最新已提交数据,则使用
LOCK IN SHARE MODE或FOR UPDATE当前读,会读到最新版本,并可能加锁。
3. MyBatis #{} 与 ${} 以及排序注入
#{}:编译为 JDBC 预编译语句的占位符?,自动加引号,防止 SQL 注入。${}:直接把字符串拼接到 SQL 语句,有注入风险,通常用于动态表名、列名、排序字段。- 排序场景:如果你允许用户传
order=asc/desc,可以使用#{order}参数化吗?不可以,因为#{}会变成'asc',SQL 变成 ORDER BY 'asc' 不是期望语义。正确做法是对排序字段和方向做白名单校验:只允许create_time,price,id等固定字段,方向只允许asc/desc,用三元表达式或字符串映射后拼进 SQL。
4. Spring WebFlux 调用阻塞接口的风险
- WebFlux 是响应式模型,所有操作应该非阻塞。如果使用阻塞的第三方接口(如普通 HTTP Client、JDBC 同步查询),会阻塞 event loop 线程(Netty 的 I/O 线程),导致整个服务不可用。
- 正确做法:
- 使用响应式客户端(WebClient)调用第三方。
- 如果无法改变第三方,用
Schedulers.boundedElastic()将阻塞调用调度到独立的弹性线程池,而不是 event loop。 - 使用
subscribeOn/publishOn控制执行线程。
- 风险:boundedElastic 默认线程数是 10×CPU 核数,如果阻塞时间过长且请求量大,线程池也会耗尽;线程切换增加上下文切换开销。最稳妥是同化为全链路响应式(Netty + R2DBC + WebClient)。
- 实际项目如果必须集成阻塞 SDK,建议把该服务独立成为同步微服务,或使用队列异步削峰,而不是在 WebFlux 里直接阻塞。
5. 其他知识点略解
- Spring Cloud 生态:GateWay 基于 WebFlux,功能包括路由、断言、过滤器。
- Resilience4j:提供熔断器、限流器、重试、隔舱等,取代 Hystrix。
- Flyway / Liquibase:数据库版本控制,SQL 变更脚本自动化。
- JUnit 5 / Mockito / AssertJ:单元测试与断言。
- Docker / Kubernetes:容器化部署与编排。
- Spark / Flink:大数据批处理与流处理。
- WebSocket:长连接推送,用于弹幕、消息通知。
- Protobuf:高效的二进制序列化,用于 gRPC/Thrift 微服务间通信。
面试官老张最后并没有直接说“不通过”,但“回去等通知”在大多数情况下就是没有下文了。谢飞机虽然背了不少八股,但在深入追问和实战设计上明显底气不足。如果你想顺利拿到大厂 Java 岗,请把上面这些技术点扎扎实实搞懂,并多动手做项目。别做只会背答案的水货,要做能解决问题的“老司机”。
更多推荐




所有评论(0)