谢飞机大闹大厂面试:从音视频缓存到微服务熔断的JVM奇遇记
谢飞机大闹大厂面试从音视频缓存到微服务熔断的JVM奇遇记“谢飞机是吧坐。”面试官老猫推了推眼镜面前摊开一台笔记本电脑屏幕上是谢飞机的简历“看了你的项目经历写过电商秒杀也搞过内容社区还碰过一点AIGC的图像处理。今天咱们不聊虚的就从你实际做过的场景开始。”谢飞机挺直腰板露出一个自信的笑容“好的面试官您尽管问我虽然有些地方不太熟但我学得很快。”老猫点点头目光落在第一行“你之前说你们社区UGC系统用户上传图片和视频特别多高峰期整个服务经常卡顿你当时是怎么排查和解决的先说说你用了什么监控手段和缓存策略吧。”第一轮社区UGC与音视频场景的缓存和日志排查“你的服务用的是Spring Boot用户上传视频后你如何用Spring Cache或者Caffeine对视频元数据做本地缓存如果缓存穿透了比如大量请求同时查询一个不存在的视频ID你会怎么处理”“你提到用Redis缓存了热门的视频流列表。如果这时候Redis集群发生主从切换或者某个key因为Big Key问题导致阻塞你如何用Micrometer和Prometheus监控到具体要监控哪些指标”“你们的日志框架用的是Logback和SLF4J线上突然出现了大量错误日志磁盘快满了你怎么通过Logback的配置来避免日志风暴同时你如何用ELK从这些海量日志里快速定位到某个视频上传失败的根本原因”谢飞机搓搓手“第一个问题……我用Caffeine做本地缓存value是视频信息对象。要是缓存穿透了我就用空值缓存把不存在的ID也缓存个空对象设置很短的过期时间。还有布隆过滤器不过那个我面试前刚看的还没实际用过……”老猫眼睛一亮“不错啊还知道空值缓存和布隆过滤器。那第二个问题呢”“第二个……Redis Big Key呃我知道大key会阻塞但是监控指标我一般就看下Redis的命中率和慢查询日志。Micrometer……好像是Spring Boot Actuator里带的吧我都是默认的/actuator/metrics具体要看哪个指标我得想想。”谢飞机额头微微冒汗“主从切换那个我好像遇到过但是当时是运维处理的我就看了下业务日志报错Connection reset。”老猫没打断继续问第三个问题。“Logback的话我配置过maxFileSize和maxHistory防止磁盘满。日志风暴……是不是要加个过滤器或者把error级别也做限流ELK的话我用Kibana搜过关键字比如‘uploadFailed’然后看堆栈但要说根本原因好像每次都是看有没有异常类型。”老猫嘴角微微上扬“行能说到这个程度基础还是有的。来我们换个场景。假设你接下来去了一家做电商供应链金融的公司他们的核心系统还在用Java 8和Spring MVC要接一个支付回调要求最终一致性并且支付金额不能出错。你怎么设计”第二轮支付与金融场景下的微服务、序列化与分布式事务“支付回调这个接口你会怎么保证幂等性如果用了Redis分布式锁锁过期了怎么办结合你了解的Resilience4j怎么处理下游服务超时”“你的服务要对接供应链金融的账务系统对方用的是Apache Thrift接口而你们内部是Spring Cloud OpenFeign。这个协议转换你会怎么做如果传输的数据用Protobuf序列化和Jackson比有什么优缺点在Java 11环境下序列化时你需要注意什么”“财务需要数据对账你的支付结果数据最终要同步到Elasticsearch供查询。你会用Kafka还是RabbitMQ作为消息队列如果使用Kafka如何保证消息不丢失如果使用RabbitMQ如何防止消息积压导致消费延迟”谢飞机深吸一口气“幂等性……我一般用订单号做唯一索引或者Redis setnx。锁过期的话……嗯可以加一个续期的线程就是看门狗嘛。Resilience4j我知道有熔断和限流超时的话可以配一个TimeLimiter。”老猫点点头“那协议转换呢”“协议转换……Thrift是二进制的OpenFeign是HTTP JSON。我可能会写一个适配器先反序列化Thrift再封装成内部DTO。Protobuf的话比Jackson性能好因为是二进制占用空间小但是调试的时候没法直接看到而且需要维护.proto文件。Java 11……好像有ZGC但是序列化好像没啥特别的注意”谢飞机声音越来越小。“消息队列呢”老猫追问。“Kafka吧吞吐量高。保证不丢失的话生产者要设置acksall消费者关闭自动提交手动offset。RabbitMQ积压的话可以多开几个消费者或者用惰性队列。”谢飞机越说越快显然这些背过。老猫轻轻敲了敲桌子“第三个场景假设你现在参与一个在线教育平台的重构他们的课程视频需要做转码并且要推流到CDN同时还要通过WebSocket实时推送学习进度。后端服务是用Spring WebFlux做的配了R2DBC。你来说说这个架构的要点”第三轮在线教育场景下的WebFlux、R2DBC与云原生部署“Spring WebFlux是基于响应式编程的和Spring MVC在编程模型上最大的区别是什么你的业务里有WebSocket推送如何用WebFlux实现背压”“你用R2DBC连数据库它和JDBC本质上有什么不同为什么在这种场景下宁愿用R2DBC而不是MyBatis如果要做数据库迁移Flyway和Liquibase在这中间会扮演什么角色”“最后这个平台要部署到Kubernetes集群里用GitHub Actions做CI/CD。你会怎么将你的Spring Boot应用容器化如果遇到Pod频繁重启你怎么用Jaeger和Zipkin做链路追踪来定位问题”谢飞机已经有点晕了“WebFlux……它是响应式的非阻塞的基于Netty。MVC是阻塞的每个请求一个线程。背压……就是消费者处理不过来的时候告诉生产者慢一点具体API我不太记得了。R2DBC的话是异步非阻塞的JDBCMyBatis是同步的会阻塞。”“Flyway和Liquibase……都是做版本控制数据库的Flyway比较轻量Liquibase支持多数据库反正就是用脚本管理表结构。”谢飞机挠挠头“容器化的话我会写Dockerfile用maven打包成jar然后基础镜像用openjdk:11-jre-slim。Pod重启的话我会看logs链路追踪嗯Jaeger可以看调用链Zipkin也有UI可以在里面搜traceId。”老猫叹了口气合上电脑“谢飞机啊你今天的表现……怎么说呢一些八股概念背得不错但一到真实业务场景就有点飘。你的技术面广而不深我这边需要能沉下心啃硬骨头的人。这样吧你先回去等通知我们后面还有几轮交叉面。”谢飞机站起来鞠了一躬“谢谢面试官我回去一定好好补课”老猫摆摆手望着谢飞机离开的背影喃喃自语“这孩子倒是有几分直率。可惜了要是能把R2DBC的背压机制说清楚我还真打算让他过。”参考答案与场景技术点详解第一轮社区UGC与音视频场景的缓存、监控与日志排查问题1Spring Cache / Caffeine 缓存与缓存穿透业务背景UGC社区中视频元数据标题、封面、作者、点赞数等被频繁读取。使用本地缓存Caffeine可以避免每次请求都查MySQL减少RT。但大量请求同时查询一个不存在的视频ID时如恶意攻击或缓存过期后被删除缓存没有命中请求直达数据库造成“穿透”。解决方案空值缓存即使数据库中不存在该视频也在缓存中写入一个空对象或特殊标记设置较短的过期时间比如5分钟。这样后续相同ID的请求会命中空值不会打到DB。布隆过滤器Bloom Filter在缓存前加一层布隆过滤器维护所有合法视频ID的集合。请求进来先判断ID是否在过滤器中如果不在直接拒绝。优点是内存占用极低缺点是无法删除元素可以用Counting Bloom Filter或定期重建。参数校验对于明显不合法的ID如负数、超长字符串直接返回错误。其他增强使用Spring Cache注解如Cacheable(cacheNamesvideoMeta, key#videoId)时底层配置Caffeine缓存管理器设置初始容量、最大容量、过期策略。注意Caffeine本身支持recordStats()可以监控命中率。问题2Redis Big Key、主从切换监控与Micrometer/Prometheus业务背景Redis缓存视频流列表如果某个用户ID下的视频ID列表非常大例如千万级该Key在Redis内部可能是一个大集合读写时会导致阻塞。同时Redis主从切换时客户端连接会瞬时中断需要监控到并快速恢复。监控指标使用MicrometerSpring Boot Actuator暴露的指标系统注册Redis相关指标。具体包括redis_operations_seconds操作耗时直方图如果某个操作耗时超过阈值如100ms说明可能有Big Key。redis_connections_seconds/redis_connections_idle连接池状态。redis_command_latency命令延迟。自定义指标记录执行KEYS、SMEMBERS等危险命令的次数。使用Prometheus拉取Actuator的/actuator/prometheus端点配置Alertmanager规则比如当redis_operations_seconds_count在5分钟内增长超过100次且耗时大于50ms时告警。主从切换监控Redis连接状态可以使用lettuce或jedis的拓扑刷新事件。在业务中捕获RedisConnectionFailureException利用Resilience4j重试机制配合spring.redis.lettuce.pool的心跳检测。同时注意Redis集群模式下的MOVED和ASK重定向客户端会自动处理。Big Key 排查在Redis CLI中执行redis-cli --bigkeys或者使用MEMORY USAGE key查看内存占用。优化方案是拆分大Key如按时间分桶、使用SCAN代替KEYS。问题3Logback日志风暴与ELK定位问题业务背景线上出现大量重复错误日志消耗磁盘IO导致服务变慢甚至磁盘满。Logback配置使用SizeAndTimeBasedRollingPolicy设置maxFileSize和maxHistory保留最近30天单文件不超过200MB。使用AsyncAppender异步写日志减少阻塞但注意队列满时的丢弃策略discardingThreshold。防止日志风暴自定义Filter基于速率限制例如Guava的RateLimiter过滤相同异常类型的日志。也可使用Logback的DuplicateMessageFilter在5秒内相同消息只记录一次。设置root levelINFO业务包内使用DEBUG只在特定包开启ERROR。ELK排查日志通过Filebeat采集到KafkaLogstash过滤器解析异常堆栈输出到Elasticsearch。在Kibana中搜索关键词videoId AND uploadFailed利用Kibana的KQL语法。查看时间跨度如果某个服务实例的日志都集中出现Connection timed out说明可能是网络问题或依赖服务挂掉。通过log.error(upload failed, videoId{}, videoId, e)打印上下文。利用JSON结构化日志LogstashEncoder让ES可以索引字段方便聚合。第二轮支付与金融场景下的幂等性、协议转换与消息队列问题4支付回调幂等性与RESTful服务的可靠性业务背景支付平台回调你的接口可能重复发送因为网络超时重试。必须保证同一笔订单只能被处理一次。幂等方案数据库唯一约束订单表增加payment_id字段创建唯一索引。重复插入时抛出异常捕获后返回成功。Redis SETNX用SET key value NX EX 60orderId:paymentId确保只有第一次能获得锁。但锁过期后如果业务还没处理完第二个请求进来会再次获得锁导致重复处理。解决方案是Redisson看门狗自动续期或者使用数据库乐观锁version字段。状态机把订单状态设计为UNPAID - PAYING - PAID只有当PAYING状态时回调才允许更新为PAID否则拒绝。Resilience4j当通知下游账务系统时使用Resilience4j TimeLimiter超时、CircuitBreaker熔断和Retry重试。例如配置TimeLimiter超时3秒。CircuitBreaker10秒内错误率超过50%则打开熔断熔断后快速失败避免拖垮调用方。Retry最多重试3次间隔指数退避。重试时注意幂等性下游也需要用同样的幂等键。问题5Thrift vs OpenFeignProtobuf vs Jackson业务背景供应链金融账务系统是老系统暴露Thrift接口内部数据结构用IDL定义。而你的服务是Spring Cloud微服务平时用OpenFeign调用JSON API。协议转换编写一个TCP客户端使用Apache Thrift生成的Client类先在本地将Thrift响应反序列化为Java对象再包装为内部DTO。将这个步骤封装在一个FinanceAccountClient接口中供Service调用。注意Thrift是长连接二进制协议需要连接池管理可以使用commons-pool2。如果并发高改用gRPCHTTP/2进行内部通信但对方是老系统只能写适配器。Protobuf vs JacksonProtobuf二进制格式体积小压缩后比JSON小3-10倍序列化/反序列化速度快通过生成的代码直接操作字节强类型。需要定义.proto文件并编译成Java类。缺点可读性差调试需要工具字段变更需要保证兼容性。JacksonJSON格式可读性好通用性强各种REST API都用它。性能相比Protobuf低体积大但对于大多数场景足够。Java 11注意事项如果使用Java 11的模块化系统JPMS需要确保.proto生成代码依赖的javax.annotation模块被正确引入。另外Protobuf的反射和动态消息DynamicMessage在Java 11下可能遇到模块反射限制需要使用--add-opens。如果使用RecordJava 16作为DTO注意Jackson的ParameterNamesModule或Lombok的ConstructorProperties。补充在Java 11中序列化还要注意JAXBJava Architecture for XML Binding在Java 11中已被移除如果历史代码中有XML序列化需要额外引入依赖。使用List.of()等不可变集合时Jackson 2.10以上版本才能正确反序列化。问题6Kafka vs RabbitMQ 保证消息不丢失与防积压业务背景支付结果需要异步同步到ES供财务查询。消息队列作为中间缓冲。选择理由Kafka吞吐量高适合日志、事件流、大数据场景。但配置多保证不丢失需要同时配置生产者和消费者。RabbitMQ功能丰富支持ACK、死信、延迟队列适合业务消息但吞吐量低于Kafka。Kafka不丢失生产者acksall等待所有副本确认retries0默认Integer.MAX_VALUEenable.idempotencetrue幂等生产者防止重复写入。Brokermin.insync.replicas2配合replication.factor3如果有一个副本挂了生产者会收到NotEnoughReplicasException。消费者enable.auto.commitfalse手动提交offset。在业务逻辑处理完后再提交。如果处理失败则seek到当前记录重新消费。RabbitMQ防积压增加消费者数量提高并发度一个队列绑定多个消费者。使用basic.qos控制预取数量prefetchCount避免单个消费者累积大量消息。如果消费速度永久跟不上生产速度使用**惰性队列Lazy Queue**将消息存磁盘减少内存压力。监控RabbitMQ Management API的messages_ready数量超过阈值时动态扩容消费者K8s HPA。第三轮在线教育场景下的WebFlux、R2DBC与云原生问题7WebFlux响应式编程与WebSocket背压对比 Spring MVCSpring MVC基于Servlet每个请求占用一个线程线程池有限高并发下容易阻塞。WebFlux基于ReactorNetty使用事件循环线程用少量线程支撑大量连接。编程模型MVC是命令式同步WebFlux是声明式异步返回MonoT或FluxT。WebSocket 背压WebFlux的WebSocketHandler处理消息流WebSocketMessage作为Flux的一部分。背压是响应式流的核心——消费者通过request(n)告诉生产者每次最多发多少个数据项。例如直播弹幕场景用Flux.create(fluxSink - {...}, FluxSink.OverflowStrategy.BACK_PRESSURE)生成者速率快于消费者时会通过背压信号反馈。但注意WebSocket协议本身不支持背压所以只能在应用层模拟当服务端处理不过来时可以丢弃非重要消息、合并消息批次、或者降低发送频率。实际中可以用onBackpressureBuffer(1000)或onBackpressureDrop()。R2DBC与MyBatis本质区别JDBC/MyBatis是同步阻塞的每个数据库连接在执行SQL时会被占用直到结果返回。连接池数量有限线程等待在高并发下会积压。R2DBC是反应式数据库驱动基于Reactive Streams非阻塞地执行SQL。不占用线程等待数据库响应而是当结果到达时回调。使用R2DBC时整个链路必须都是响应式的从Controller到Repository如果中间用了block()就会阻塞EventLoop线程破坏架构。MyBatis也有异步版本MyBatis Reactive或结合Spring的Async但本质上还是线程池。Flyway / Liquibase在数据库版本管理层面Flyway使用版本化SQL脚本V1__init.sql, V2__add_column.sql按顺序执行。适合简单快速。Liquibase使用XML/YAML/JSON描述变更支持自动生成回滚脚本适合复杂数据库支持存储过程、视图。在R2DBC项目中Flyway/Liquibase可以在应用启动时连接一个JDBC连接非R2DBC执行脚本因为Flyway本身是同步的。但它们不受WebFlux的非阻塞限制因为只在启动时运行一次。问题8容器化与Kubernetes定位问题Dockerfile示例FROM maven:3.8-eclipse-temurin-11 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:11-jre WORKDIR /app COPY --frombuilder /app/target/online-education.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -XX:UseContainerSupport, -XX:MaxRAMPercentage75, -jar, app.jar]使用镜像时注意UseContainerSupport让JVM识别容器内存限制。CI/CD GitHub Actions工作流中构建镜像并推送仓库然后通过kubectl set image deployment滚动更新。Pod频繁重启排查先看kubectl describe pod的Events判断是OOM还是健康检查失败。看kubectl logs --previoustrue查看上一次退出的日志。Jaeger / Zipkin链路追踪在Spring Cloud中集成sleuth给每个请求分配traceId。调用链数据发送到Jaeger UI搜索traceId可以看到每个Span服务调用的耗时。如果某个服务如视频转码服务的Span都显示超时说明瓶颈在那里。监控金丝雀发布时用Prometheus对比新旧版本的P99延迟。总结谢飞机在面试中暴露的问题概念知道但无法深入尤其是“为什么”和“怎么做”的细节。对于求职Java后端岗位不仅要知道工具名称还要理解原理和结合业务设计。面试官的问题环环相扣从具体场景出发考察候选人的架构意识和排查能力。如果能完整回答出以上要点特别是R2DBC背压、Thrift转OpenFeign的适配器、Kafka幂等生产者配置那么这份工作基本稳了。而谢飞机还得回去继续啃《Spring in Action》和《Java并发编程实战》——至少他记住了面试官的最后一句话“回去等通知。”